diff --git a/collections/card_novel.json b/collections/card_novel.json new file mode 100644 index 0000000..f5f8677 --- /dev/null +++ b/collections/card_novel.json @@ -0,0 +1,39 @@ +{ + "id": "card_novel", + "name": "Card Novel", + "author": "ct", + "version": "1.0.0", + "description": "A visual novel with turn-based battles resolved as a card game: story core, battle engine, card layer and the novel interface.", + "mechanics": [ + { + "id": "vn_core", + "name": "Novel Core", + "description": "Scenes, scenarios, dialogue and the story engine.", + "mods": ["visual_novel_core>=1"] + }, + { + "id": "story", + "name": "Story", + "description": "The bundled story content.", + "mods": ["murakami>=1"] + }, + { + "id": "battle", + "name": "Turn Battle", + "description": "Turn-based combat foundation.", + "mods": ["turn_battle_base>=1"] + }, + { + "id": "cards", + "name": "Card Game", + "description": "Card hands, decks and card-driven battle actions.", + "mods": ["card_battle>=1"] + }, + { + "id": "interface", + "name": "Interface", + "description": "Shared GUI panels and the narrative HUD.", + "mods": ["interface>=1"] + } + ] +} diff --git a/locale/ui_en.properties b/locale/ui_en.properties index e2a74ae..4fbcf76 100644 --- a/locale/ui_en.properties +++ b/locale/ui_en.properties @@ -1,4 +1,39 @@ -mod_card_battle_name = CardBattle -mod_card_battle_desc = Card battle mechanics +mod_card_battle_name=CardBattle +mod_card_battle_desc=Card battle mechanics -battle/end_turn = End turn +battle/end_turn=End turn + +battle/hud_title=Card battle +battle/round=Round +battle/enemies_alive=Enemies alive +battle/turn_player=Player turn +battle/turn_enemy=Enemy turn + +battle/actions_title=Actions +battle/no_character=no character selected +battle/empty_actions=Select a character to see their actions +battle/desc_default=Hover a card to see the numbers. Drag it onto a target to execute. +battle/action_hint=LMB — select a character · drag an action card onto a target +battle/action_autoend=0 AP — the turn ends by itself + +battle/preview_damage=Damage {0} (ATK {1} × {2}) · cost {3} AP +battle/preview_defence=Target shield +{0} · cost {1} AP +battle/preview_stun=Stun for {0} turns · cost {1} AP +battle/turns_badge={0} turns +battle/stun_status=Stunned {0} + +battle/log_title=Battle log +battle/log_round=round {0} +battle/log_turn_player=— Player turn +battle/log_turn_enemy=— Enemy turn +battle/log_enemy_action={0}: {1} → {2} +battle/log_damage={0}: {1} → {2} · −{3} HP +battle/log_shield_absorb=Shield of {0} absorbed {1} +battle/log_shield_gain=Defence: {0} · +{1} DEF +battle/log_absorbed={0}: {1} → {2} · damage absorbed by shield +battle/log_stun=Stun: {0} · {1} turns +battle/log_stun_skip={0} is stunned — skips the turn +battle/log_dead={0} is defeated +battle/log_out_of_points=The party is out of action points +battle/log_invalid_target=Invalid target for "{0}" +battle/log_not_enough_points=Not enough AP for "{0}" diff --git a/locale/ui_ru.properties b/locale/ui_ru.properties index be650d0..4be24b8 100644 --- a/locale/ui_ru.properties +++ b/locale/ui_ru.properties @@ -1,4 +1,39 @@ mod_card_battle_name=Карточная битва mod_card_battle_desc=Механики карточных сражений -battle/end_turn=Конец хода \ No newline at end of file +battle/end_turn=Конец хода + +battle/hud_title=Карточная битва +battle/round=Раунд +battle/enemies_alive=Живых врагов +battle/turn_player=Ход игрока +battle/turn_enemy=Ход врага + +battle/actions_title=Действия +battle/no_character=персонаж не выбран +battle/empty_actions=Выберите персонажа, чтобы увидеть его действия +battle/desc_default=Наведите на карточку — покажу расчёт. Перетащите её на цель, чтобы выполнить. +battle/action_hint=ЛКМ — выбрать персонажа · карточка действия перетаскивается на цель +battle/action_autoend=0 ОД — ход завершится сам + +battle/preview_damage=Урон {0} (АТК {1} × {2}) · расход {3} ОД +battle/preview_defence=Щит цели +{0} · расход {1} ОД +battle/preview_stun=Ошеломление на {0} хода · расход {1} ОД +battle/turns_badge={0} хода +battle/stun_status=Ошеломлён {0} + +battle/log_title=Журнал боя +battle/log_round=раунд {0} +battle/log_turn_player=— Ход игрока +battle/log_turn_enemy=— Ход врага +battle/log_enemy_action={0}: {1} → {2} +battle/log_damage={0}: {1} → {2} · −{3} HP +battle/log_shield_absorb=Щит {0} поглотил {1} +battle/log_shield_gain=Защита: {0} · +{1} ЗАЩ +battle/log_absorbed={0}: {1} → {2} · урон поглощён щитом +battle/log_stun=Ошеломление: {0} · {1} хода +battle/log_stun_skip={0} ошеломлён — пропускает ход +battle/log_dead={0} повержен +battle/log_out_of_points=У отряда закончились ОД +battle/log_invalid_target=Недопустимая цель для «{0}» +battle/log_not_enough_points=Недостаточно ОД для «{0}» diff --git a/scenes/action_card.tscn b/scenes/action_card.tscn new file mode 100644 index 0000000..0cb50dc --- /dev/null +++ b/scenes/action_card.tscn @@ -0,0 +1,85 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/ActionCardNode.cs" id="1_script"] + +[node name="ActionCardNode" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(0, 86) +layout_mode = 3 +size_flags_horizontal = 3 + +[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="Row" type="HBoxContainer" parent="Panel"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 0 + +[node name="Accent" type="ColorRect" parent="Panel/Row"] +custom_minimum_size = Vector2(4, 0) +layout_mode = 2 +mouse_filter = 2 + +[node name="IconWrap" type="MarginContainer" parent="Panel/Row"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 8 + +[node name="Icon" type="TextureRect" parent="Panel/Row/IconWrap"] +custom_minimum_size = Vector2(52, 52) +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 6 + +[node name="Body" type="MarginContainer" parent="Panel/Row"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 8 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 8 + +[node name="Column" type="VBoxContainer" parent="Panel/Row/Body"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="NameRow" type="HBoxContainer" parent="Panel/Row/Body/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 8 + +[node name="NameLabel" type="Label" parent="Panel/Row/Body/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="IdLabel" type="Label" parent="Panel/Row/Body/Column/NameRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 9 + +[node name="Badges" type="HBoxContainer" parent="Panel/Row/Body/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="Props" type="HBoxContainer" parent="Panel/Row/Body/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 5 diff --git a/scenes/action_card_node.tscn b/scenes/action_card_node.tscn deleted file mode 100644 index 83011dd..0000000 --- a/scenes/action_card_node.tscn +++ /dev/null @@ -1,46 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="src/UI/ActionCardNode.cs" id="ActionCardNodeScript"] - -[node name="ActionCardNode" type="Control" script=ExtResource("ActionCardNodeScript")] -custom_minimum_size = Vector2(200, 280) -layout_mode = 3 - -[node name="Image" type="TextureRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -offset_left = 15.0 -offset_top = 15.0 -offset_right = -15.0 -offset_bottom = -40.0 -mouse_filter = 2 -expand_mode = 1 -stretch_mode = 4 - -[node name="Frame" type="TextureRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -expand_mode = 1 -stretch_mode = 4 - -[node name="NameLabel" type="Label" parent="."] -layout_mode = 1 -anchors_preset = 12 -anchor_left = 0.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -offset_top = -40.0 -offset_bottom = 0.0 -theme_override_font_sizes/font_size = 14 -horizontal_alignment = 1 diff --git a/scenes/battle_actions_panel.tscn b/scenes/battle_actions_panel.tscn new file mode 100644 index 0000000..63be3b6 --- /dev/null +++ b/scenes/battle_actions_panel.tscn @@ -0,0 +1,152 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/BattleActionsPanel.cs" id="1_script"] + +[node name="BattleActionsPanel" 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 +mouse_filter = 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 = 18 +theme_override_constants/margin_top = 14 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 11 + +[node name="HeaderRow" type="HBoxContainer" parent="Window/Column/Header"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 12 + +[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 = "battle/actions_title" +uppercase = true + +[node name="SubjectLabel" type="Label" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 14 +text = "battle/no_character" + +[node name="Spacer" type="Control" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_horizontal = 3 +mouse_filter = 2 + +[node name="ApCaption" type="Label" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 +text = "ui/character_panel/stat_ap" +uppercase = true + +[node name="Pips" type="HBoxContainer" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="ApHint" type="Label" parent="Window/Column/Header/HeaderRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 + +[node name="Rule" type="HSeparator" parent="Window/Column"] +layout_mode = 2 + +[node name="Body" type="MarginContainer" parent="Window/Column"] +layout_mode = 2 +size_flags_vertical = 3 +mouse_filter = 2 +theme_override_constants/margin_left = 18 +theme_override_constants/margin_top = 14 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 0 + +[node name="BodyColumn" type="VBoxContainer" parent="Window/Column/Body"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 10 + +[node name="EmptyLabel" type="Label" parent="Window/Column/Body/BodyColumn"] +layout_mode = 2 +size_flags_vertical = 3 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 14 +text = "battle/empty_actions" +horizontal_alignment = 1 +vertical_alignment = 1 +autowrap_mode = 3 + +[node name="Grid" type="GridContainer" parent="Window/Column/Body/BodyColumn"] +layout_mode = 2 +mouse_filter = 2 +columns = 2 +theme_override_constants/h_separation = 10 +theme_override_constants/v_separation = 10 + +[node name="DescWrap" type="MarginContainer" parent="Window/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/margin_left = 18 +theme_override_constants/margin_top = 12 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 12 + +[node name="DescWell" type="PanelContainer" parent="Window/Column/DescWrap"] +custom_minimum_size = Vector2(0, 58) +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="DescLabel" type="Label" parent="Window/Column/DescWrap/DescWell"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +autowrap_mode = 3 + +[node name="FooterRule" type="HSeparator" parent="Window/Column"] +layout_mode = 2 + +[node name="Footer" type="MarginContainer" parent="Window/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/margin_left = 18 +theme_override_constants/margin_top = 9 +theme_override_constants/margin_right = 18 +theme_override_constants/margin_bottom = 11 + +[node name="FooterRow" type="HBoxContainer" parent="Window/Column/Footer"] +layout_mode = 2 +mouse_filter = 2 + +[node name="HintLabel" type="Label" parent="Window/Column/Footer/FooterRow"] +layout_mode = 2 +size_flags_vertical = 4 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 12 +text = "battle/action_hint" + +[node name="AutoEndLabel" type="Label" parent="Window/Column/Footer/FooterRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "battle/action_autoend" diff --git a/scenes/battle_banner.tscn b/scenes/battle_banner.tscn new file mode 100644 index 0000000..a010113 --- /dev/null +++ b/scenes/battle_banner.tscn @@ -0,0 +1,21 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/BattleBanner.cs" id="1_script"] + +[node name="BattleBanner" type="Control" script=ExtResource("1_script")] +layout_mode = 3 +mouse_filter = 2 + +[node name="Plate" type="PanelContainer" parent="."] +modulate = Color(1, 1, 1, 0) +layout_mode = 1 +anchors_preset = 5 +anchor_left = 0.5 +anchor_right = 0.5 +grow_horizontal = 2 +mouse_filter = 2 + +[node name="Label" type="Label" parent="Plate"] +layout_mode = 2 +theme_override_font_sizes/font_size = 30 +horizontal_alignment = 1 diff --git a/scenes/battle_card_panel.tscn b/scenes/battle_card_panel.tscn index 9b50421..4f0d6f1 100644 --- a/scenes/battle_card_panel.tscn +++ b/scenes/battle_card_panel.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3] +[gd_scene format=3] [ext_resource type="Script" path="src/UI/BattleCardPanel.cs" id="1_script"] diff --git a/scenes/battle_hud.tscn b/scenes/battle_hud.tscn new file mode 100644 index 0000000..20bcd33 --- /dev/null +++ b/scenes/battle_hud.tscn @@ -0,0 +1,76 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/BattleHud.cs" id="1_script"] + +[node name="BattleHud" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(320, 88) +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 +mouse_filter = 2 +theme_override_constants/separation = 9 + +[node name="TitleRow" type="HBoxContainer" parent="Panel/Column"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 12 + +[node name="TitleLabel" type="Label" parent="Panel/Column/TitleRow"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 12 +text = "battle/hud_title" +uppercase = true + +[node name="PhaseBadge" type="PanelContainer" parent="Panel/Column/TitleRow"] +layout_mode = 2 +size_flags_horizontal = 8 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="PhaseLabel" type="Label" parent="Panel/Column/TitleRow/PhaseBadge"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "battle/turn_player" +uppercase = true + +[node name="MetaRow" type="HBoxContainer" parent="Panel/Column"] +layout_mode = 2 +mouse_filter = 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 = "battle/round" + +[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 = "battle/enemies_alive" + +[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" diff --git a/scenes/battle_log_panel.tscn b/scenes/battle_log_panel.tscn new file mode 100644 index 0000000..187e8f6 --- /dev/null +++ b/scenes/battle_log_panel.tscn @@ -0,0 +1,65 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/BattleLogPanel.cs" id="1_script"] + +[node name="BattleLogPanel" 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 +mouse_filter = 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 = "battle/log_title" +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 +mouse_filter = 2 +theme_override_constants/separation = 0 diff --git a/scenes/card_battle_screen.tscn b/scenes/card_battle_screen.tscn new file mode 100644 index 0000000..7105552 --- /dev/null +++ b/scenes/card_battle_screen.tscn @@ -0,0 +1,83 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/CardBattleScreen.cs" id="1_screen"] +[ext_resource type="Script" path="src/UI/CardController.cs" id="2_controller"] +[ext_resource type="PackedScene" path="scenes/battle_hud.tscn" id="3_hud"] +[ext_resource type="PackedScene" path="scenes/battle_card_panel.tscn" id="4_panel"] +[ext_resource type="PackedScene" path="scenes/battle_actions_panel.tscn" id="5_actions"] +[ext_resource type="PackedScene" path="scenes/battle_log_panel.tscn" id="6_log"] +[ext_resource type="PackedScene" path="scenes/battle_banner.tscn" id="7_banner"] +[ext_resource type="PackedScene" path="scenes/end_turn_button.tscn" id="8_endturn"] + +[node name="CardBattleScreen" type="Control" script=ExtResource("1_screen")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="Hud" parent="." instance=ExtResource("3_hud")] +layout_mode = 1 +offset_left = 20.0 +offset_top = 26.0 +offset_right = 340.0 +offset_bottom = 114.0 + +[node name="EnemyPanel" parent="." instance=ExtResource("4_panel")] +layout_mode = 1 +anchor_left = 0.02 +anchor_top = 0.09375 +anchor_right = 0.98 +anchor_bottom = 0.53975 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="PlayerPanel" parent="." instance=ExtResource("4_panel")] +layout_mode = 1 +anchor_left = 0.01354 +anchor_top = 0.5498 +anchor_right = 0.56354 +anchor_bottom = 0.96 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ActionsPanel" parent="." instance=ExtResource("5_actions")] +layout_mode = 1 +anchor_left = 0.5297 +anchor_top = 0.5498 +anchor_right = 0.9865 +anchor_bottom = 0.9521 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="LogPanel" parent="." instance=ExtResource("6_log")] +layout_mode = 1 +anchor_left = 0.2708 +anchor_top = 0.5566 +anchor_right = 0.5156 +anchor_bottom = 0.9629 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="EndTurnButton" parent="." instance=ExtResource("8_endturn")] +layout_mode = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -376.0 +offset_top = 20.0 +offset_right = -216.0 +offset_bottom = 70.0 + +[node name="Banner" parent="." instance=ExtResource("7_banner")] +layout_mode = 1 +anchor_top = 0.315 +anchor_right = 1.0 +anchor_bottom = 0.42 +grow_horizontal = 2 + +[node name="CardController" type="Node" parent="." script=ExtResource("2_controller")] + +[node name="DragLayer" type="CanvasLayer" parent="."] +layer = 100 diff --git a/scenes/character_card.tscn b/scenes/character_card.tscn new file mode 100644 index 0000000..15b154a --- /dev/null +++ b/scenes/character_card.tscn @@ -0,0 +1,226 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/UI/CharacterCardNode.cs" id="1_script"] + +[node name="CharacterCardNode" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(230, 420) +layout_mode = 3 + +[node name="Art" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 4 + +[node name="TeamFrame" type="Panel" 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="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 + +[node name="Column" type="VBoxContainer" parent="Info"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="NameLabel" type="Label" parent="Info/Column"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +horizontal_alignment = 1 +clip_text = true + +[node name="Stats" type="HBoxContainer" parent="Info/Column"] +layout_mode = 2 +mouse_filter = 2 +alignment = 1 +theme_override_constants/separation = 4 + +[node name="HpChip" type="PanelContainer" parent="Info/Column/Stats"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Info/Column/Stats/HpChip"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="Key" type="Label" parent="Info/Column/Stats/HpChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "ui/character_panel/stat_hp" +uppercase = true + +[node name="Value" type="Label" parent="Info/Column/Stats/HpChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 + +[node name="DefChip" type="PanelContainer" parent="Info/Column/Stats"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Info/Column/Stats/DefChip"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="Key" type="Label" parent="Info/Column/Stats/DefChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "ui/character_panel/stat_def" +uppercase = true + +[node name="Value" type="Label" parent="Info/Column/Stats/DefChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 + +[node name="AtkChip" type="PanelContainer" parent="Info/Column/Stats"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Info/Column/Stats/AtkChip"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="Key" type="Label" parent="Info/Column/Stats/AtkChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "ui/character_panel/stat_atk" +uppercase = true + +[node name="Value" type="Label" parent="Info/Column/Stats/AtkChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 11 + +[node name="ApChip" type="PanelContainer" parent="Info/Column/Stats"] +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Info/Column/Stats/ApChip"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 4 + +[node name="Key" type="Label" parent="Info/Column/Stats/ApChip/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +text = "ui/character_panel/stat_ap" +uppercase = true + +[node name="Value" type="Label" parent="Info/Column/Stats/ApChip/Row"] +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, 7) +layout_mode = 2 +size_flags_vertical = 4 +mouse_filter = 2 +show_percentage = false + +[node name="Status" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -160.0 +offset_top = 10.0 +offset_right = -10.0 +offset_bottom = 40.0 +grow_horizontal = 0 +mouse_filter = 2 +theme_override_constants/separation = 6 + +[node name="StunBadge" type="PanelContainer" parent="Status"] +visible = false +layout_mode = 2 +size_flags_horizontal = 8 +mouse_filter = 2 + +[node name="Row" type="HBoxContainer" parent="Status/StunBadge"] +layout_mode = 2 +mouse_filter = 2 +theme_override_constants/separation = 5 + +[node name="StunIcon" type="TextureRect" parent="Status/StunBadge/Row"] +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="Status/StunBadge/Row"] +layout_mode = 2 +size_flags_vertical = 4 +theme_override_font_sizes/font_size = 10 +uppercase = true + +[node name="Zap" type="TextureRect" parent="."] +modulate = Color(1, 1, 1, 0) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 14.0 +offset_top = 20.0 +offset_right = -14.0 +offset_bottom = -70.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 5 + +[node name="Fx" type="Panel" parent="."] +modulate = Color(1, 1, 1, 0) +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="Selection" type="TextureRect" parent="."] +modulate = Color(1, 1, 1, 0) +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 0 diff --git a/scenes/character_card_node.tscn b/scenes/character_card_node.tscn deleted file mode 100644 index 918cc71..0000000 --- a/scenes/character_card_node.tscn +++ /dev/null @@ -1,135 +0,0 @@ -[gd_scene format=3] - -[ext_resource type="Script" path="src/UI/CharacterCardNode.cs" id="1_script"] - -[node name="CharacterCardNode" type="Control" script=ExtResource("1_script")] -custom_minimum_size = Vector2(230, 420) -layout_mode = 3 - -[node name="TeamOutline" type="ColorRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -offset_left = -4.0 -offset_top = -4.0 -offset_right = 4.0 -offset_bottom = 4.0 -mouse_filter = 2 - -[node name="Image" type="TextureRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -offset_left = 20.0 -offset_top = 20.0 -offset_right = -20.0 -offset_bottom = -60.0 -mouse_filter = 2 -expand_mode = 1 -stretch_mode = 4 - -[node name="Frame" type="TextureRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -expand_mode = 1 -stretch_mode = 4 - -[node name="Selection" type="TextureRect" 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="InfoPanel" type="Control" parent="."] -layout_mode = 1 -anchors_preset = 12 -anchor_left = 0.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -offset_top = -60.0 -offset_bottom = 0.0 - -[node name="NameLabel" type="Label" parent="InfoPanel"] -layout_mode = 1 -anchors_preset = 10 -anchor_right = 1.0 -grow_horizontal = 2 -offset_bottom = 18.0 -horizontal_alignment = 1 -theme_override_font_sizes/font_size = 16 - -[node name="HealthBar" type="Control" parent="InfoPanel"] -layout_mode = 1 -anchors_preset = 12 -anchor_left = 0.0 -anchor_top = 1.0 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -offset_top = -18.0 -offset_bottom = 0.0 - -[node name="HealthBg" type="ColorRect" parent="InfoPanel/HealthBar"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0.137255, 0.145098, 0.184314, 1) - -[node name="HealthRect" type="ColorRect" parent="InfoPanel/HealthBar"] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0.560784, 0.682353, 0.419608, 1) - -[node name="Labels" type="HBoxContainer" parent="InfoPanel"] -layout_mode = 1 -anchors_preset = 13 -anchor_left = 0.0 -anchor_top = 0.5 -anchor_right = 1.0 -anchor_bottom = 0.5 -grow_horizontal = 2 -alignment = 1 - -[node name="LabelS" type="Label" parent="InfoPanel/Labels"] -text = "S" -horizontal_alignment = 1 - -[node name="DefenceLabel" type="Label" parent="InfoPanel/Labels"] -text = "0" - -[node name="LabelA" type="Label" parent="InfoPanel/Labels"] -text = "A" -horizontal_alignment = 1 - -[node name="AttackLabel" type="Label" parent="InfoPanel/Labels"] -text = "0" - -[node name="LabelP" type="Label" parent="InfoPanel/Labels"] -text = "P" -horizontal_alignment = 1 - -[node name="PointsLabel" type="Label" parent="InfoPanel/Labels"] -text = "0" diff --git a/scenes/end_turn_button.tscn b/scenes/end_turn_button.tscn index 27d3858..4951269 100644 --- a/scenes/end_turn_button.tscn +++ b/scenes/end_turn_button.tscn @@ -1,4 +1,4 @@ -[gd_scene format=3] +[gd_scene format=3] [ext_resource type="Script" path="src/UI/EndTurnButton.cs" id="1_script"] @@ -14,3 +14,4 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 theme_override_font_sizes/font_size = 16 +text = "battle/end_turn" diff --git a/src/Actions/PhysicsAttackAction.cs b/src/Actions/PhysicsAttackAction.cs index 4af8690..a5a60f6 100644 --- a/src/Actions/PhysicsAttackAction.cs +++ b/src/Actions/PhysicsAttackAction.cs @@ -1,5 +1,6 @@ using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.UI.Theming; using Cthangover.Core.Utils; using Cthangover.TurnBattle; using Cthangover.Core.Characters; @@ -167,12 +168,7 @@ namespace Cthangover.CardBattle.Actions if (result.Result) { - if (result.Target.Damage > 0) - ShowDamageBehaviour.SpawnDamage(result.Target.Damage, TargetControl, TargetControl.GlobalPosition); - - var defenceLost = -result.Target.Defence; - if (defenceLost > 0) - ShowDamageBehaviour.SpawnDefence(defenceLost, TargetControl, TargetControl.GlobalPosition); + LogAttackResult(result); } else { @@ -277,6 +273,52 @@ namespace Cthangover.CardBattle.Actions } } + /// + /// Mirrors the resolved action onto the UI: floating damage / absorbed + /// shield numbers, a colour pulse on the target card and journal rows, so + /// an AI attack reads the same as the player's own attack. Stun actions + /// (a "Turn" above 1) flash the electric zap instead of damage numbers. + /// + private void LogAttackResult(ChangedAttributes result) + { + var targetName = TranslationServer.Translate(Target.Card.Name).ToString(); + var isStun = Action != null && Action.GetInt("Turn", 0) > 1; + + var defenceLost = -result.Target.Defence; + if (defenceLost > 0) + { + ShowDamageBehaviour.SpawnDefence(defenceLost, TargetControl, TargetControl.GlobalPosition); + BattleLogHub.Log( + string.Format(Translate("battle/log_shield_absorb"), targetName, defenceLost), + UI.BattleLogKind.Shield); + } + + if (isStun) + { + Target.PlayZap(); + BattleLogHub.Log( + string.Format(Translate("battle/log_stun"), targetName, Action.GetInt("Turn", 0)), + UI.BattleLogKind.Status); + return; + } + + if (result.Target.Damage > 0) + { + ShowDamageBehaviour.SpawnDamage(result.Target.Damage, TargetControl, TargetControl.GlobalPosition); + Target.Flash(UiTokens.DangerText); + BattleLogHub.Log( + string.Format(Translate("battle/log_damage"), + Translate(Action.Name), + TranslationServer.Translate(Source.Card.Name).ToString(), + targetName, + result.Target.Damage), + UI.BattleLogKind.Damage); + } + } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); + private void InitializeMovementPattern() { Vector2 direction = (TargetPos - SourcePos).Normalized(); diff --git a/src/Actions/PhysicsDamageActionCard.cs b/src/Actions/PhysicsDamageActionCard.cs index cdafbf4..df6e13b 100644 --- a/src/Actions/PhysicsDamageActionCard.cs +++ b/src/Actions/PhysicsDamageActionCard.cs @@ -1,3 +1,4 @@ +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Characters; using Godot; @@ -34,8 +35,8 @@ namespace Cthangover.CardBattle.Actions return new ChangedAttributes { Result = false }; var damageDelta = Mathf.RoundToInt(actionCard.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/PhysicsDefenceAction.cs b/src/Actions/PhysicsDefenceAction.cs index 472ca03..2d00436 100644 --- a/src/Actions/PhysicsDefenceAction.cs +++ b/src/Actions/PhysicsDefenceAction.cs @@ -1,5 +1,6 @@ using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.UI.Theming; using Cthangover.Core.Characters; using Cthangover.TurnBattle; using Cthangover.CardBattle.UI; @@ -119,7 +120,15 @@ namespace Cthangover.CardBattle.Actions Target.UpdateInfo(); if (result.Result && result.Target.Defence > 0) + { ShowDamageBehaviour.SpawnDefence(result.Target.Defence, TargetControl, TargetControl.GlobalPosition); + Target.Flash(UiTokens.Info); + BattleLogHub.Log( + string.Format(Translate("battle/log_shield_gain"), + TranslationServer.Translate(Target.Card.Name).ToString(), + result.Target.Defence), + UI.BattleLogKind.Buff); + } } if (progress >= 1f) @@ -212,5 +221,8 @@ namespace Cthangover.CardBattle.Actions { return t < 0.5f ? 2f * t * t : 1f - Mathf.Pow(-2f * t + 2f, 2f) / 2f; } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); } } diff --git a/src/Actions/PhysicsStunActionCard.cs b/src/Actions/PhysicsStunActionCard.cs index e42c67e..462041a 100644 --- a/src/Actions/PhysicsStunActionCard.cs +++ b/src/Actions/PhysicsStunActionCard.cs @@ -1,3 +1,4 @@ +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Characters; namespace Cthangover.CardBattle.Actions @@ -30,7 +31,7 @@ namespace Cthangover.CardBattle.Actions if (target.HasPassiveEffect("StunImmune")) return new ChangedAttributes { Result = true }; - target.StatusEffectQueue.Add("effect/physics/stun", actionCard.GetInt("Turn", 3)); + StatusEffectService.Of(target).Add("effect/physics/stun", actionCard.GetInt("Turn", 3)); return new ChangedAttributes { Result = true }; } diff --git a/src/BattleLogHub.cs b/src/BattleLogHub.cs new file mode 100644 index 0000000..2c43522 --- /dev/null +++ b/src/BattleLogHub.cs @@ -0,0 +1,24 @@ +using System; +using Cthangover.CardBattle.UI; + +namespace Cthangover.CardBattle +{ + /// + /// Static sink for battle journal entries. Player action strategies and + /// the battle core run without a reference to the UI screen, so they + /// publish here instead of reaching into the scene tree; the + /// subscribes while it exists and unsubscribes + /// on tree exit, which keeps the hub empty outside of a battle. This is the + /// same service-locator pattern the battle code already uses for + /// ActionExecutorHub and ShowDamageBehaviour. + /// + public static class BattleLogHub + { + /// Fired for every entry; handlers receive the text and its kind. + public static event Action Entry; + + /// Publishes one journal row. No-op when no battle UI is listening. + public static void Log(string text, BattleLogKind kind = BattleLogKind.Info) + => Entry?.Invoke(text, kind); + } +} diff --git a/src/CardBattleCore.cs b/src/CardBattleCore.cs index e3c7122..456a31d 100644 --- a/src/CardBattleCore.cs +++ b/src/CardBattleCore.cs @@ -4,7 +4,9 @@ using System.Linq; using System.Threading.Tasks; using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.Cards.StatusEffect; using Cthangover.Core.Scenes; +using Cthangover.Core.UI.CharacterPanel; using Cthangover.Core.Utils; using Cthangover.Core.Characters; using Cthangover.CardBattle.Player; @@ -14,49 +16,58 @@ using Godot; namespace Cthangover.CardBattle { /// - /// Core orchestrator for the card-based turn battle system. - /// Manages the full battle lifecycle: initializing UI panels ( and ), - /// cycling between player and enemy turns, spawning animated actions via , - /// applying status effects each turn, and detecting win/loss conditions when all cards on one side are dead. - /// Implements so it can be registered as a battle type provider in the core battle engine. + /// Core orchestrator for the card-based turn battle system. Owns the full + /// battle lifecycle: it instantiates the UI screen rebuilt from the mockup + /// (mockups/card_battle, defined in scenes/card_battle_screen.tscn), + /// cycles between player and enemy turns, drives the animated enemy actions + /// through , applies status effects each + /// turn and detects win/loss when one side loses all cards. + /// + /// The core talks to the screen through its assembled widgets + /// (, , + /// , panels) instead of walking the scene tree, and + /// publishes journal rows via so other battle code + /// can log without a UI reference. Implements so the + /// battle engine can select this mod as the active battle type. /// public class CardBattleCore : IBattleCore, IBattleAutoPlayable { - /// - /// Unique identifier for this battle core, used by the battle engine to select the card battle mod. - /// - public string Id => "card_battle"; - - /// - /// Provides instances for the three card action types - /// (physics/attack, physics/defence, physics/stun) used during action resolution. - /// - public IActionExecutorProvider ActionProvider { get; } = new Actions.CardBattleActionProvider(); + private const int PlayerCardLimit = 4; + private const float BaseCardWidth = 230f; + private const float BaseCardHeight = 420f; + private const float CellOffset = 10f; private Character[] _playerChars; private Character[] _enemyChars; private IBattleContext _ctx; + private CardBattleScreen _screen; private BattleCardPanel _playerPanel; private BattleCardPanel _enemyPanel; private CardController _cardController; - private Control _actionPanel; - private Control _toolPanel; private EndTurnButton _endTurnButton; private BattleSide _currentOrder; private bool _playerTurnActive; + private int _round; - private const float CELL_OFFSET = 10f; - private const int PLAYER_CARD_LIMIT = 4; - private const float BASE_CARD_WIDTH = 230f; - private const float BASE_CARD_HEIGHT = 420f; + /// Unique identifier for this battle core, used by the battle engine to select the card battle mod. + public string Id => "card_battle"; /// - /// Initializes the card battle by constructing the player and enemy instances, - /// the for drag-and-drop input, and the . - /// Enemy characters are deep-copied so defeats are tracked independently per battle instance. - /// The is registered with . + /// Provides instances for the three card + /// action types (physics/attack, physics/defence, physics/stun) used + /// during action resolution. + /// + public IActionExecutorProvider ActionProvider { get; } = new Actions.CardBattleActionProvider(); + + /// + /// Instantiates the battle screen from scenes/card_battle_screen.tscn, + /// attaches it to the battle scene's Panel and binds the panels, + /// end-turn button and input controller. Enemy characters are deep-copied + /// so defeats are tracked independently per battle instance. The + /// is registered with + /// . /// public void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx) { @@ -64,71 +75,42 @@ namespace Cthangover.CardBattle _ctx = ctx; _enemyChars = new Character[enemyChars.Length]; - for (int i = 0; i < enemyChars.Length; i++) + for (var i = 0; i < enemyChars.Length; i++) _enemyChars[i] = enemyChars[i]?.Copy() ?? enemyChars[i]; ActionExecutorHub.Instance.SetActiveProvider(ActionProvider); var root = ctx.RootNode as Node; - var panel = root.GetNodeOrNull("Panel") ?? root; - _playerPanel = (BattleCardPanel)TscnScenes.LoadAndBuild("scenes/battle_card_panel.tscn"); - _playerPanel.Name = "PlayerPanel"; - _playerPanel.EnsureConstructed(); - panel.AddChild(_playerPanel); + _screen = (CardBattleScreen)TscnScenes.LoadAndBuild("scenes/card_battle_screen.tscn"); + _screen.Name = "CardBattleScreen"; + _screen.EnsureConstructed(); + panel.AddChild(_screen); - _enemyPanel = (BattleCardPanel)TscnScenes.LoadAndBuild("scenes/battle_card_panel.tscn"); - _enemyPanel.Name = "EnemyPanel"; - _enemyPanel.AlignType = "Right"; - _enemyPanel.EnsureConstructed(); - _enemyPanel.MouseFilter = Control.MouseFilterEnum.Ignore; - panel.AddChild(_enemyPanel); + _playerPanel = _screen.PlayerPanel; + _enemyPanel = _screen.EnemyPanel; - _actionPanel = new Control { Name = "ActionPanel" }; - panel.AddChild(_actionPanel); - - _toolPanel = root.GetNodeOrNull("ToolPanel"); - - _endTurnButton = (EndTurnButton)TscnScenes.LoadAndBuild("scenes/end_turn_button.tscn"); - _endTurnButton.Name = "EndTurnButton"; - _endTurnButton.EnsureConstructed(); + _endTurnButton = _screen.EndTurnButton; _endTurnButton.SetVisible(false); _endTurnButton.OnPressed += OnEndTurnPressed; - _toolPanel?.AddChild(_endTurnButton); - _cardController = new CardController { Name = "CardController" }; - panel.AddChild(_cardController); - _cardController.FindPanels(); + _cardController = _screen.Controller; + _cardController.Setup(_screen); } /// - /// Positions the UI panels relative to the viewport, calculates a unified cardScale that - /// fits both player and enemy cards on screen, initializes all character cards, and starts the first player turn. - /// Subscribes to OnCardDead to record defeated enemies and to OnActionExecuted to detect - /// automatic turn completion when all player cards exhaust their action points. + /// Calculates the pointer scale fitting both sides on screen, initializes + /// every card and starts the first player turn. Subscribes to + /// OnCardDead to record defeated enemies for the victory screen and + /// to OnActionExecuted to detect automatic turn completion. /// public void Start() { - var root = _ctx.RootNode as Node; - - var panelSize = root.GetViewport().GetVisibleRect().Size; var enemyCount = _enemyChars?.Length ?? 1; _enemyPanel.UseRows = enemyCount > 6; - _playerPanel.Position = new Vector2(0, panelSize.Y * 0.55f); - _playerPanel.Size = new Vector2(panelSize.X * 0.55f, panelSize.Y * 0.35f); - - _enemyPanel.Position = new Vector2(panelSize.X * 0.02f, panelSize.Y * 0.022f); - _enemyPanel.Size = new Vector2(panelSize.X * 0.96f, panelSize.Y * 0.446f); - - _actionPanel.Position = new Vector2(panelSize.X * 0.53f, panelSize.Y * 0.55f); - _actionPanel.Size = new Vector2(panelSize.X * 0.46f, panelSize.Y * 0.35f); - - if (_endTurnButton != null) - _endTurnButton.Position = new Vector2(panelSize.X - 180f, (panelSize.Y * 0.1f - 50f) / 2); - var cardScale = CalculateCardScale(enemyCount); _cardController.CardScale = cardScale; @@ -140,6 +122,7 @@ namespace Cthangover.CardBattle BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared; + _round = 0; _currentOrder = BattleSide.Player; StartPlayerTurn(); } @@ -147,31 +130,31 @@ namespace Cthangover.CardBattle private float CalculateCardScale(int enemyCount) { var viewport = ((Node)_ctx.RootNode).GetViewport().GetVisibleRect().Size; - float enemyPanelWidth = viewport.X * 0.96f; - float playerPanelWidth = viewport.X * 0.55f; - float enemyPanelHeight = viewport.Y * 0.446f; - - float maxCardScale = 1f; + var enemyPanelWidth = viewport.X * 0.96f; + var playerPanelWidth = viewport.X * 0.55f; + var enemyPanelHeight = viewport.Y * 0.446f; + float maxCardScale; if (_enemyPanel.UseRows) { - int maxPerRow = _enemyPanel.GetMaxCardsPerRow(); - int rows = _enemyPanel.GetCurrentRowCount(enemyCount); + var maxPerRow = _enemyPanel.GetMaxCardsPerRow(); + var rows = _enemyPanel.GetCurrentRowCount(enemyCount); - float widthScale = (enemyPanelWidth - (maxPerRow - 1) * CELL_OFFSET) / (maxPerRow * BASE_CARD_WIDTH); - float heightScale = (enemyPanelHeight - (rows - 1) * CELL_OFFSET) / (rows * BASE_CARD_HEIGHT); + var widthScale = (enemyPanelWidth - (maxPerRow - 1) * CellOffset) / (maxPerRow * BaseCardWidth); + var heightScale = (enemyPanelHeight - (rows - 1) * CellOffset) / (rows * BaseCardHeight); maxCardScale = Mathf.Max(0.1f, Mathf.Min(widthScale, heightScale)); } else { - int effectiveCount = Mathf.Max(enemyCount, 1); - maxCardScale = Mathf.Max(0.1f, (enemyPanelWidth - (effectiveCount - 1) * CELL_OFFSET) / (effectiveCount * BASE_CARD_WIDTH)); + var effectiveCount = Mathf.Max(enemyCount, 1); + maxCardScale = Mathf.Max(0.1f, + (enemyPanelWidth - (effectiveCount - 1) * CellOffset) / (effectiveCount * BaseCardWidth)); } - float playerFitScale = Mathf.Max(0.1f, (playerPanelWidth - (PLAYER_CARD_LIMIT - 1) * CELL_OFFSET) / (PLAYER_CARD_LIMIT * BASE_CARD_WIDTH)); + var playerFitScale = Mathf.Max(0.1f, + (playerPanelWidth - (PlayerCardLimit - 1) * CellOffset) / (PlayerCardLimit * BaseCardWidth)); - float cardScale = Mathf.Min(maxCardScale, playerFitScale); - return Mathf.Min(cardScale, 1f); + return Mathf.Min(Mathf.Min(maxCardScale, playerFitScale), 1f); } private void OnEnemyCardDead(CharacterCardNode card) @@ -185,34 +168,43 @@ namespace Cthangover.CardBattle ctx.RecordEnemyDefeated(card.Card); ctx.NotifyCharacterDied(card.Card); } + + _screen.Hud.SetEnemiesAlive(CountAlive(_enemyPanel)); + BattleLogHub.Log(string.Format(Translate("battle/log_dead"), card.Card.Name), BattleLogKind.Dead); } private void StartPlayerTurn() { - GameLogger.Log("CARD_BATTLE", "Player turn started", LogLevel.Debug); - + _round += 1; _currentOrder = BattleSide.Player; _playerTurnActive = true; BattleSceneContext.Instance.IsWait = false; _cardController.ClearSelections(); _cardController.ClearActions(); + _screen.ActionsPanel.ClearBody(); + _screen.ActionsPanel.ClearSubject(); _endTurnButton?.SetVisible(true); foreach (var card in _playerPanel.CardList) { - card.Card.StatusEffectQueue.OnTurnStart(); + StatusEffectService.Of(card.Card).OnTurnStart(); card.Card.Attributes.Point.Value = card.Card.Attributes.Point.BaseValue; card.UpdateInfo(); } + + _screen.Hud.SetTurn(true); + _screen.Hud.SetRound(_round); + _screen.Hud.SetEnemiesAlive(CountAlive(_enemyPanel)); + _screen.LogPanel.SetRound(_round); + _screen.Banner.Show(Translate("battle/turn_player")); + BattleLogHub.Log( + Translate("battle/log_turn_player") + " · " + string.Format(Translate("battle/log_round"), _round), + BattleLogKind.Turn); } - private void OnEndTurnPressed() - { - GameLogger.Log("CARD_BATTLE", "End turn button pressed", LogLevel.Debug); - OnPlayerTurnEnd(); - } + private void OnEndTurnPressed() => OnPlayerTurnEnd(); private void CheckPlayerTurnEnd() { @@ -225,30 +217,24 @@ namespace Cthangover.CardBattle return; } - bool allOutOfPoints = true; foreach (var card in _playerPanel.CardList) { if (!card.IsDead && card.Card?.Attributes?.Point?.Value > 0) - { - allOutOfPoints = false; - break; - } + return; } - if (allOutOfPoints) - OnPlayerTurnEnd(); + BattleLogHub.Log(Translate("battle/log_out_of_points"), BattleLogKind.Info); + OnPlayerTurnEnd(); } /// - /// Ends the player's turn by hiding the end-turn button, setting the battle to IsWait, - /// and yielding two frames for UI to settle before running the enemy AI turn via RunEnemyTurn. - /// If all enemies are already dead, the battle ends immediately with the player as winner. - /// This method is async void so it can await frames without blocking the game loop. + /// Ends the player's turn: hides the end-turn button, pauses the battle, + /// plays the enemy-turn banner and journal line, waits for two frames plus + /// a short pause so the UI settles, then runs the enemy AI. Ends the battle + /// immediately when no enemies remain. /// public async void OnPlayerTurnEnd() { - GameLogger.Log("CARD_BATTLE", "Player turn ended", LogLevel.Debug); - _playerTurnActive = false; _endTurnButton?.SetVisible(false); BattleSceneContext.Instance.IsWait = true; @@ -259,6 +245,10 @@ namespace Cthangover.CardBattle return; } + _screen.Hud.SetTurn(false); + _screen.Banner.Show(Translate("battle/turn_enemy")); + BattleLogHub.Log(Translate("battle/log_turn_enemy"), BattleLogKind.Turn); + var root = (Node)_ctx.RootNode; await root.ToSignal(root.GetTree(), "process_frame"); await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout"); @@ -268,8 +258,6 @@ namespace Cthangover.CardBattle private async Task RunEnemyTurn() { - GameLogger.Log("CARD_BATTLE", "Enemy turn started", LogLevel.Debug); - _currentOrder = BattleSide.Enemy; _endTurnButton?.SetVisible(false); @@ -280,11 +268,15 @@ namespace Cthangover.CardBattle if (card.IsDead || card.Card?.Actions == null || card.Card.Actions.Count == 0) continue; - card.Card.StatusEffectQueue.OnTurnStart(); + StatusEffectService.Of(card.Card).OnTurnStart(); - if (card.Card.StatusEffectQueue.HasStun()) + if (StatusEffectService.Of(card.Card).HasStun()) { - GameLogger.Log("CARD_BATTLE", $"{card.Card.Name} is stunned, skipping", LogLevel.Debug); + card.UpdateInfo(); + card.PlayZap(); + BattleLogHub.Log( + string.Format(Translate("battle/log_stun_skip"), card.Card.Name), + BattleLogKind.Status); continue; } @@ -298,9 +290,17 @@ namespace Cthangover.CardBattle if (target == null) continue; - GameLogger.Log("CARD_BATTLE", - $"Enemy action: {card.Card.Name} -> {action.Name} -> {target.Card.Name}", - LogLevel.Message); + card.UpdateInfo(); + + if (action.Type == ActionCharacterType.ToEnemy) + { + BattleLogHub.Log( + string.Format(Translate("battle/log_enemy_action"), + card.Card.Name, + ActionCharacterUi.Translate(action.Name), + target.Card.Name), + BattleLogKind.Info); + } var animatedAction = CreateAnimatedAction(card, target, action); if (animatedAction != null) @@ -315,7 +315,6 @@ namespace Cthangover.CardBattle if (!HasAliveCards(_playerPanel)) { - GameLogger.Log("CARD_BATTLE", "All players dead — enemy wins", LogLevel.Message); _ctx.EndBattle(BattleSide.Enemy); return; } @@ -336,8 +335,7 @@ namespace Cthangover.CardBattle if (actions == null || actions.Count == 0) return null; - var random = new Random(); - return actions[random.Next(actions.Count)]; + return actions[GD.RandRange(0, actions.Count - 1)]; } private CharacterCardNode PickEnemyTarget(CharacterCardNode source, ActionCharacter action) @@ -350,7 +348,7 @@ namespace Cthangover.CardBattle if (alivePlayers.Count == 0) return null; - return alivePlayers[new Random().Next(alivePlayers.Count)]; + return alivePlayers[GD.RandRange(0, alivePlayers.Count - 1)]; } if (action.Type == ActionCharacterType.ToAlias || action.Type == ActionCharacterType.ToSelf) @@ -360,7 +358,8 @@ namespace Cthangover.CardBattle return null; } - private Actions.AbstractBattleAction CreateAnimatedAction(CharacterCardNode source, CharacterCardNode target, ActionCharacter action) + private Actions.AbstractBattleAction CreateAnimatedAction( + CharacterCardNode source, CharacterCardNode target, ActionCharacter action) { if (action.Type == ActionCharacterType.ToEnemy) return new Actions.PhysicsAttackAction(source, target, action); @@ -372,7 +371,7 @@ namespace Cthangover.CardBattle return null; } - private bool HasAliveCards(BattleCardPanel panel) + private static bool HasAliveCards(BattleCardPanel panel) { foreach (var card in panel.CardList) { @@ -382,14 +381,25 @@ namespace Cthangover.CardBattle return false; } + private static int CountAlive(BattleCardPanel panel) + { + var alive = 0; + foreach (var card in panel.CardList) + { + if (!card.IsDead && card.Card?.Attributes?.Health?.Value > 0) + alive++; + } + return alive; + } + private void OnBattleCleared() { - _playerPanel?.ClearAll(); - _enemyPanel?.ClearAll(); - _cardController?.ClearActions(); - _endTurnButton?.SetVisible(false); + _screen?.Clear(); } + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); + /// /// true between and the start of /// — matches the visibility window of the @@ -397,9 +407,7 @@ namespace Cthangover.CardBattle /// public bool IsWaitingForPlayerInput => _playerTurnActive; - /// - /// Automation hook: ends the player's turn without selecting cards. - /// + /// Automation hook: ends the player's turn without selecting cards. public void AdvancePlayerTurn() => OnPlayerTurnEnd(); } } diff --git a/src/ICard.cs b/src/ICard.cs index 173ce8b..ae7be6d 100644 --- a/src/ICard.cs +++ b/src/ICard.cs @@ -2,23 +2,15 @@ namespace Cthangover.CardBattle { - /// - /// Common interface for all card UI nodes in the card battle system. - /// Exposes the Frame and Image controls so that - /// can perform hit-testing during drag-and-drop targeting - /// without knowing the concrete card type ( or ). + /// Common contract for every card the input controller can hit-test. Both + /// the character cards on the field and the draggable action cards expose + /// their root , because layout, scale and the drag + /// ghost all operate on the whole card rect rather than on an inner sprite. /// public interface ICard { - /// - /// The frame used for hit-testing whether a pointer is over this card. - /// - TextureRect Frame { get; } - /// - /// The portrait/image of the card, used for dissolve shader effects on death. - /// - TextureRect Image { get; } + /// Root control used for hit-testing and for reparenting during a drag. + Control GetControlNode(); } - -} \ No newline at end of file +} diff --git a/src/Player/AttackToEnemyStrategy.cs b/src/Player/AttackToEnemyStrategy.cs index 2b5fef0..7289ad2 100644 --- a/src/Player/AttackToEnemyStrategy.cs +++ b/src/Player/AttackToEnemyStrategy.cs @@ -1,30 +1,31 @@ using Cthangover.CardBattle.UI; using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.UI.Theming; using Cthangover.TurnBattle; using Cthangover.Core.Characters; +using Godot; namespace Cthangover.CardBattle.Player { /// - /// Strategy for ToEnemy attack actions. Validates that the target is an enemy - /// (non-player card), highlights both cards with a red attack outline, and executes - /// damage via with floating damage/defence numbers. - /// This is the standard strategy for all offensive cards in the player's deck. + /// Strategy for ToEnemy actions. Validates that the target is an enemy, + /// highlights both cards with a red attack outline, executes the action via + /// and reports the outcome: floating damage / + /// defence-loss numbers on the target, a colour pulse on the card and a + /// journal row through . Stun actions (those with a + /// "Turn" above 1) additionally flash the electric zap over the target + /// instead of printing damage, matching the mockup's stun feedback. /// public class AttackToEnemyStrategy : ICardActionStrategy { - /// - /// Returns true if the action type is ToEnemy and the target is an enemy card. - /// + /// Returns true if the action type is ToEnemy and the target is an enemy card. public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) { return action.Type == ActionCharacterType.ToEnemy && target != null && !target.IsPlayer; } - /// - /// Shows a red attack overlay on both the dragged action card and the target enemy card. - /// + /// Shows a red attack overlay on both the dragged action card and the target enemy card. public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target) { actionCard.Attack(); @@ -32,8 +33,9 @@ namespace Cthangover.CardBattle.Player } /// - /// Resolves the attack via and spawns floating damage - /// and defence-loss numbers above the card. + /// Resolves the attack and publishes its result: damage and absorbed + /// shield as floating numbers plus journal rows, or the stun effect with + /// its zap flash for control actions. /// public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action) { @@ -44,15 +46,50 @@ namespace Cthangover.CardBattle.Player source.UpdateInfo(); target.UpdateInfo(); - if (result.Result) - { - if (result.Target.Damage > 0) - ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition); + if (!result.Result) + return; - var defenceLost = -result.Target.Defence; - if (defenceLost > 0) - ShowDamageBehaviour.SpawnDefence(defenceLost, target, target.GlobalPosition); + var actionName = Translate(action.Name); + var sourceName = Translate(source.Card.Name); + var targetName = Translate(target.Card.Name); + var isStun = action.GetInt("Turn", 0) > 1; + + var defenceLost = -result.Target.Defence; + if (defenceLost > 0) + { + ShowDamageBehaviour.SpawnDefence(defenceLost, target, target.GlobalPosition); + BattleLogHub.Log( + string.Format(Translate("battle/log_shield_absorb"), targetName, defenceLost), + BattleLogKind.Shield); + } + + if (isStun) + { + target.PlayZap(); + BattleLogHub.Log( + string.Format(Translate("battle/log_stun"), targetName, action.GetInt("Turn", 0)), + BattleLogKind.Status); + return; + } + + if (result.Target.Damage > 0) + { + ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition); + target.Flash(UiTokens.DangerText); + BattleLogHub.Log( + string.Format(Translate("battle/log_damage"), + actionName, sourceName, targetName, result.Target.Damage), + BattleLogKind.Damage); + } + else if (defenceLost == 0) + { + BattleLogHub.Log( + string.Format(Translate("battle/log_absorbed"), actionName, sourceName, targetName), + BattleLogKind.Shield); } } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); } } diff --git a/src/Player/SelfStrategy.cs b/src/Player/SelfStrategy.cs index d6bff74..cb86946 100644 --- a/src/Player/SelfStrategy.cs +++ b/src/Player/SelfStrategy.cs @@ -1,41 +1,36 @@ using Cthangover.CardBattle.UI; using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.UI.Theming; using Cthangover.TurnBattle; using Cthangover.Core.Characters; +using Godot; namespace Cthangover.CardBattle.Player { /// - /// Strategy for self-targeting actions (ToSelf). Validates that source and target - /// are the same card, highlights with a green selection outline, and applies the action - /// to the source card itself via . Used for self-buffs like - /// defence boosts. Damage and defence floating text spawn above the source card. + /// Strategy for self-targeting actions (ToSelf). Validates that the + /// source and target are the same card, highlights with a green selection + /// outline and applies the effect to the caster. Shield gains print a + /// floating blue number and a journal row; the card pulses to make the buff + /// readable without reading the log. /// public class SelfStrategy : ICardActionStrategy { - /// - /// Returns true only when and are the same card. - /// + /// Returns true only when and are the same card. public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) { return source != null && source == target; } - /// - /// Shows a green selection overlay on both the dragged action card and the source card. - /// + /// Shows a green selection overlay on both the dragged action card and the source card. public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target) { actionCard.Select(); target.Select(); } - /// - /// Executes the self-targeted action and spawns floating defence/damage numbers - /// above the card. Damage numbers are marked with the - /// isHeal flag for green coloring via . - /// + /// Executes the self-targeted action and reports shields and healing. public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action) { if (source?.Card == null || action == null) @@ -44,14 +39,23 @@ namespace Cthangover.CardBattle.Player var result = ActionExecutorHub.Instance.Execute(action, source.Card, source.Card); source.UpdateInfo(); - if (result.Result) - { - if (result.Target.Defence > 0) - ShowDamageBehaviour.SpawnDefence(result.Target.Defence, source, source.GlobalPosition); + if (!result.Result) + return; - if (result.Target.Damage > 0) - ShowDamageBehaviour.SpawnDamage(result.Target.Damage, source, source.GlobalPosition, true); + if (result.Target.Defence > 0) + { + ShowDamageBehaviour.SpawnDefence(result.Target.Defence, source, source.GlobalPosition); + source.Flash(UiTokens.Info); + BattleLogHub.Log( + string.Format(Translate("battle/log_shield_gain"), Translate(source.Card.Name), result.Target.Defence), + BattleLogKind.Buff); } + + if (result.Target.Damage > 0) + ShowDamageBehaviour.SpawnDamage(result.Target.Damage, source, source.GlobalPosition, true); } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); } } diff --git a/src/Player/SupportStrategy.cs b/src/Player/SupportStrategy.cs index 3ecfd3c..1d085b2 100644 --- a/src/Player/SupportStrategy.cs +++ b/src/Player/SupportStrategy.cs @@ -1,40 +1,36 @@ using Cthangover.CardBattle.UI; using Cthangover.Core.Battle; using Cthangover.Core.Battle.Actions; +using Cthangover.Core.UI.Theming; using Cthangover.TurnBattle; using Cthangover.Core.Characters; +using Godot; namespace Cthangover.CardBattle.Player { /// - /// Strategy for ally-support actions (ToAlias). Validates that the target is a friendly - /// player card, highlights with green selection overlays, and applies defence/healing effects - /// via . Allows one player card to buff another during the player's turn. + /// Strategy for ally-support actions (ToAlias). Validates that the + /// target is a friendly card, highlights with green selection overlays and + /// applies the effect via . Shield gains are + /// shown as floating blue numbers, a colour pulse on the ally and a journal + /// row; healing, when the executor produces it, prints a green number. /// public class SupportStrategy : ICardActionStrategy { - /// - /// Returns true when the action type is ToAlias and the target is a player card. - /// + /// Returns true when the action type is ToAlias and the target is a player card. public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target) { return action.Type == ActionCharacterType.ToAlias && target != null && target.IsPlayer; } - /// - /// Shows a green selection overlay on both the dragged action card and the target ally card. - /// + /// Shows a green selection overlay on both the dragged action card and the target ally card. public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target) { actionCard.Select(); target.Select(); } - /// - /// Resolves the support action and spawns floating defence and heal text above - /// the ally. Damage numbers are shown with the isHeal - /// flag for green coloring. - /// + /// Resolves the support action and reports shields and healing. public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action) { if (source?.Card == null || target?.Card == null || action == null) @@ -44,14 +40,23 @@ namespace Cthangover.CardBattle.Player source.UpdateInfo(); target.UpdateInfo(); - if (result.Result) - { - if (result.Target.Defence > 0) - ShowDamageBehaviour.SpawnDefence(result.Target.Defence, target, target.GlobalPosition); + if (!result.Result) + return; - if (result.Target.Damage > 0) - ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition, true); + if (result.Target.Defence > 0) + { + ShowDamageBehaviour.SpawnDefence(result.Target.Defence, target, target.GlobalPosition); + target.Flash(UiTokens.Info); + BattleLogHub.Log( + string.Format(Translate("battle/log_shield_gain"), Translate(target.Card.Name), result.Target.Defence), + BattleLogKind.Buff); } + + if (result.Target.Damage > 0) + ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition, true); } + + private static string Translate(string key) + => TranslationServer.Translate(key).ToString(); } } diff --git a/src/UI/ActionCardNode.cs b/src/UI/ActionCardNode.cs index 19e3c42..9a2fc1c 100644 --- a/src/UI/ActionCardNode.cs +++ b/src/UI/ActionCardNode.cs @@ -1,5 +1,5 @@ -using Cthangover.Core.Cards; using Cthangover.Core.Characters; +using Cthangover.Core.UI.CharacterPanel; using Cthangover.Core.UI; using Cthangover.Core.UI.Theming; using Godot; @@ -7,134 +7,210 @@ using Godot; namespace Cthangover.CardBattle.UI { /// - /// UI node representing a single action card that the player can drag onto a target. - /// Implements so can hit-test drag targets. - /// Displayed in the 's action panel below the character cards. - /// Supports hover scale-up animation, selection highlighting (, , ), - /// and info updates from the underlying data model. + /// Draggable action card from the interface mockup (mockups/card_battle): + /// the horizontal kit card with a type-coloured accent strip, action icon, + /// name and ID, badges (targeting type, action-point cost, stun turns) and + /// a property-chip row. The node tree lives in scenes/action_card.tscn; + /// this class fetches the nodes, dresses them and fills the dynamic badge / + /// chip rows using the shared presentation + /// rules, so the battle UI can never disagree with the character panel about + /// an action's colour, caption or cost. + /// + /// Implements so the input controller can hit-test drag + /// targets; the hover style, disabled state and the preview builder + /// () are the only UI surface the controller and + /// the actions panel need. /// public partial class ActionCardNode : ModWidget, ICard { - private TextureRect frame; - private TextureRect image; - private Label nameLabel; + private PanelContainer _panel; + private ColorRect _accent; + private TextureRect _icon; + private Label _nameLabel; + private Label _idLabel; + private HBoxContainer _badges; + private HBoxContainer _props; - /// - public TextureRect Frame => frame; - /// - public TextureRect Image => image; + private StyleBoxFlat _idleStyle; + private StyleBoxFlat _hoverStyle; + private bool _disabled; - /// - /// The data model backing this card. Contains the action's - /// name, image, type, and stat modifiers used during execution. - /// + /// The action data model backing this card. public ActionCharacter Card { get; set; } - /// - /// Returns this — action cards are nodes themselves, - /// unlike which wraps a separate control. - /// - public Control GetControlNode() => this; + /// True while the character cannot pay the card's action-point cost. + public bool IsDisabled => _disabled; - private Tween hoverTween; - private float baseY; - private bool isHovered; - private Vector2 _baseScale; + /// Raised when the cursor enters the card; the actions panel uses it to print the preview. + public event System.Action Hovered; + + /// Raised when the cursor leaves the card; the preview returns to its default text. + public event System.Action Unhovered; + + /// + public Control GetControlNode() => this; protected override void Construct() { CthangoverTheme.Apply(this); - image = GetNode("Image"); - frame = GetNode("Frame"); - nameLabel = GetNode