From ba2c08b1dcc61cbdc77cf9b0676d1b2834d80950 Mon Sep 17 00:00:00 2001 From: iisaev Date: Mon, 28 Sep 2026 10:48:06 +0300 Subject: [PATCH] new battle gui --- locale/ui_en.properties | 46 +++++ locale/ui_ru.properties | 46 +++++ scenes/ff_action_detail_panel.tscn | 94 +++++++++ scenes/ff_battle_banner.tscn | 26 +++ scenes/ff_battle_hud.tscn | 88 +++++++++ scenes/ff_battle_log_panel.tscn | 65 ++++++ scenes/ff_battle_target_hint.tscn | 35 ++++ scenes/ff_character_widget.tscn | 160 ++++++++++++--- scenes/ff_menu_panel.tscn | 10 + src/Actions/FFDamageExecutor.cs | 5 +- src/Actions/FFStunExecutor.cs | 4 +- src/FFBattleCore.cs | 297 +++++++++++++++++++++++++++- src/UI/FFActionDetailPanel.cs | 179 +++++++++++++++++ src/UI/FFBattleBanner.cs | 52 +++++ src/UI/FFBattleHud.cs | 89 +++++++++ src/UI/FFBattleLogKind.cs | 19 ++ src/UI/FFBattleLogPanel.cs | 130 ++++++++++++ src/UI/FFBattleTargetHint.cs | 51 +++++ src/UI/FFCharacterWidget.cs | 305 ++++++++++++++++++++++------- src/UI/FFEnemyPanel.cs | 2 +- src/UI/FFMenuPanel.cs | 28 ++- src/UI/FFPlayerPanel.cs | 2 +- 22 files changed, 1613 insertions(+), 120 deletions(-) create mode 100644 scenes/ff_action_detail_panel.tscn create mode 100644 scenes/ff_battle_banner.tscn create mode 100644 scenes/ff_battle_hud.tscn create mode 100644 scenes/ff_battle_log_panel.tscn create mode 100644 scenes/ff_battle_target_hint.tscn create mode 100644 src/UI/FFActionDetailPanel.cs create mode 100644 src/UI/FFBattleBanner.cs create mode 100644 src/UI/FFBattleHud.cs create mode 100644 src/UI/FFBattleLogKind.cs create mode 100644 src/UI/FFBattleLogPanel.cs create mode 100644 src/UI/FFBattleTargetHint.cs diff --git a/locale/ui_en.properties b/locale/ui_en.properties index e38ad1e..4234c22 100644 --- a/locale/ui_en.properties +++ b/locale/ui_en.properties @@ -12,3 +12,49 @@ ff_battle/select_target = Select a target ff_battle/escape_fail = Escape failed! ff_battle/escape_success = You escaped! ff_battle/no_actions = No actions + +ff_battle/hud_title = Battle +ff_battle/round = Round +ff_battle/enemies = Enemies +ff_battle/allies = Party +ff_battle/phase_player = Player turn +ff_battle/phase_enemy = Enemy turn +ff_battle/banner_player = Player turn +ff_battle/banner_enemy = Enemy turn +ff_battle/hint_target = Select a target · RMB / Esc — cancel +ff_battle/hint_ally = Select an ally · RMB / Esc — cancel +ff_battle/menu_caption = {0} · AP {1}/{2} +ff_battle/menu_actions = Actions + +ff_battle/log_title = Battle log +ff_battle/log_round = round {0} +ff_battle/log_battle_start = Battle started +ff_battle/log_turn_player = — Player turn · round {0} — +ff_battle/log_turn_enemy = — Enemy turn · round {0} — +ff_battle/log_action = {0}: {1} → {2} +ff_battle/log_action_self = {0}: {1} +ff_battle/log_damage = {0}: {1} → {2} · -{3} HP +ff_battle/log_shield = {0}'s shield absorbed {1} · left {2} +ff_battle/log_defence = Defend: {0} · +{1} DEF (total {2}) +ff_battle/log_stun = Stunned: {0} · {1} turns +ff_battle/log_stun_skip = {0} is stunned — skips the turn +ff_battle/log_heal = {0}: {1} restored {2} HP +ff_battle/log_death = {0} defeated +ff_battle/log_escape_success = Escape succeeded +ff_battle/log_escape_failed = Escape failed! {0} loses 1 AP +ff_battle/log_no_points = The party is out of AP + +ff_battle/detail_attack = Character action submenu. AP cost is shown next to the name. +ff_battle/detail_items = 1 AP per item. Targeted items require an ally. +ff_battle/detail_defend = Shortcut: the ally-target action is applied to self. Shield +4 DEF. +ff_battle/detail_escape = 70% success chance. On failure the character loses 1 AP. +ff_battle/detail_back = Return to the main command menu. +ff_battle/detail_action = Special action without numeric parameters. +ff_battle/detail_item = An item from the shared inventory. +ff_battle/detail_damage = Damage {0} (ATK {1} × {2}) · cost {3} AP +ff_battle/detail_stun = Stun for {0} turns · cost {1} AP +ff_battle/detail_defence = Target shield +{0} · cost {1} AP + +ff_battle/status_stun = Stunned {0} +ff_battle/badge_turns = {0} turns +ff_battle/badge_self = Self diff --git a/locale/ui_ru.properties b/locale/ui_ru.properties index a3570c6..3b5fe6c 100644 --- a/locale/ui_ru.properties +++ b/locale/ui_ru.properties @@ -12,3 +12,49 @@ ff_battle/select_target=Выберите цель ff_battle/escape_fail=Побег не удался! ff_battle/escape_success=Вы сбежали! ff_battle/no_actions=Нет действий + +ff_battle/hud_title=Бой +ff_battle/round=Раунд +ff_battle/enemies=Врагов +ff_battle/allies=Отряд +ff_battle/phase_player=Ход игрока +ff_battle/phase_enemy=Ход врага +ff_battle/banner_player=Ход игрока +ff_battle/banner_enemy=Ход врага +ff_battle/hint_target=Выберите цель · ПКМ / Esc — отмена +ff_battle/hint_ally=Выберите союзника · ПКМ / Esc — отмена +ff_battle/menu_caption={0} · ОД {1}/{2} +ff_battle/menu_actions=Действия + +ff_battle/log_title=Журнал боя +ff_battle/log_round=раунд {0} +ff_battle/log_battle_start=Бой начался +ff_battle/log_turn_player=— Ход игрока · раунд {0} — +ff_battle/log_turn_enemy=— Ход врага · раунд {0} — +ff_battle/log_action={0}: {1} → {2} +ff_battle/log_action_self={0}: {1} +ff_battle/log_damage={0}: {1} → {2} · −{3} HP +ff_battle/log_shield=Щит {0} поглотил {1} · осталось {2} +ff_battle/log_defence=Защита: {0} · +{1} ЗАЩ (всего {2}) +ff_battle/log_stun=Ошеломление: {0} · {1} хода +ff_battle/log_stun_skip={0} ошеломлён — пропускает ход +ff_battle/log_heal={0}: {1} восстановил {2} HP +ff_battle/log_death={0} повержен +ff_battle/log_escape_success=Побег удался +ff_battle/log_escape_failed=Побег не удался! {0} теряет 1 ОД +ff_battle/log_no_points=У отряда закончились ОД + +ff_battle/detail_attack=Подменю действий персонажа. Стоимость в ОД указана справа от названия. +ff_battle/detail_items=Расход 1 ОД за предмет. Предметы «по цели» требуют выбора союзника. +ff_battle/detail_defend=Шорткат: действие «по союзнику» применяется на себя. Щит +4 ЗАЩ. +ff_battle/detail_escape=Шанс успеха 70%. При провале персонаж теряет 1 ОД. +ff_battle/detail_back=Вернуться в главное меню команд. +ff_battle/detail_action=Особое действие без числовых параметров. +ff_battle/detail_item=Предмет из общего инвентаря. +ff_battle/detail_damage=Урон {0} (АТК {1} × {2}) · расход {3} ОД +ff_battle/detail_stun=Ошеломление на {0} хода · расход {1} ОД +ff_battle/detail_defence=Щит цели +{0} · расход {1} ОД + +ff_battle/status_stun=Ошеломлён {0} +ff_battle/badge_turns={0} хода +ff_battle/badge_self=На себя diff --git a/scenes/ff_action_detail_panel.tscn b/scenes/ff_action_detail_panel.tscn new file mode 100644 index 0000000..9aab6f3 --- /dev/null +++ b/scenes/ff_action_detail_panel.tscn @@ -0,0 +1,94 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/FFActionDetailPanel.cs" id="1_script"] + +[node name="FFActionDetailPanel" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(300, 160) +layout_mode = 3 +mouse_filter = 2 + +[node name="Box" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_constants/separation = 10 + +[node name="Card" type="PanelContainer" parent="Box"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="CardRow" type="HBoxContainer" parent="Box/Card"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="Stripe" type="ColorRect" parent="Box/Card/CardRow"] +custom_minimum_size = Vector2(4, 0) +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 + +[node name="IconMargin" type="MarginContainer" parent="Box/Card/CardRow"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 12 +theme_override_constants/margin_bottom = 8 + +[node name="CardIcon" type="TextureRect" parent="Box/Card/CardRow/IconMargin"] +custom_minimum_size = Vector2(52, 52) +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 5 + +[node name="Body" type="VBoxContainer" parent="Box/Card/CardRow"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="NameRow" type="HBoxContainer" parent="Box/Card/CardRow/Body"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="NameLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 14 +clip_text = true +text_overrun_behavior = 3 + +[node name="IdLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 9 + +[node name="Badges" type="HFlowContainer" parent="Box/Card/CardRow/Body"] +layout_mode = 2 +theme_override_constants/h_separation = 5 +theme_override_constants/v_separation = 5 + +[node name="Props" type="HFlowContainer" parent="Box/Card/CardRow/Body"] +layout_mode = 2 +theme_override_constants/h_separation = 5 +theme_override_constants/v_separation = 3 + +[node name="Note" type="PanelContainer" parent="Box"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="NoteLabel" type="Label" parent="Box/Note"] +layout_mode = 2 +theme_override_font_sizes/font_size = 12 +autowrap_mode = 3 diff --git a/scenes/ff_battle_banner.tscn b/scenes/ff_battle_banner.tscn new file mode 100644 index 0000000..76baac8 --- /dev/null +++ b/scenes/ff_battle_banner.tscn @@ -0,0 +1,26 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/FFBattleBanner.cs" id="1_script"] + +[node name="FFBattleBanner" type="Control" script=ExtResource("1_script")] +layout_mode = 3 +mouse_filter = 2 + +[node name="Center" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Panel" type="PanelContainer" parent="Center"] +layout_mode = 2 +mouse_filter = 2 + +[node name="Label" type="Label" parent="Center/Panel"] +layout_mode = 2 +theme_override_font_sizes/font_size = 34 +horizontal_alignment = 1 +uppercase = true diff --git a/scenes/ff_battle_hud.tscn b/scenes/ff_battle_hud.tscn new file mode 100644 index 0000000..5f48277 --- /dev/null +++ b/scenes/ff_battle_hud.tscn @@ -0,0 +1,88 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/FFBattleHud.cs" id="1_script"] + +[node name="FFBattleHud" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(380, 92) +layout_mode = 3 +mouse_filter = 2 + +[node name="Panel" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Column" type="VBoxContainer" parent="Panel"] +layout_mode = 2 +theme_override_constants/separation = 9 + +[node name="TopRow" type="HBoxContainer" parent="Panel/Column"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="TitleLabel" type="Label" parent="Panel/Column/TopRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "ff_battle/hud_title" +auto_translate = true +uppercase = true + +[node name="PhaseBadge" type="PanelContainer" parent="Panel/Column/TopRow"] +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="PhaseLabel" type="Label" parent="Panel/Column/TopRow/PhaseBadge"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +uppercase = true + +[node name="MetaRow" type="HBoxContainer" parent="Panel/Column"] +layout_mode = 2 +theme_override_constants/separation = 12 + +[node name="RoundCaption" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "ff_battle/round" +auto_translate = true + +[node name="RoundValue" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "1" + +[node name="EnemiesCaption" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "ff_battle/enemies" +auto_translate = true + +[node name="EnemiesValue" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "0" + +[node name="AlliesCaption" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "ff_battle/allies" +auto_translate = true + +[node name="AlliesValue" type="Label" parent="Panel/Column/MetaRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "0" diff --git a/scenes/ff_battle_log_panel.tscn b/scenes/ff_battle_log_panel.tscn new file mode 100644 index 0000000..162db4c --- /dev/null +++ b/scenes/ff_battle_log_panel.tscn @@ -0,0 +1,65 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/FFBattleLogPanel.cs" id="1_script"] + +[node name="FFBattleLogPanel" type="Control" script=ExtResource("1_script")] +layout_mode = 3 +mouse_filter = 2 + +[node name="Window" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Column" type="VBoxContainer" parent="Window"] +layout_mode = 2 +theme_override_constants/separation = 0 + +[node name="Header" type="MarginContainer" parent="Window/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_top = 13 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_bottom = 10 + +[node name="HeaderRow" type="HBoxContainer" parent="Window/Column/Header"] +layout_mode = 2 +mouse_filter = 2 + +[node name="TitleLabel" type="Label" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "ff_battle/log_title" +auto_translate = true +uppercase = true + +[node name="Spacer" type="Control" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 + +[node name="RoundLabel" type="Label" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 + +[node name="Rule" type="HSeparator" parent="Window/Column"] +layout_mode = 2 + +[node name="Scroll" type="ScrollContainer" parent="Window/Column"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 0 +mouse_filter = 2 + +[node name="Rows" type="VBoxContainer" parent="Window/Column/Scroll"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_constants/separation = 0 +mouse_filter = 2 diff --git a/scenes/ff_battle_target_hint.tscn b/scenes/ff_battle_target_hint.tscn new file mode 100644 index 0000000..648e20e --- /dev/null +++ b/scenes/ff_battle_target_hint.tscn @@ -0,0 +1,35 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/FFBattleTargetHint.cs" id="1_script"] + +[node name="FFBattleTargetHint" type="Control" script=ExtResource("1_script")] +layout_mode = 3 +mouse_filter = 2 + +[node name="Center" type="CenterContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Toast" type="PanelContainer" parent="Center"] +layout_mode = 2 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Center/Toast"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="IconLabel" type="Label" parent="Center/Toast/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 14 +text = "◎" + +[node name="TextLabel" type="Label" parent="Center/Toast/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 13 diff --git a/scenes/ff_character_widget.tscn b/scenes/ff_character_widget.tscn index 3316aa2..6e2e659 100644 --- a/scenes/ff_character_widget.tscn +++ b/scenes/ff_character_widget.tscn @@ -5,51 +5,155 @@ [node name="FFCharacterWidget" type="Control" script=ExtResource("1_script")] custom_minimum_size = Vector2(180, 260) layout_mode = 3 +mouse_filter = 0 -[node name="Sprite" type="TextureRect" parent="."] +[node name="TeamLine" type="ColorRect" parent="."] +layout_mode = 1 +anchor_right = 1.0 +offset_bottom = 2.0 +mouse_filter = 2 + +[node name="Art" type="TextureRect" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 +offset_left = 3.0 +offset_top = 3.0 +offset_right = -3.0 +offset_bottom = -92.0 grow_horizontal = 2 grow_vertical = 2 -offset_left = 8.0 -offset_top = 8.0 -offset_right = -8.0 -offset_bottom = -44.0 mouse_filter = 2 expand_mode = 1 stretch_mode = 5 -[node name="HpBg" type="ColorRect" parent="."] -layout_mode = 0 -offset_left = 4.0 -offset_top = 218.0 -offset_right = 176.0 -offset_bottom = 236.0 +[node name="Info" type="PanelContainer" parent="."] +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -92.0 +grow_horizontal = 2 +grow_vertical = 0 mouse_filter = 2 -color = Color(0.137255, 0.145098, 0.184314, 1) -[node name="HpFill" type="ColorRect" parent="."] -layout_mode = 0 -offset_left = 4.0 -offset_top = 218.0 -offset_right = 176.0 -offset_bottom = 236.0 +[node name="Column" type="VBoxContainer" parent="Info"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="NameRow" type="HBoxContainer" parent="Info/Column"] +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="TeamDot" type="Panel" parent="Info/Column/NameRow"] +custom_minimum_size = Vector2(7, 7) +layout_mode = 2 +size_flags_vertical = 4 mouse_filter = 2 -color = Color(0.560784, 0.682353, 0.419608, 1) -[node name="NameLabel" type="Label" parent="."] -layout_mode = 0 -offset_left = 4.0 -offset_top = 238.0 -offset_right = 176.0 -offset_bottom = 256.0 +[node name="NameLabel" type="Label" parent="Info/Column/NameRow"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 14 +clip_text = true +text_overrun_behavior = 3 + +[node name="HpNumLabel" type="Label" parent="Info/Column/NameRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 + +[node name="HpBar" type="ProgressBar" parent="Info/Column"] +custom_minimum_size = Vector2(0, 8) +layout_mode = 2 +size_flags_vertical = 4 mouse_filter = 2 -horizontal_alignment = 1 -theme_override_font_sizes/font_size = 13 +show_percentage = false -[node name="Selection" type="TextureRect" parent="."] +[node name="Foot" type="HBoxContainer" parent="Info/Column"] +layout_mode = 2 +theme_override_constants/separation = 6 + +[node name="PipsBox" type="HBoxContainer" parent="Info/Column/Foot"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_constants/separation = 4 + +[node name="AtkLabel" type="Label" parent="Info/Column/Foot"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 + +[node name="Spacer" type="Control" parent="Info/Column/Foot"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 + +[node name="ShieldChip" type="PanelContainer" parent="Info/Column/Foot"] +visible = false +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="ShieldRow" type="HBoxContainer" parent="Info/Column/Foot/ShieldChip"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="ShieldKey" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "ui/character_panel/stat_def" +auto_translate = true +uppercase = true + +[node name="ShieldValue" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 +text = "0" + +[node name="StatusBox" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -140.0 +offset_top = 8.0 +offset_right = -8.0 +offset_bottom = 40.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="StunBadge" type="PanelContainer" parent="StatusBox"] +visible = false +layout_mode = 2 +size_flags_horizontal = 8 +mouse_filter = 2 + +[node name="StunRow" type="HBoxContainer" parent="StatusBox/StunBadge"] +layout_mode = 2 +theme_override_constants/separation = 5 + +[node name="StunIcon" type="TextureRect" parent="StatusBox/StunBadge/StunRow"] +custom_minimum_size = Vector2(14, 14) +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 5 + +[node name="StunLabel" type="Label" parent="StatusBox/StunBadge/StunRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +uppercase = true + +[node name="Selection" type="Panel" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 diff --git a/scenes/ff_menu_panel.tscn b/scenes/ff_menu_panel.tscn index a4f83ef..d6050a2 100644 --- a/scenes/ff_menu_panel.tscn +++ b/scenes/ff_menu_panel.tscn @@ -34,3 +34,13 @@ mouse_filter = 2 visible = false theme_override_font_sizes/font_size = 18 theme_override_colors/font_color = Color(0.941176, 0.756863, 0.411765, 1) + +[node name="Caption" type="Label" parent="."] +offset_left = 2.0 +offset_top = -25.0 +offset_right = 218.0 +offset_bottom = -7.0 +mouse_filter = 2 +visible = false +theme_override_font_sizes/font_size = 11 +uppercase = true diff --git a/src/Actions/FFDamageExecutor.cs b/src/Actions/FFDamageExecutor.cs index 772e446..a2747ac 100644 --- a/src/Actions/FFDamageExecutor.cs +++ b/src/Actions/FFDamageExecutor.cs @@ -1,3 +1,4 @@ +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Characters; using Cthangover.Core.Utils; using Godot; @@ -23,8 +24,8 @@ namespace Cthangover.FFBattle.Actions return new ChangedAttributes { Result = false }; var damageDelta = Mathf.RoundToInt(action.GetFloat("Attack", 1f) * user.Attributes.Attack.Value); - target.StatusEffectQueue.OnTakeDamage(user, ref damageDelta); - user.StatusEffectQueue.OnDealDamage(target, ref damageDelta); + StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta); + StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta); var defenceDelta = 0; var ignoreArmor = user.HasPassiveEffect("IgnoreArmor"); diff --git a/src/Actions/FFStunExecutor.cs b/src/Actions/FFStunExecutor.cs index 44d71cb..c19d7ae 100644 --- a/src/Actions/FFStunExecutor.cs +++ b/src/Actions/FFStunExecutor.cs @@ -1,3 +1,4 @@ +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Characters; using Cthangover.Core.Utils; @@ -23,8 +24,7 @@ namespace Cthangover.FFBattle.Actions return new ChangedAttributes { Result = true }; var turns = action.GetInt("Turn", 3); - target.StatusEffectQueue.Add("effect/physics/stun", turns); - + StatusEffectService.Of(target).Add("effect/physics/stun", turns); GameLogger.Log("FF_BATTLE", $"{user.Name} stuns {target.Name} for {turns} turns", LogLevel.Debug); diff --git a/src/FFBattleCore.cs b/src/FFBattleCore.cs index 0b5d733..d451014 100644 --- a/src/FFBattleCore.cs +++ b/src/FFBattleCore.cs @@ -4,10 +4,13 @@ using System.Linq; using System.Threading.Tasks; using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Characters; using Cthangover.Core.Items; using Cthangover.Core.Settings; using Cthangover.Core.Scenes; +using Cthangover.Core.UI; +using Cthangover.Core.UI.CharacterPanel; using Cthangover.Core.Utils; using Cthangover.FFBattle.UI; using Cthangover.Core.UI.Theming; @@ -38,6 +41,11 @@ namespace Cthangover.FFBattle private FFPlayerPanel _playerPanel; private FFEnemyPanel _enemyPanel; private FFMenuPanel _menuPanel; + private FFActionDetailPanel _detailPanel; + private FFBattleHud _hud; + private FFBattleLogPanel _logPanel; + private FFBattleBanner _banner; + private FFBattleTargetHint _targetHint; private FFBattleController _controller; private Control _toolPanel; @@ -49,6 +57,12 @@ namespace Cthangover.FFBattle private bool _targetSelectMode; private bool _targetAlly; private System.Action _onTargetSelected; + private int _round; + private int _detailKind; + + private const int DetailMain = 0; + private const int DetailActions = 1; + private const int DetailItems = 2; private const float PLAYER_SCALE = 0.85f; @@ -91,6 +105,33 @@ namespace Cthangover.FFBattle _menuPanel.HideMenu(); panel.AddChild(_menuPanel); + _detailPanel = (FFActionDetailPanel)TscnScenes.LoadAndBuild("scenes/ff_action_detail_panel.tscn"); + _detailPanel.Name = "FFActionDetailPanel"; + _detailPanel.EnsureConstructed(); + _detailPanel.HidePassport(); + panel.AddChild(_detailPanel); + + _hud = (FFBattleHud)TscnScenes.LoadAndBuild("scenes/ff_battle_hud.tscn"); + _hud.Name = "FFBattleHud"; + _hud.EnsureConstructed(); + panel.AddChild(_hud); + + _logPanel = (FFBattleLogPanel)TscnScenes.LoadAndBuild("scenes/ff_battle_log_panel.tscn"); + _logPanel.Name = "FFBattleLogPanel"; + _logPanel.EnsureConstructed(); + panel.AddChild(_logPanel); + + _banner = (FFBattleBanner)TscnScenes.LoadAndBuild("scenes/ff_battle_banner.tscn"); + _banner.Name = "FFBattleBanner"; + _banner.EnsureConstructed(); + panel.AddChild(_banner); + + _targetHint = (FFBattleTargetHint)TscnScenes.LoadAndBuild("scenes/ff_battle_target_hint.tscn"); + _targetHint.Name = "FFBattleTargetHint"; + _targetHint.EnsureConstructed(); + _targetHint.HideHint(); + panel.AddChild(_targetHint); + _endTurnButton = new Button(); _endTurnButton.Text = TranslationServer.Translate("ff_battle/end_turn"); _endTurnButton.Pressed += OnEndTurnPressed; @@ -112,6 +153,7 @@ namespace Cthangover.FFBattle _enemyPanel.OnEnemyClicked += OnEnemyClicked; _enemyPanel.OnEnemyDead += OnEnemyDied; _menuPanel.OnItemSelected += OnMenuItemSelected; + _menuPanel.OnSelectionChanged += OnMenuSelectionChanged; _menuPanel.OnCancelled += OnMenuCancel; } @@ -138,6 +180,20 @@ namespace Cthangover.FFBattle _menuPanel.Position = new Vector2(viewportSize.X * 0.66f, viewportSize.Y * 0.58f); _menuPanel.Size = new Vector2(viewportSize.X * 0.3f, 160f); + _detailPanel.Position = new Vector2(viewportSize.X * 0.66f - 320f, viewportSize.Y * 0.58f); + _detailPanel.Size = new Vector2(300f, 190f); + + _hud.Position = new Vector2(26f, 20f); + + _logPanel.Position = new Vector2(viewportSize.X - 466f, 64f); + _logPanel.Size = new Vector2(440f, 440f); + + _banner.Position = new Vector2(0f, viewportSize.Y * 0.43f); + _banner.Size = new Vector2(viewportSize.X, 70f); + + _targetHint.Position = new Vector2(0f, viewportSize.Y * 0.48f); + _targetHint.Size = new Vector2(viewportSize.X, 40f); + if (_endTurnButton != null) _endTurnButton.Position = new Vector2(viewportSize.X - 180f, viewportSize.Y * 0.92f); @@ -147,6 +203,9 @@ namespace Cthangover.FFBattle BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared; _isPlayerTurn = true; + _round = 0; + UpdateHud(); + _logPanel.AddEntry(Translate("ff_battle/log_battle_start"), FFBattleLogKind.Turn); StartPlayerTurn(); } @@ -160,13 +219,15 @@ namespace Cthangover.FFBattle _currentCharacter = null; _targetSelectMode = false; _menuPanel.HideMenu(); + _detailPanel.HidePassport(); + _targetHint.HideHint(); BattleSceneContext.Instance.ActiveCharacter = null; foreach (var widget in _playerPanel.Widgets) { if (!widget.IsDead) { - widget.Card.StatusEffectQueue.OnTurnStart(); + StatusEffectService.Of(widget.Card).OnTurnStart(); widget.Card.Attributes.Point.Value = widget.Card.Attributes.Point.BaseValue; } widget.UpdateInfo(); @@ -180,6 +241,13 @@ namespace Cthangover.FFBattle foreach (var widget in _playerPanel.Widgets) widget.ClearHighlight(); + + _round += 1; + UpdateHud(); + _hud.SetPhase(true); + _banner.Show(Translate("ff_battle/banner_player")); + _logPanel.SetRound(_round); + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_player"), _round), FFBattleLogKind.Turn); } private void OnWidgetClicked(FFCharacterWidget widget) @@ -201,6 +269,7 @@ namespace Cthangover.FFBattle _targetSelectMode = false; foreach (var w in _enemyPanel.Widgets) w.ClearHighlight(); foreach (var w in _playerPanel.Widgets) w.ClearHighlight(); + _targetHint.HideHint(); ExecuteMenuAction(widget); return; @@ -250,12 +319,14 @@ namespace Cthangover.FFBattle }; _currentMenuAction = null; - _menuPanel.ShowMenu(entries); + _detailKind = DetailMain; + _menuPanel.ShowMenu(entries, string.Format(Translate("ff_battle/menu_caption"), + ActorName(), _currentCharacter.Card.Attributes.Point.Value, _currentCharacter.Card.Attributes.Point.BaseValue)); _controller.State = FFBattleState.MenuOpen; foreach (var widget in _playerPanel.Widgets) widget.ClearHighlight(); - _currentCharacter?.Highlight(UiTokens.Alpha(UiTokens.Info, 0.6f)); + _currentCharacter?.Highlight(UiTokens.Accent); } private void OnMenuItemSelected(int index) @@ -367,7 +438,8 @@ namespace Cthangover.FFBattle Enabled = true }); - _menuPanel.ShowMenu(entries); + _detailKind = DetailActions; + _menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_actions")); _controller.State = FFBattleState.SubMenuOpen; } @@ -408,7 +480,8 @@ namespace Cthangover.FFBattle Enabled = true }); - _menuPanel.ShowMenu(entries); + _detailKind = DetailItems; + _menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_items")); _controller.State = FFBattleState.SubMenuOpen; } @@ -418,6 +491,7 @@ namespace Cthangover.FFBattle _targetAlly = targetAlly; _menuPanel.HideMenu(); _controller.State = FFBattleState.TargetSelect; + _targetHint.ShowHint(Translate(targetAlly ? "ff_battle/hint_ally" : "ff_battle/hint_target")); foreach (var widget in _playerPanel.Widgets) widget.ClearHighlight(); @@ -428,13 +502,13 @@ namespace Cthangover.FFBattle { foreach (var widget in _playerPanel.Widgets) if (!widget.IsDead) - widget.Highlight(UiTokens.Alpha(UiTokens.Success, 0.5f)); + widget.Highlight(UiTokens.Success); } else { foreach (var widget in _enemyPanel.Widgets) if (!widget.IsDead) - widget.Highlight(UiTokens.Alpha(UiTokens.Danger, 0.5f)); + widget.Highlight(UiTokens.Danger); } _onTargetSelected = null; @@ -451,6 +525,7 @@ namespace Cthangover.FFBattle _targetSelectMode = false; foreach (var w in _enemyPanel.Widgets) w.ClearHighlight(); foreach (var w in _playerPanel.Widgets) w.ClearHighlight(); + _targetHint.HideHint(); ExecuteMenuAction(widget); } @@ -469,6 +544,7 @@ namespace Cthangover.FFBattle _targetSelectMode = false; foreach (var w in _enemyPanel.Widgets) w.ClearHighlight(); foreach (var w in _playerPanel.Widgets) w.ClearHighlight(); + _targetHint.HideHint(); ExecuteMenuAction(widget); } @@ -480,12 +556,22 @@ namespace Cthangover.FFBattle _controller.State = FFBattleState.AnimationPlaying; _endTurnButton.Visible = false; + _detailPanel.HidePassport(); + _targetHint.HideHint(); + + var source = _currentCharacter; + var actionName = string.Empty; + var hpBefore = target.Card.Attributes.Health.Value; + var defBefore = target.Card.Attributes.Defence.Value; + var stunBefore = CountStunTurns(target.Card); switch (_currentMenuAction.Key) { case "action": { var action = _currentMenuAction.Data as ActionCharacter; + actionName = TranslationServer.Translate(action.Name).ToString(); + LogActionDeclared(source, actionName, target); IBattleAction anim = null; if (action.Type == ActionCharacterType.ToEnemy) @@ -504,6 +590,8 @@ namespace Cthangover.FFBattle case "item": { var item = _currentMenuAction.Data as IItem; + actionName = TranslationServer.Translate(item.Name).ToString(); + LogActionDeclared(source, actionName, target); var dummyAction = new ActionCharacter { ID = "ff/item" }; var anim = new Actions.FFItemAnimation(_currentCharacter, target, dummyAction, item); await RunAnimation(anim); @@ -511,6 +599,8 @@ namespace Cthangover.FFBattle } } + LogOutcome(source, target, actionName, hpBefore, defBefore, stunBefore); + _currentMenuAction = null; _endTurnButton.Visible = true; @@ -537,12 +627,15 @@ namespace Cthangover.FFBattle private void DoEscapeAction() { _menuPanel.HideMenu(); + _detailPanel.HidePassport(); _controller.State = FFBattleState.Idle; var escapeRoll = (float)GD.RandRange(0, 100); if (escapeRoll < 70f) { GameLogger.Log("FF_BATTLE", "Escape succeeded", LogLevel.Debug); + _banner.Show(Translate("ff_battle/escape_success")); + _logPanel.AddEntry(Translate("ff_battle/log_escape_success"), FFBattleLogKind.Turn); _ctx.EndBattle(BattleSide.Player); } else @@ -550,6 +643,8 @@ namespace Cthangover.FFBattle GameLogger.Log("FF_BATTLE", "Escape failed", LogLevel.Debug); _currentCharacter.Card.Attributes.Point.Value -= 1; _currentCharacter.UpdateInfo(); + _banner.Show(Translate("ff_battle/escape_fail")); + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_escape_failed"), NameOf(_currentCharacter)), FFBattleLogKind.Status); _currentCharacter = null; BattleSceneContext.Instance.ActiveCharacter = null; TryNextCharacterOrEndTurn(); @@ -588,6 +683,7 @@ namespace Cthangover.FFBattle _targetSelectMode = false; foreach (var w in _enemyPanel.Widgets) w.ClearHighlight(); foreach (var w in _playerPanel.Widgets) w.ClearHighlight(); + _targetHint.HideHint(); ShowMainMenu(); return; } @@ -599,6 +695,7 @@ namespace Cthangover.FFBattle } _menuPanel.HideMenu(); + _detailPanel.HidePassport(); _controller.State = FFBattleState.Idle; _currentCharacter = null; _currentMenuAction = null; @@ -635,11 +732,12 @@ namespace Cthangover.FFBattle if (widget.IsDead || widget.Card?.Attributes?.Point?.Value <= 0) continue; - if (widget.Card.StatusEffectQueue.HasStun()) + if (StatusEffectService.Of(widget.Card).HasStun()) { GameLogger.Log("FF_BATTLE", $"{widget.Card.Name} is stunned, skipping", LogLevel.Debug); widget.Card.Attributes.Point.Value = 0; widget.UpdateInfo(); + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(widget)), FFBattleLogKind.Status); continue; } @@ -649,6 +747,7 @@ namespace Cthangover.FFBattle return; } + _logPanel.AddEntry(Translate("ff_battle/log_no_points"), FFBattleLogKind.Info); OnPlayerTurnEnd(); } @@ -668,6 +767,8 @@ namespace Cthangover.FFBattle _isPlayerTurn = false; _endTurnButton.Visible = false; _menuPanel.HideMenu(); + _detailPanel.HidePassport(); + _targetHint.HideHint(); _controller.State = FFBattleState.AnimationPlaying; BattleSceneContext.Instance.IsWait = true; @@ -676,6 +777,10 @@ namespace Cthangover.FFBattle foreach (var widget in _enemyPanel.Widgets) widget.ClearHighlight(); + _hud.SetPhase(false); + _banner.Show(Translate("ff_battle/banner_enemy")); + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_enemy"), _round), FFBattleLogKind.Turn); + var root = (Node)_ctx.RootNode; await root.ToSignal(root.GetTree(), "process_frame"); await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout"); @@ -694,11 +799,13 @@ namespace Cthangover.FFBattle if (enemyWidget.IsDead || enemyWidget.Card?.Actions == null || enemyWidget.Card.Actions.Count == 0) continue; - enemyWidget.Card.StatusEffectQueue.OnTurnStart(); + StatusEffectService.Of(enemyWidget.Card).OnTurnStart(); + enemyWidget.UpdateInfo(); - if (enemyWidget.Card.StatusEffectQueue.HasStun()) + if (StatusEffectService.Of(enemyWidget.Card).HasStun()) { GameLogger.Log("FF_BATTLE", $"{enemyWidget.Card.Name} is stunned, skipping", LogLevel.Debug); + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(enemyWidget)), FFBattleLogKind.Status); continue; } @@ -716,6 +823,12 @@ namespace Cthangover.FFBattle $"Enemy: {enemyWidget.Card.Name} → {action.Name} → {target.Card.Name}", LogLevel.Debug); + var actionName = TranslationServer.Translate(action.Name).ToString(); + LogActionDeclared(enemyWidget, actionName, target); + var hpBefore = target.Card.Attributes.Health.Value; + var defBefore = target.Card.Attributes.Defence.Value; + var stunBefore = CountStunTurns(target.Card); + var anim = CreateEnemyAnimation(enemyWidget, target, action); if (anim != null) { @@ -725,6 +838,8 @@ namespace Cthangover.FFBattle anim.DoEnd(); } + LogOutcome(enemyWidget, target, actionName, hpBefore, defBefore, stunBefore); + await root.ToSignal(root.GetTree().CreateTimer(0.2f), "timeout"); if (!_playerPanel.HasAlive()) @@ -803,6 +918,8 @@ namespace Cthangover.FFBattle ctx.NotifyCharacterDied(widget.Card); } + UpdateHudCounts(); + if (!_enemyPanel.HasAlive()) { GameLogger.Log("FF_BATTLE", "All enemies dead — player wins", LogLevel.Debug); @@ -852,10 +969,170 @@ namespace Cthangover.FFBattle _playerPanel?.ClearAll(); _enemyPanel?.ClearAll(); _menuPanel?.HideMenu(); + _detailPanel?.HidePassport(); + _targetHint?.HideHint(); + _logPanel?.Clear(); if (_endTurnButton != null) _endTurnButton.Visible = false; } + private void OnMenuSelectionChanged(int index) + { + if (_menuPanel.Entries == null || index < 0 || index >= _menuPanel.Entries.Count) + { + _detailPanel.HidePassport(); + return; + } + + var entry = _menuPanel.Entries[index]; + _detailPanel.ShowPassport(); + + if (_detailKind == DetailActions) + { + if (entry.Data is ActionCharacter action) + _detailPanel.ShowAction(action, BuildActionNote(action)); + else + _detailPanel.ShowHint(Translate("ff_battle/detail_back")); + return; + } + + if (_detailKind == DetailItems) + { + if (entry.Data is IItem item) + _detailPanel.ShowItem(item, item.ItemType.HasFlag(ItemType.TargetUsed), BuildItemNote(item)); + else + _detailPanel.ShowHint(Translate("ff_battle/detail_back")); + return; + } + + _detailPanel.ShowHint(MainHintFor(entry.Key)); + } + + private string BuildActionNote(ActionCharacter action) + { + if (action.Properties == null) + return TranslationServer.Translate("ff_battle/detail_action").ToString(); + + var cost = action.GetInt("RequiredPoint", 1); + var attack = action.GetFloat("Attack", 0f); + if (attack > 0f) + { + var attacker = _currentCharacter?.Card?.Attributes?.Attack?.Value ?? 0; + return string.Format(Translate("ff_battle/detail_damage"), + Mathf.RoundToInt(attack * attacker), attacker, attack.ToString("0.0"), cost); + } + + var turns = action.GetInt("Turn", 0); + if (turns > 0) + return string.Format(Translate("ff_battle/detail_stun"), turns, cost); + + var defence = action.GetInt("Defence", 0); + if (defence > 0) + return string.Format(Translate("ff_battle/detail_defence"), defence, cost); + + var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(action.Description)); + return !string.IsNullOrEmpty(description) ? description : Translate("ff_battle/detail_action"); + } + + private static string BuildItemNote(IItem item) + { + var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(item.Description)); + return !string.IsNullOrEmpty(description) + ? description + : TranslationServer.Translate("ff_battle/detail_item").ToString(); + } + + private static string MainHintFor(string key) + { + switch (key) + { + case "attack": return TranslationServer.Translate("ff_battle/detail_attack").ToString(); + case "items": return TranslationServer.Translate("ff_battle/detail_items").ToString(); + case "defend": return TranslationServer.Translate("ff_battle/detail_defend").ToString(); + case "escape": return TranslationServer.Translate("ff_battle/detail_escape").ToString(); + default: return TranslationServer.Translate("ff_battle/detail_back").ToString(); + } + } + + private void LogActionDeclared(FFCharacterWidget source, string actionName, FFCharacterWidget target) + { + var entry = source == target + ? string.Format(Translate("ff_battle/log_action_self"), NameOf(source), actionName) + : string.Format(Translate("ff_battle/log_action"), NameOf(source), actionName, NameOf(target)); + _logPanel.AddEntry(entry, FFBattleLogKind.Info); + } + + private void LogOutcome(FFCharacterWidget source, FFCharacterWidget target, string actionName, + int hpBefore, int defBefore, int stunBefore) + { + var hpAfter = target.Card.Attributes.Health.Value; + var defAfter = target.Card.Attributes.Defence.Value; + var stunAfter = CountStunTurns(target.Card); + + if (defAfter > defBefore) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_defence"), + NameOf(target), defAfter - defBefore, defAfter), FFBattleLogKind.Buff); + + var shieldLost = defBefore - defAfter; + if (shieldLost > 0) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_shield"), + NameOf(target), shieldLost, defAfter), FFBattleLogKind.Shield); + + if (stunAfter > stunBefore) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun"), + NameOf(target), stunAfter), FFBattleLogKind.Status); + + if (hpAfter > hpBefore) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_heal"), + actionName, NameOf(target), hpAfter - hpBefore), FFBattleLogKind.Buff); + + if (hpAfter < hpBefore) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_damage"), + actionName, NameOf(source), NameOf(target), hpBefore - hpAfter), FFBattleLogKind.Damage); + + if (hpAfter <= 0 && hpBefore > 0) + _logPanel.AddEntry(string.Format(Translate("ff_battle/log_death"), NameOf(target)), FFBattleLogKind.Dead); + + UpdateHudCounts(); + } + + private static int CountStunTurns(Character character) + { + var stun = StatusEffectService.Of(character)?.ActiveStatusEffects? + .FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun); + return stun?.RemainingTurns ?? 0; + } + + private string ActorName() + => _currentCharacter?.Card == null + ? string.Empty + : TranslationServer.Translate(_currentCharacter.Card.Name).ToString(); + + private static string NameOf(FFCharacterWidget widget) + => widget?.Card == null + ? string.Empty + : TranslationServer.Translate(widget.Card.Name).ToString(); + + private void UpdateHud() + { + if (_hud == null) + return; + _hud.SetRound(_round); + UpdateHudCounts(); + } + + private void UpdateHudCounts() + { + if (_hud == null) + return; + _hud.SetCounts( + _enemyPanel.Widgets.Count(w => !w.IsDead), + _playerPanel.Widgets.Count(w => !w.IsDead)); + } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); + /// /// true while the core accepts a player turn advance; flips to /// false synchronously at the start of . diff --git a/src/UI/FFActionDetailPanel.cs b/src/UI/FFActionDetailPanel.cs new file mode 100644 index 0000000..135f89a --- /dev/null +++ b/src/UI/FFActionDetailPanel.cs @@ -0,0 +1,179 @@ +using Cthangover.Core.Characters; +using Cthangover.Core.Items; +using Cthangover.Core.UI; +using Cthangover.Core.UI.CharacterPanel; +using Cthangover.Core.UI.Theming; +using Godot; + +namespace Cthangover.FFBattle.UI +{ + /// + /// Action/item passport shown to the left of the command menu. It mirrors the + /// action-card component of the unified style guide: a type accent stripe, the + /// action icon, name and ID, type/cost/cooldown badges, property chips and a + /// note line with the computed effect preview. The panel exists so the small + /// FF-style menu can stay a compact list while the player still sees what the + /// highlighted command actually does. Node structure lives in + /// scenes/ff_action_detail_panel.tscn; the class fills it and reuses + /// for badges and properties so the battle and + /// the character panel can never disagree about an action's presentation. + /// + public partial class FFActionDetailPanel : ModWidget + { + private PanelContainer _card; + private ColorRect _stripe; + private TextureRect _icon; + private Label _nameLabel; + private Label _idLabel; + private HFlowContainer _badges; + private HFlowContainer _props; + private PanelContainer _note; + private Label _noteLabel; + + protected override void Construct() + { + CthangoverTheme.Apply(this); + + _card = GetNode("Box/Card"); + _stripe = GetNode("Box/Card/CardRow/Stripe"); + _icon = GetNode("Box/Card/CardRow/IconMargin/CardIcon"); + _nameLabel = GetNode