new battle gui
This commit is contained in:
1 parent
f65663fd91
commit
b243a318f4
35 files changed
+2423
-958
No files matched your search
@@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+38
-3
@@ -1,4 +1,39 @@
|
|||||||
mod_card_battle_name = CardBattle
|
mod_card_battle_name=CardBattle
|
||||||
mod_card_battle_desc = Card battle mechanics
|
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}"
|
||||||
+36
-1
@@ -1,4 +1,39 @@
|
|||||||
mod_card_battle_name=Карточная битва
|
mod_card_battle_name=Карточная битва
|
||||||
mod_card_battle_desc=Механики карточных сражений
|
mod_card_battle_desc=Механики карточных сражений
|
||||||
|
|
||||||
battle/end_turn=Конец хода
|
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}»
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene format=3]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="src/UI/BattleCardPanel.cs" id="1_script"]
|
[ext_resource type="Script" path="src/UI/BattleCardPanel.cs" id="1_script"]
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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"
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene format=3]
|
[gd_scene format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="src/UI/EndTurnButton.cs" id="1_script"]
|
[ext_resource type="Script" path="src/UI/EndTurnButton.cs" id="1_script"]
|
||||||
|
|
||||||
@@ -14,3 +14,4 @@ anchor_bottom = 1.0
|
|||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
theme_override_font_sizes/font_size = 16
|
theme_override_font_sizes/font_size = 16
|
||||||
|
text = "battle/end_turn"
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Cthangover.TurnBattle;
|
using Cthangover.TurnBattle;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
@@ -167,12 +168,7 @@ namespace Cthangover.CardBattle.Actions
|
|||||||
|
|
||||||
if (result.Result)
|
if (result.Result)
|
||||||
{
|
{
|
||||||
if (result.Target.Damage > 0)
|
LogAttackResult(result);
|
||||||
ShowDamageBehaviour.SpawnDamage(result.Target.Damage, TargetControl, TargetControl.GlobalPosition);
|
|
||||||
|
|
||||||
var defenceLost = -result.Target.Defence;
|
|
||||||
if (defenceLost > 0)
|
|
||||||
ShowDamageBehaviour.SpawnDefence(defenceLost, TargetControl, TargetControl.GlobalPosition);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -277,6 +273,52 @@ namespace Cthangover.CardBattle.Actions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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()
|
private void InitializeMovementPattern()
|
||||||
{
|
{
|
||||||
Vector2 direction = (TargetPos - SourcePos).Normalized();
|
Vector2 direction = (TargetPos - SourcePos).Normalized();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Cthangover.Core.Cards.StatusEffect;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
@@ -34,8 +35,8 @@ namespace Cthangover.CardBattle.Actions
|
|||||||
return new ChangedAttributes { Result = false };
|
return new ChangedAttributes { Result = false };
|
||||||
|
|
||||||
var damageDelta = Mathf.RoundToInt(actionCard.GetFloat("Attack", 1f) * user.Attributes.Attack.Value);
|
var damageDelta = Mathf.RoundToInt(actionCard.GetFloat("Attack", 1f) * user.Attributes.Attack.Value);
|
||||||
target.StatusEffectQueue.OnTakeDamage(user, ref damageDelta);
|
StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta);
|
||||||
user.StatusEffectQueue.OnDealDamage(target, ref damageDelta);
|
StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta);
|
||||||
|
|
||||||
var defenceDelta = 0;
|
var defenceDelta = 0;
|
||||||
var ignoreArmor = user.HasPassiveEffect("IgnoreArmor");
|
var ignoreArmor = user.HasPassiveEffect("IgnoreArmor");
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
using Cthangover.TurnBattle;
|
using Cthangover.TurnBattle;
|
||||||
using Cthangover.CardBattle.UI;
|
using Cthangover.CardBattle.UI;
|
||||||
@@ -119,7 +120,15 @@ namespace Cthangover.CardBattle.Actions
|
|||||||
Target.UpdateInfo();
|
Target.UpdateInfo();
|
||||||
|
|
||||||
if (result.Result && result.Target.Defence > 0)
|
if (result.Result && result.Target.Defence > 0)
|
||||||
|
{
|
||||||
ShowDamageBehaviour.SpawnDefence(result.Target.Defence, TargetControl, TargetControl.GlobalPosition);
|
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)
|
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;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Cthangover.Core.Cards.StatusEffect;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
|
|
||||||
namespace Cthangover.CardBattle.Actions
|
namespace Cthangover.CardBattle.Actions
|
||||||
@@ -30,7 +31,7 @@ namespace Cthangover.CardBattle.Actions
|
|||||||
if (target.HasPassiveEffect("StunImmune"))
|
if (target.HasPassiveEffect("StunImmune"))
|
||||||
return new ChangedAttributes { Result = true };
|
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 };
|
return new ChangedAttributes { Result = true };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using Cthangover.CardBattle.UI;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// <see cref="BattleLogPanel"/> 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
|
||||||
|
/// <c>ActionExecutorHub</c> and <c>ShowDamageBehaviour</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class BattleLogHub
|
||||||
|
{
|
||||||
|
/// <summary>Fired for every entry; handlers receive the text and its kind.</summary>
|
||||||
|
public static event Action<string, BattleLogKind> Entry;
|
||||||
|
|
||||||
|
/// <summary>Publishes one journal row. No-op when no battle UI is listening.</summary>
|
||||||
|
public static void Log(string text, BattleLogKind kind = BattleLogKind.Info)
|
||||||
|
=> Entry?.Invoke(text, kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
+132
-124
@@ -4,7 +4,9 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.Cards.StatusEffect;
|
||||||
using Cthangover.Core.Scenes;
|
using Cthangover.Core.Scenes;
|
||||||
|
using Cthangover.Core.UI.CharacterPanel;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
using Cthangover.CardBattle.Player;
|
using Cthangover.CardBattle.Player;
|
||||||
@@ -14,49 +16,58 @@ using Godot;
|
|||||||
namespace Cthangover.CardBattle
|
namespace Cthangover.CardBattle
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core orchestrator for the card-based turn battle system.
|
/// Core orchestrator for the card-based turn battle system. Owns the full
|
||||||
/// Manages the full battle lifecycle: initializing UI panels (<see cref="BattleCardPanel"/> and <see cref="CardController"/>),
|
/// battle lifecycle: it instantiates the UI screen rebuilt from the mockup
|
||||||
/// cycling between player and enemy turns, spawning animated actions via <see cref="AbstractBattleAction"/>,
|
/// (mockups/card_battle, defined in <c>scenes/card_battle_screen.tscn</c>),
|
||||||
/// applying status effects each turn, and detecting win/loss conditions when all cards on one side are dead.
|
/// cycles between player and enemy turns, drives the animated enemy actions
|
||||||
/// Implements <see cref="IBattleCore"/> so it can be registered as a battle type provider in the core battle engine.
|
/// through <see cref="AbstractBattleAction"/>, applies status effects each
|
||||||
|
/// turn and detects win/loss when one side loses all cards.
|
||||||
|
/// <para/>
|
||||||
|
/// The core talks to the screen through its assembled widgets
|
||||||
|
/// (<see cref="CardBattleScreen.Hud"/>, <see cref="BattleLogPanel"/>,
|
||||||
|
/// <see cref="BattleBanner"/>, panels) instead of walking the scene tree, and
|
||||||
|
/// publishes journal rows via <see cref="BattleLogHub"/> so other battle code
|
||||||
|
/// can log without a UI reference. Implements <see cref="IBattleCore"/> so the
|
||||||
|
/// battle engine can select this mod as the active battle type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CardBattleCore : IBattleCore, IBattleAutoPlayable
|
public class CardBattleCore : IBattleCore, IBattleAutoPlayable
|
||||||
{
|
{
|
||||||
/// <summary>
|
private const int PlayerCardLimit = 4;
|
||||||
/// Unique identifier for this battle core, used by the battle engine to select the card battle mod.
|
private const float BaseCardWidth = 230f;
|
||||||
/// </summary>
|
private const float BaseCardHeight = 420f;
|
||||||
public string Id => "card_battle";
|
private const float CellOffset = 10f;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides <see cref="IActionExecutor"/> instances for the three card action types
|
|
||||||
/// (physics/attack, physics/defence, physics/stun) used during action resolution.
|
|
||||||
/// </summary>
|
|
||||||
public IActionExecutorProvider ActionProvider { get; } = new Actions.CardBattleActionProvider();
|
|
||||||
|
|
||||||
private Character[] _playerChars;
|
private Character[] _playerChars;
|
||||||
private Character[] _enemyChars;
|
private Character[] _enemyChars;
|
||||||
private IBattleContext _ctx;
|
private IBattleContext _ctx;
|
||||||
|
|
||||||
|
private CardBattleScreen _screen;
|
||||||
private BattleCardPanel _playerPanel;
|
private BattleCardPanel _playerPanel;
|
||||||
private BattleCardPanel _enemyPanel;
|
private BattleCardPanel _enemyPanel;
|
||||||
private CardController _cardController;
|
private CardController _cardController;
|
||||||
private Control _actionPanel;
|
|
||||||
private Control _toolPanel;
|
|
||||||
private EndTurnButton _endTurnButton;
|
private EndTurnButton _endTurnButton;
|
||||||
|
|
||||||
private BattleSide _currentOrder;
|
private BattleSide _currentOrder;
|
||||||
private bool _playerTurnActive;
|
private bool _playerTurnActive;
|
||||||
|
private int _round;
|
||||||
|
|
||||||
private const float CELL_OFFSET = 10f;
|
/// <summary>Unique identifier for this battle core, used by the battle engine to select the card battle mod.</summary>
|
||||||
private const int PLAYER_CARD_LIMIT = 4;
|
public string Id => "card_battle";
|
||||||
private const float BASE_CARD_WIDTH = 230f;
|
|
||||||
private const float BASE_CARD_HEIGHT = 420f;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the card battle by constructing the player and enemy <see cref="BattleCardPanel"/> instances,
|
/// Provides <see cref="IActionExecutor"/> instances for the three card
|
||||||
/// the <see cref="CardController"/> for drag-and-drop input, and the <see cref="EndTurnButton"/>.
|
/// action types (physics/attack, physics/defence, physics/stun) used
|
||||||
/// Enemy characters are deep-copied so defeats are tracked independently per battle instance.
|
/// during action resolution.
|
||||||
/// The <see cref="CardBattleActionProvider"/> is registered with <see cref="ActionExecutorHub"/>.
|
/// </summary>
|
||||||
|
public IActionExecutorProvider ActionProvider { get; } = new Actions.CardBattleActionProvider();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates the battle screen from <c>scenes/card_battle_screen.tscn</c>,
|
||||||
|
/// attaches it to the battle scene's <c>Panel</c> and binds the panels,
|
||||||
|
/// end-turn button and input controller. Enemy characters are deep-copied
|
||||||
|
/// so defeats are tracked independently per battle instance. The
|
||||||
|
/// <see cref="CardBattleActionProvider"/> is registered with
|
||||||
|
/// <see cref="ActionExecutorHub"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx)
|
public void Init(Character[] playerChars, Character[] enemyChars, IBattleContext ctx)
|
||||||
{
|
{
|
||||||
@@ -64,71 +75,42 @@ namespace Cthangover.CardBattle
|
|||||||
_ctx = ctx;
|
_ctx = ctx;
|
||||||
|
|
||||||
_enemyChars = new Character[enemyChars.Length];
|
_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];
|
_enemyChars[i] = enemyChars[i]?.Copy() ?? enemyChars[i];
|
||||||
|
|
||||||
ActionExecutorHub.Instance.SetActiveProvider(ActionProvider);
|
ActionExecutorHub.Instance.SetActiveProvider(ActionProvider);
|
||||||
|
|
||||||
var root = ctx.RootNode as Node;
|
var root = ctx.RootNode as Node;
|
||||||
|
|
||||||
var panel = root.GetNodeOrNull<Control>("Panel") ?? root;
|
var panel = root.GetNodeOrNull<Control>("Panel") ?? root;
|
||||||
|
|
||||||
_playerPanel = (BattleCardPanel)TscnScenes.LoadAndBuild("scenes/battle_card_panel.tscn");
|
_screen = (CardBattleScreen)TscnScenes.LoadAndBuild("scenes/card_battle_screen.tscn");
|
||||||
_playerPanel.Name = "PlayerPanel";
|
_screen.Name = "CardBattleScreen";
|
||||||
_playerPanel.EnsureConstructed();
|
_screen.EnsureConstructed();
|
||||||
panel.AddChild(_playerPanel);
|
panel.AddChild(_screen);
|
||||||
|
|
||||||
_enemyPanel = (BattleCardPanel)TscnScenes.LoadAndBuild("scenes/battle_card_panel.tscn");
|
_playerPanel = _screen.PlayerPanel;
|
||||||
_enemyPanel.Name = "EnemyPanel";
|
_enemyPanel = _screen.EnemyPanel;
|
||||||
_enemyPanel.AlignType = "Right";
|
|
||||||
_enemyPanel.EnsureConstructed();
|
|
||||||
_enemyPanel.MouseFilter = Control.MouseFilterEnum.Ignore;
|
|
||||||
panel.AddChild(_enemyPanel);
|
|
||||||
|
|
||||||
_actionPanel = new Control { Name = "ActionPanel" };
|
_endTurnButton = _screen.EndTurnButton;
|
||||||
panel.AddChild(_actionPanel);
|
|
||||||
|
|
||||||
_toolPanel = root.GetNodeOrNull<Control>("ToolPanel");
|
|
||||||
|
|
||||||
_endTurnButton = (EndTurnButton)TscnScenes.LoadAndBuild("scenes/end_turn_button.tscn");
|
|
||||||
_endTurnButton.Name = "EndTurnButton";
|
|
||||||
_endTurnButton.EnsureConstructed();
|
|
||||||
_endTurnButton.SetVisible(false);
|
_endTurnButton.SetVisible(false);
|
||||||
_endTurnButton.OnPressed += OnEndTurnPressed;
|
_endTurnButton.OnPressed += OnEndTurnPressed;
|
||||||
_toolPanel?.AddChild(_endTurnButton);
|
|
||||||
|
|
||||||
_cardController = new CardController { Name = "CardController" };
|
_cardController = _screen.Controller;
|
||||||
panel.AddChild(_cardController);
|
_cardController.Setup(_screen);
|
||||||
_cardController.FindPanels();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Positions the UI panels relative to the viewport, calculates a unified <c>cardScale</c> that
|
/// Calculates the pointer scale fitting both sides on screen, initializes
|
||||||
/// fits both player and enemy cards on screen, initializes all character cards, and starts the first player turn.
|
/// every card and starts the first player turn. Subscribes to
|
||||||
/// Subscribes to <c>OnCardDead</c> to record defeated enemies and to <c>OnActionExecuted</c> to detect
|
/// <c>OnCardDead</c> to record defeated enemies for the victory screen and
|
||||||
/// automatic turn completion when all player cards exhaust their action points.
|
/// to <c>OnActionExecuted</c> to detect automatic turn completion.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
var root = _ctx.RootNode as Node;
|
|
||||||
|
|
||||||
var panelSize = root.GetViewport().GetVisibleRect().Size;
|
|
||||||
var enemyCount = _enemyChars?.Length ?? 1;
|
var enemyCount = _enemyChars?.Length ?? 1;
|
||||||
|
|
||||||
_enemyPanel.UseRows = enemyCount > 6;
|
_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);
|
var cardScale = CalculateCardScale(enemyCount);
|
||||||
_cardController.CardScale = cardScale;
|
_cardController.CardScale = cardScale;
|
||||||
|
|
||||||
@@ -140,6 +122,7 @@ namespace Cthangover.CardBattle
|
|||||||
|
|
||||||
BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared;
|
BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared;
|
||||||
|
|
||||||
|
_round = 0;
|
||||||
_currentOrder = BattleSide.Player;
|
_currentOrder = BattleSide.Player;
|
||||||
StartPlayerTurn();
|
StartPlayerTurn();
|
||||||
}
|
}
|
||||||
@@ -147,31 +130,31 @@ namespace Cthangover.CardBattle
|
|||||||
private float CalculateCardScale(int enemyCount)
|
private float CalculateCardScale(int enemyCount)
|
||||||
{
|
{
|
||||||
var viewport = ((Node)_ctx.RootNode).GetViewport().GetVisibleRect().Size;
|
var viewport = ((Node)_ctx.RootNode).GetViewport().GetVisibleRect().Size;
|
||||||
float enemyPanelWidth = viewport.X * 0.96f;
|
var enemyPanelWidth = viewport.X * 0.96f;
|
||||||
float playerPanelWidth = viewport.X * 0.55f;
|
var playerPanelWidth = viewport.X * 0.55f;
|
||||||
float enemyPanelHeight = viewport.Y * 0.446f;
|
var enemyPanelHeight = viewport.Y * 0.446f;
|
||||||
|
|
||||||
float maxCardScale = 1f;
|
|
||||||
|
|
||||||
|
float maxCardScale;
|
||||||
if (_enemyPanel.UseRows)
|
if (_enemyPanel.UseRows)
|
||||||
{
|
{
|
||||||
int maxPerRow = _enemyPanel.GetMaxCardsPerRow();
|
var maxPerRow = _enemyPanel.GetMaxCardsPerRow();
|
||||||
int rows = _enemyPanel.GetCurrentRowCount(enemyCount);
|
var rows = _enemyPanel.GetCurrentRowCount(enemyCount);
|
||||||
|
|
||||||
float widthScale = (enemyPanelWidth - (maxPerRow - 1) * CELL_OFFSET) / (maxPerRow * BASE_CARD_WIDTH);
|
var widthScale = (enemyPanelWidth - (maxPerRow - 1) * CellOffset) / (maxPerRow * BaseCardWidth);
|
||||||
float heightScale = (enemyPanelHeight - (rows - 1) * CELL_OFFSET) / (rows * BASE_CARD_HEIGHT);
|
var heightScale = (enemyPanelHeight - (rows - 1) * CellOffset) / (rows * BaseCardHeight);
|
||||||
maxCardScale = Mathf.Max(0.1f, Mathf.Min(widthScale, heightScale));
|
maxCardScale = Mathf.Max(0.1f, Mathf.Min(widthScale, heightScale));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
int effectiveCount = Mathf.Max(enemyCount, 1);
|
var effectiveCount = Mathf.Max(enemyCount, 1);
|
||||||
maxCardScale = Mathf.Max(0.1f, (enemyPanelWidth - (effectiveCount - 1) * CELL_OFFSET) / (effectiveCount * BASE_CARD_WIDTH));
|
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(Mathf.Min(maxCardScale, playerFitScale), 1f);
|
||||||
return Mathf.Min(cardScale, 1f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEnemyCardDead(CharacterCardNode card)
|
private void OnEnemyCardDead(CharacterCardNode card)
|
||||||
@@ -185,34 +168,43 @@ namespace Cthangover.CardBattle
|
|||||||
ctx.RecordEnemyDefeated(card.Card);
|
ctx.RecordEnemyDefeated(card.Card);
|
||||||
ctx.NotifyCharacterDied(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()
|
private void StartPlayerTurn()
|
||||||
{
|
{
|
||||||
GameLogger.Log("CARD_BATTLE", "Player turn started", LogLevel.Debug);
|
_round += 1;
|
||||||
|
|
||||||
_currentOrder = BattleSide.Player;
|
_currentOrder = BattleSide.Player;
|
||||||
_playerTurnActive = true;
|
_playerTurnActive = true;
|
||||||
BattleSceneContext.Instance.IsWait = false;
|
BattleSceneContext.Instance.IsWait = false;
|
||||||
|
|
||||||
_cardController.ClearSelections();
|
_cardController.ClearSelections();
|
||||||
_cardController.ClearActions();
|
_cardController.ClearActions();
|
||||||
|
_screen.ActionsPanel.ClearBody();
|
||||||
|
_screen.ActionsPanel.ClearSubject();
|
||||||
|
|
||||||
_endTurnButton?.SetVisible(true);
|
_endTurnButton?.SetVisible(true);
|
||||||
|
|
||||||
foreach (var card in _playerPanel.CardList)
|
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.Card.Attributes.Point.Value = card.Card.Attributes.Point.BaseValue;
|
||||||
card.UpdateInfo();
|
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()
|
private void OnEndTurnPressed() => OnPlayerTurnEnd();
|
||||||
{
|
|
||||||
GameLogger.Log("CARD_BATTLE", "End turn button pressed", LogLevel.Debug);
|
|
||||||
OnPlayerTurnEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CheckPlayerTurnEnd()
|
private void CheckPlayerTurnEnd()
|
||||||
{
|
{
|
||||||
@@ -225,30 +217,24 @@ namespace Cthangover.CardBattle
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool allOutOfPoints = true;
|
|
||||||
foreach (var card in _playerPanel.CardList)
|
foreach (var card in _playerPanel.CardList)
|
||||||
{
|
{
|
||||||
if (!card.IsDead && card.Card?.Attributes?.Point?.Value > 0)
|
if (!card.IsDead && card.Card?.Attributes?.Point?.Value > 0)
|
||||||
{
|
return;
|
||||||
allOutOfPoints = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allOutOfPoints)
|
BattleLogHub.Log(Translate("battle/log_out_of_points"), BattleLogKind.Info);
|
||||||
OnPlayerTurnEnd();
|
OnPlayerTurnEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ends the player's turn by hiding the end-turn button, setting the battle to <c>IsWait</c>,
|
/// Ends the player's turn: hides the end-turn button, pauses the battle,
|
||||||
/// and yielding two frames for UI to settle before running the enemy AI turn via <c>RunEnemyTurn</c>.
|
/// plays the enemy-turn banner and journal line, waits for two frames plus
|
||||||
/// If all enemies are already dead, the battle ends immediately with the player as winner.
|
/// a short pause so the UI settles, then runs the enemy AI. Ends the battle
|
||||||
/// This method is <c>async void</c> so it can await frames without blocking the game loop.
|
/// immediately when no enemies remain.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async void OnPlayerTurnEnd()
|
public async void OnPlayerTurnEnd()
|
||||||
{
|
{
|
||||||
GameLogger.Log("CARD_BATTLE", "Player turn ended", LogLevel.Debug);
|
|
||||||
|
|
||||||
_playerTurnActive = false;
|
_playerTurnActive = false;
|
||||||
_endTurnButton?.SetVisible(false);
|
_endTurnButton?.SetVisible(false);
|
||||||
BattleSceneContext.Instance.IsWait = true;
|
BattleSceneContext.Instance.IsWait = true;
|
||||||
@@ -259,6 +245,10 @@ namespace Cthangover.CardBattle
|
|||||||
return;
|
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;
|
var root = (Node)_ctx.RootNode;
|
||||||
await root.ToSignal(root.GetTree(), "process_frame");
|
await root.ToSignal(root.GetTree(), "process_frame");
|
||||||
await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout");
|
await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout");
|
||||||
@@ -268,8 +258,6 @@ namespace Cthangover.CardBattle
|
|||||||
|
|
||||||
private async Task RunEnemyTurn()
|
private async Task RunEnemyTurn()
|
||||||
{
|
{
|
||||||
GameLogger.Log("CARD_BATTLE", "Enemy turn started", LogLevel.Debug);
|
|
||||||
|
|
||||||
_currentOrder = BattleSide.Enemy;
|
_currentOrder = BattleSide.Enemy;
|
||||||
_endTurnButton?.SetVisible(false);
|
_endTurnButton?.SetVisible(false);
|
||||||
|
|
||||||
@@ -280,11 +268,15 @@ namespace Cthangover.CardBattle
|
|||||||
if (card.IsDead || card.Card?.Actions == null || card.Card.Actions.Count == 0)
|
if (card.IsDead || card.Card?.Actions == null || card.Card.Actions.Count == 0)
|
||||||
continue;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,9 +290,17 @@ namespace Cthangover.CardBattle
|
|||||||
if (target == null)
|
if (target == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
GameLogger.Log("CARD_BATTLE",
|
card.UpdateInfo();
|
||||||
$"Enemy action: {card.Card.Name} -> {action.Name} -> {target.Card.Name}",
|
|
||||||
LogLevel.Message);
|
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);
|
var animatedAction = CreateAnimatedAction(card, target, action);
|
||||||
if (animatedAction != null)
|
if (animatedAction != null)
|
||||||
@@ -315,7 +315,6 @@ namespace Cthangover.CardBattle
|
|||||||
|
|
||||||
if (!HasAliveCards(_playerPanel))
|
if (!HasAliveCards(_playerPanel))
|
||||||
{
|
{
|
||||||
GameLogger.Log("CARD_BATTLE", "All players dead — enemy wins", LogLevel.Message);
|
|
||||||
_ctx.EndBattle(BattleSide.Enemy);
|
_ctx.EndBattle(BattleSide.Enemy);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -336,8 +335,7 @@ namespace Cthangover.CardBattle
|
|||||||
if (actions == null || actions.Count == 0)
|
if (actions == null || actions.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var random = new Random();
|
return actions[GD.RandRange(0, actions.Count - 1)];
|
||||||
return actions[random.Next(actions.Count)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharacterCardNode PickEnemyTarget(CharacterCardNode source, ActionCharacter action)
|
private CharacterCardNode PickEnemyTarget(CharacterCardNode source, ActionCharacter action)
|
||||||
@@ -350,7 +348,7 @@ namespace Cthangover.CardBattle
|
|||||||
|
|
||||||
if (alivePlayers.Count == 0)
|
if (alivePlayers.Count == 0)
|
||||||
return null;
|
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)
|
if (action.Type == ActionCharacterType.ToAlias || action.Type == ActionCharacterType.ToSelf)
|
||||||
@@ -360,7 +358,8 @@ namespace Cthangover.CardBattle
|
|||||||
return null;
|
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)
|
if (action.Type == ActionCharacterType.ToEnemy)
|
||||||
return new Actions.PhysicsAttackAction(source, target, action);
|
return new Actions.PhysicsAttackAction(source, target, action);
|
||||||
@@ -372,7 +371,7 @@ namespace Cthangover.CardBattle
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasAliveCards(BattleCardPanel panel)
|
private static bool HasAliveCards(BattleCardPanel panel)
|
||||||
{
|
{
|
||||||
foreach (var card in panel.CardList)
|
foreach (var card in panel.CardList)
|
||||||
{
|
{
|
||||||
@@ -382,14 +381,25 @@ namespace Cthangover.CardBattle
|
|||||||
return false;
|
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()
|
private void OnBattleCleared()
|
||||||
{
|
{
|
||||||
_playerPanel?.ClearAll();
|
_screen?.Clear();
|
||||||
_enemyPanel?.ClearAll();
|
|
||||||
_cardController?.ClearActions();
|
|
||||||
_endTurnButton?.SetVisible(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string Translate(string key)
|
||||||
|
=> TranslationServer.Translate(key).ToString();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <c>true</c> between <see cref="StartPlayerTurn"/> and the start of
|
/// <c>true</c> between <see cref="StartPlayerTurn"/> and the start of
|
||||||
/// <see cref="OnPlayerTurnEnd"/> — matches the visibility window of the
|
/// <see cref="OnPlayerTurnEnd"/> — matches the visibility window of the
|
||||||
@@ -397,9 +407,7 @@ namespace Cthangover.CardBattle
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsWaitingForPlayerInput => _playerTurnActive;
|
public bool IsWaitingForPlayerInput => _playerTurnActive;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Automation hook: ends the player's turn without selecting cards.</summary>
|
||||||
/// Automation hook: ends the player's turn without selecting cards.
|
|
||||||
/// </summary>
|
|
||||||
public void AdvancePlayerTurn() => OnPlayerTurnEnd();
|
public void AdvancePlayerTurn() => OnPlayerTurnEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+7
-15
@@ -2,23 +2,15 @@
|
|||||||
|
|
||||||
namespace Cthangover.CardBattle
|
namespace Cthangover.CardBattle
|
||||||
{
|
{
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Common interface for all card UI nodes in the card battle system.
|
/// Common contract for every card the input controller can hit-test. Both
|
||||||
/// Exposes the <c>Frame</c> and <c>Image</c> <see cref="TextureRect"/> controls so that
|
/// the character cards on the field and the draggable action cards expose
|
||||||
/// <see cref="CardController"/> can perform hit-testing during drag-and-drop targeting
|
/// their root <see cref="Control"/>, because layout, scale and the drag
|
||||||
/// without knowing the concrete card type (<see cref="CharacterCardNode"/> or <see cref="ActionCardNode"/>).
|
/// ghost all operate on the whole card rect rather than on an inner sprite.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICard
|
public interface ICard
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Root control used for hit-testing and for reparenting during a drag.</summary>
|
||||||
/// The frame <see cref="TextureRect"/> used for hit-testing whether a pointer is over this card.
|
Control GetControlNode();
|
||||||
/// </summary>
|
|
||||||
TextureRect Frame { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The portrait/image <see cref="TextureRect"/> of the card, used for dissolve shader effects on death.
|
|
||||||
/// </summary>
|
|
||||||
TextureRect Image { get; }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
@@ -1,30 +1,31 @@
|
|||||||
using Cthangover.CardBattle.UI;
|
using Cthangover.CardBattle.UI;
|
||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
using Cthangover.TurnBattle;
|
using Cthangover.TurnBattle;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.CardBattle.Player
|
namespace Cthangover.CardBattle.Player
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategy for <c>ToEnemy</c> attack actions. Validates that the target is an enemy
|
/// Strategy for <c>ToEnemy</c> actions. Validates that the target is an enemy,
|
||||||
/// (non-player card), highlights both cards with a red attack outline, and executes
|
/// highlights both cards with a red attack outline, executes the action via
|
||||||
/// damage via <see cref="ActionExecutorHub"/> with floating damage/defence numbers.
|
/// <see cref="ActionExecutorHub"/> and reports the outcome: floating damage /
|
||||||
/// This is the standard strategy for all offensive cards in the player's deck.
|
/// defence-loss numbers on the target, a colour pulse on the card and a
|
||||||
|
/// journal row through <see cref="BattleLogHub"/>. 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AttackToEnemyStrategy : ICardActionStrategy
|
public class AttackToEnemyStrategy : ICardActionStrategy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Returns <c>true</c> if the action type is <c>ToEnemy</c> and the target is an enemy card.</summary>
|
||||||
/// Returns <c>true</c> if the action type is <c>ToEnemy</c> and the target is an enemy card.
|
|
||||||
/// </summary>
|
|
||||||
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
return action.Type == ActionCharacterType.ToEnemy && target != null && !target.IsPlayer;
|
return action.Type == ActionCharacterType.ToEnemy && target != null && !target.IsPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Shows a red attack overlay on both the dragged action card and the target enemy card.</summary>
|
||||||
/// Shows a red attack overlay on both the dragged action card and the target enemy card.
|
|
||||||
/// </summary>
|
|
||||||
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
actionCard.Attack();
|
actionCard.Attack();
|
||||||
@@ -32,8 +33,9 @@ namespace Cthangover.CardBattle.Player
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the attack via <see cref="ActionExecutorHub"/> and spawns floating damage
|
/// Resolves the attack and publishes its result: damage and absorbed
|
||||||
/// and defence-loss numbers above the <paramref name="target"/> card.
|
/// shield as floating numbers plus journal rows, or the stun effect with
|
||||||
|
/// its zap flash for control actions.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
||||||
{
|
{
|
||||||
@@ -44,15 +46,50 @@ namespace Cthangover.CardBattle.Player
|
|||||||
source.UpdateInfo();
|
source.UpdateInfo();
|
||||||
target.UpdateInfo();
|
target.UpdateInfo();
|
||||||
|
|
||||||
if (result.Result)
|
if (!result.Result)
|
||||||
{
|
return;
|
||||||
if (result.Target.Damage > 0)
|
|
||||||
ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition);
|
|
||||||
|
|
||||||
var defenceLost = -result.Target.Defence;
|
var actionName = Translate(action.Name);
|
||||||
if (defenceLost > 0)
|
var sourceName = Translate(source.Card.Name);
|
||||||
ShowDamageBehaviour.SpawnDefence(defenceLost, target, target.GlobalPosition);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+25
-21
@@ -1,41 +1,36 @@
|
|||||||
using Cthangover.CardBattle.UI;
|
using Cthangover.CardBattle.UI;
|
||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
using Cthangover.TurnBattle;
|
using Cthangover.TurnBattle;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.CardBattle.Player
|
namespace Cthangover.CardBattle.Player
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategy for self-targeting actions (<c>ToSelf</c>). Validates that source and target
|
/// Strategy for self-targeting actions (<c>ToSelf</c>). Validates that the
|
||||||
/// are the same card, highlights with a green selection outline, and applies the action
|
/// source and target are the same card, highlights with a green selection
|
||||||
/// to the source card itself via <see cref="ActionExecutorHub"/>. Used for self-buffs like
|
/// outline and applies the effect to the caster. Shield gains print a
|
||||||
/// defence boosts. Damage and defence floating text spawn above the source card.
|
/// floating blue number and a journal row; the card pulses to make the buff
|
||||||
|
/// readable without reading the log.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SelfStrategy : ICardActionStrategy
|
public class SelfStrategy : ICardActionStrategy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Returns <c>true</c> only when <paramref name="source"/> and <paramref name="target"/> are the same card.</summary>
|
||||||
/// Returns <c>true</c> only when <paramref name="source"/> and <paramref name="target"/> are the same card.
|
|
||||||
/// </summary>
|
|
||||||
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
return source != null && source == target;
|
return source != null && source == target;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Shows a green selection overlay on both the dragged action card and the source card.</summary>
|
||||||
/// Shows a green selection overlay on both the dragged action card and the source card.
|
|
||||||
/// </summary>
|
|
||||||
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
actionCard.Select();
|
actionCard.Select();
|
||||||
target.Select();
|
target.Select();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Executes the self-targeted action and reports shields and healing.</summary>
|
||||||
/// Executes the self-targeted action and spawns floating defence/damage numbers
|
|
||||||
/// above the <paramref name="source"/> card. Damage numbers are marked with the
|
|
||||||
/// <c>isHeal</c> flag for green coloring via <see cref="ShowDamageBehaviour"/>.
|
|
||||||
/// </summary>
|
|
||||||
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
||||||
{
|
{
|
||||||
if (source?.Card == null || action == null)
|
if (source?.Card == null || action == null)
|
||||||
@@ -44,14 +39,23 @@ namespace Cthangover.CardBattle.Player
|
|||||||
var result = ActionExecutorHub.Instance.Execute(action, source.Card, source.Card);
|
var result = ActionExecutorHub.Instance.Execute(action, source.Card, source.Card);
|
||||||
source.UpdateInfo();
|
source.UpdateInfo();
|
||||||
|
|
||||||
if (result.Result)
|
if (!result.Result)
|
||||||
{
|
return;
|
||||||
if (result.Target.Defence > 0)
|
|
||||||
ShowDamageBehaviour.SpawnDefence(result.Target.Defence, source, source.GlobalPosition);
|
|
||||||
|
|
||||||
if (result.Target.Damage > 0)
|
if (result.Target.Defence > 0)
|
||||||
ShowDamageBehaviour.SpawnDamage(result.Target.Damage, source, source.GlobalPosition, true);
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,40 +1,36 @@
|
|||||||
using Cthangover.CardBattle.UI;
|
using Cthangover.CardBattle.UI;
|
||||||
using Cthangover.Core.Battle;
|
using Cthangover.Core.Battle;
|
||||||
using Cthangover.Core.Battle.Actions;
|
using Cthangover.Core.Battle.Actions;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
using Cthangover.TurnBattle;
|
using Cthangover.TurnBattle;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.CardBattle.Player
|
namespace Cthangover.CardBattle.Player
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strategy for ally-support actions (<c>ToAlias</c>). Validates that the target is a friendly
|
/// Strategy for ally-support actions (<c>ToAlias</c>). Validates that the
|
||||||
/// player card, highlights with green selection overlays, and applies defence/healing effects
|
/// target is a friendly card, highlights with green selection overlays and
|
||||||
/// via <see cref="ActionExecutorHub"/>. Allows one player card to buff another during the player's turn.
|
/// applies the effect via <see cref="ActionExecutorHub"/>. 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SupportStrategy : ICardActionStrategy
|
public class SupportStrategy : ICardActionStrategy
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>Returns <c>true</c> when the action type is <c>ToAlias</c> and the target is a player card.</summary>
|
||||||
/// Returns <c>true</c> when the action type is <c>ToAlias</c> and the target is a player card.
|
|
||||||
/// </summary>
|
|
||||||
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
public bool Check(ActionCharacter action, CharacterCardNode source, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
return action.Type == ActionCharacterType.ToAlias && target != null && target.IsPlayer;
|
return action.Type == ActionCharacterType.ToAlias && target != null && target.IsPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Shows a green selection overlay on both the dragged action card and the target ally card.</summary>
|
||||||
/// Shows a green selection overlay on both the dragged action card and the target ally card.
|
|
||||||
/// </summary>
|
|
||||||
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
public void HighlightTarget(ActionCardNode actionCard, CharacterCardNode target)
|
||||||
{
|
{
|
||||||
actionCard.Select();
|
actionCard.Select();
|
||||||
target.Select();
|
target.Select();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Resolves the support action and reports shields and healing.</summary>
|
||||||
/// Resolves the support action and spawns floating defence and heal text above
|
|
||||||
/// the <paramref name="target"/> ally. Damage numbers are shown with the <c>isHeal</c>
|
|
||||||
/// flag for green coloring.
|
|
||||||
/// </summary>
|
|
||||||
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
public void Execute(CharacterCardNode source, CharacterCardNode target, ActionCharacter action)
|
||||||
{
|
{
|
||||||
if (source?.Card == null || target?.Card == null || action == null)
|
if (source?.Card == null || target?.Card == null || action == null)
|
||||||
@@ -44,14 +40,23 @@ namespace Cthangover.CardBattle.Player
|
|||||||
source.UpdateInfo();
|
source.UpdateInfo();
|
||||||
target.UpdateInfo();
|
target.UpdateInfo();
|
||||||
|
|
||||||
if (result.Result)
|
if (!result.Result)
|
||||||
{
|
return;
|
||||||
if (result.Target.Defence > 0)
|
|
||||||
ShowDamageBehaviour.SpawnDefence(result.Target.Defence, target, target.GlobalPosition);
|
|
||||||
|
|
||||||
if (result.Target.Damage > 0)
|
if (result.Target.Defence > 0)
|
||||||
ShowDamageBehaviour.SpawnDamage(result.Target.Damage, target, target.GlobalPosition, true);
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+160
-84
@@ -1,5 +1,5 @@
|
|||||||
using Cthangover.Core.Cards;
|
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
|
using Cthangover.Core.UI.CharacterPanel;
|
||||||
using Cthangover.Core.UI;
|
using Cthangover.Core.UI;
|
||||||
using Cthangover.Core.UI.Theming;
|
using Cthangover.Core.UI.Theming;
|
||||||
using Godot;
|
using Godot;
|
||||||
@@ -7,134 +7,210 @@ using Godot;
|
|||||||
namespace Cthangover.CardBattle.UI
|
namespace Cthangover.CardBattle.UI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI node representing a single action card that the player can drag onto a target.
|
/// Draggable action card from the interface mockup (mockups/card_battle):
|
||||||
/// Implements <see cref="ICard"/> so <see cref="CardController"/> can hit-test drag targets.
|
/// the horizontal kit card with a type-coloured accent strip, action icon,
|
||||||
/// Displayed in the <see cref="CardController"/>'s action panel below the character cards.
|
/// name and ID, badges (targeting type, action-point cost, stun turns) and
|
||||||
/// Supports hover scale-up animation, selection highlighting (<see cref="Select"/>, <see cref="Attack"/>, <see cref="Invalid"/>),
|
/// a property-chip row. The node tree lives in <c>scenes/action_card.tscn</c>;
|
||||||
/// and info updates from the underlying <see cref="ActionCharacter"/> data model.
|
/// this class fetches the nodes, dresses them and fills the dynamic badge /
|
||||||
|
/// chip rows using the shared <see cref="ActionCharacterUi"/> presentation
|
||||||
|
/// rules, so the battle UI can never disagree with the character panel about
|
||||||
|
/// an action's colour, caption or cost.
|
||||||
|
/// <para/>
|
||||||
|
/// Implements <see cref="ICard"/> so the input controller can hit-test drag
|
||||||
|
/// targets; the hover style, disabled state and the preview builder
|
||||||
|
/// (<see cref="DescribeFrom"/>) are the only UI surface the controller and
|
||||||
|
/// the actions panel need.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class ActionCardNode : ModWidget, ICard
|
public partial class ActionCardNode : ModWidget, ICard
|
||||||
{
|
{
|
||||||
private TextureRect frame;
|
private PanelContainer _panel;
|
||||||
private TextureRect image;
|
private ColorRect _accent;
|
||||||
private Label nameLabel;
|
private TextureRect _icon;
|
||||||
|
private Label _nameLabel;
|
||||||
|
private Label _idLabel;
|
||||||
|
private HBoxContainer _badges;
|
||||||
|
private HBoxContainer _props;
|
||||||
|
|
||||||
/// <inheritdoc />
|
private StyleBoxFlat _idleStyle;
|
||||||
public TextureRect Frame => frame;
|
private StyleBoxFlat _hoverStyle;
|
||||||
/// <inheritdoc />
|
private bool _disabled;
|
||||||
public TextureRect Image => image;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>The action data model backing this card.</summary>
|
||||||
/// The <see cref="ActionCharacter"/> data model backing this card. Contains the action's
|
|
||||||
/// name, image, type, and stat modifiers used during execution.
|
|
||||||
/// </summary>
|
|
||||||
public ActionCharacter Card { get; set; }
|
public ActionCharacter Card { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>True while the character cannot pay the card's action-point cost.</summary>
|
||||||
/// Returns <c>this</c> — action cards are <see cref="Control"/> nodes themselves,
|
public bool IsDisabled => _disabled;
|
||||||
/// unlike <see cref="CharacterCardNode"/> which wraps a separate control.
|
|
||||||
/// </summary>
|
|
||||||
public Control GetControlNode() => this;
|
|
||||||
|
|
||||||
private Tween hoverTween;
|
/// <summary>Raised when the cursor enters the card; the actions panel uses it to print the preview.</summary>
|
||||||
private float baseY;
|
public event System.Action<ActionCardNode> Hovered;
|
||||||
private bool isHovered;
|
|
||||||
private Vector2 _baseScale;
|
/// <summary>Raised when the cursor leaves the card; the preview returns to its default text.</summary>
|
||||||
|
public event System.Action<ActionCardNode> Unhovered;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Control GetControlNode() => this;
|
||||||
|
|
||||||
protected override void Construct()
|
protected override void Construct()
|
||||||
{
|
{
|
||||||
CthangoverTheme.Apply(this);
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
image = GetNode<TextureRect>("Image");
|
_panel = GetNode<PanelContainer>("Panel");
|
||||||
frame = GetNode<TextureRect>("Frame");
|
_accent = GetNode<ColorRect>("Panel/Row/Accent");
|
||||||
nameLabel = GetNode<Label>("NameLabel");
|
_icon = GetNode<TextureRect>("Panel/Row/IconWrap/Icon");
|
||||||
|
_nameLabel = GetNode<Label>("Panel/Row/Body/Column/NameRow/NameLabel");
|
||||||
|
_idLabel = GetNode<Label>("Panel/Row/Body/Column/NameRow/IdLabel");
|
||||||
|
_badges = GetNode<HBoxContainer>("Panel/Row/Body/Column/Badges");
|
||||||
|
_props = GetNode<HBoxContainer>("Panel/Row/Body/Column/Props");
|
||||||
|
|
||||||
Size = CustomMinimumSize;
|
_idleStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Surface1, UiTokens.LineCard);
|
||||||
|
_hoverStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.SurfaceHover, UiTokens.LineStrong);
|
||||||
|
_panel.AddThemeStyleboxOverride("panel", _idleStyle);
|
||||||
|
|
||||||
|
_nameLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
_idLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
|
||||||
MouseEntered += OnMouseEntered;
|
MouseEntered += OnMouseEntered;
|
||||||
MouseExited += OnMouseExited;
|
MouseExited += OnMouseExited;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Placeholder for selection highlight. Currently unused — action cards use
|
/// Fills the card from the action definition: icon, name, ID, accent
|
||||||
/// <see cref="Attack"/> and <see cref="Invalid"/> for drag-over feedback instead.
|
/// strip, badges and property chips. Called by the input controller when
|
||||||
|
/// the action deck of a character is opened.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public void Init(ActionCharacter card)
|
||||||
|
{
|
||||||
|
Card = card;
|
||||||
|
|
||||||
|
if (_icon != null && card?.Image != null)
|
||||||
|
_icon.Texture = card.Image;
|
||||||
|
|
||||||
|
_accent.Color = ActionCharacterUi.AccentColor(card.Type);
|
||||||
|
_nameLabel.Text = ActionCharacterUi.Translate(card.Name);
|
||||||
|
_idLabel.Text = card.ID;
|
||||||
|
TooltipText = ActionCharacterUi.BuildTooltip(card);
|
||||||
|
|
||||||
|
BuildBadges();
|
||||||
|
BuildProps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Refreshes the texts of an already initialised card.</summary>
|
||||||
|
public void UpdateInfo()
|
||||||
|
{
|
||||||
|
if (Card == null)
|
||||||
|
return;
|
||||||
|
_nameLabel.Text = ActionCharacterUi.Translate(Card.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dims the card when the selected character cannot pay its cost; hover
|
||||||
|
/// feedback is suppressed so the disabled state reads as inert.
|
||||||
|
/// </summary>
|
||||||
|
public void SetDisabled(bool disabled)
|
||||||
|
{
|
||||||
|
_disabled = disabled;
|
||||||
|
Modulate = UiTokens.Alpha(UiTokens.White, disabled ? 0.45f : 1f);
|
||||||
|
_panel.AddThemeStyleboxOverride("panel", _idleStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the hint printed in the actions panel when the card is
|
||||||
|
/// hovered: the expected effect computed from the source character's
|
||||||
|
/// stats ("Урон 5 (АТК 5 × 1.0) · расход 1 ОД").
|
||||||
|
/// </summary>
|
||||||
|
public string DescribeFrom(Character source)
|
||||||
|
{
|
||||||
|
if (Card == null || source?.Attributes == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var cost = ActionCharacterUi.CostOf(Card);
|
||||||
|
if (Card.Type == ActionCharacterType.ToEnemy && Card.GetInt("Turn", 0) > 1)
|
||||||
|
return string.Format(Translate("battle/preview_stun"), Card.GetInt("Turn", 0), cost);
|
||||||
|
|
||||||
|
if (Card.Type == ActionCharacterType.ToEnemy)
|
||||||
|
{
|
||||||
|
var multiplier = Card.GetFloat("Attack", 1f);
|
||||||
|
var damage = Mathf.RoundToInt(multiplier * source.Attributes.Attack.Value);
|
||||||
|
return string.Format(
|
||||||
|
Translate("battle/preview_damage"),
|
||||||
|
damage,
|
||||||
|
source.Attributes.Attack.Value,
|
||||||
|
multiplier.ToString("0.0"),
|
||||||
|
cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Format(Translate("battle/preview_defence"), Card.GetInt("Defence"), cost);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Placeholder for the source-card highlight; the drag ghost covers it.</summary>
|
||||||
public void Select()
|
public void Select()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Placeholder: target highlighting is applied by the action strategy.</summary>
|
||||||
/// Placeholder for attack-mode highlight. Currently unused — target highlighting
|
|
||||||
/// is applied by <see cref="ICardActionStrategy.HighlightTarget"/> instead.
|
|
||||||
/// </summary>
|
|
||||||
public void Attack()
|
public void Attack()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Placeholder: validity is painted on the target card instead.</summary>
|
||||||
/// Placeholder for invalid-target highlight. Currently unused — invalidity is
|
|
||||||
/// indicated by <see cref="ICardActionStrategy.HighlightTarget"/> calling
|
|
||||||
/// <see cref="CharacterCardNode.Invalid"/> on the target card instead.
|
|
||||||
/// </summary>
|
|
||||||
public void Invalid()
|
public void Invalid()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Restores the neutral Z-index after a cancelled drag.</summary>
|
||||||
/// Resets the Z-index to 0 when the card is no longer being dragged.
|
|
||||||
/// </summary>
|
|
||||||
public void Unselect()
|
public void Unselect()
|
||||||
{
|
{
|
||||||
ZIndex = 0;
|
ZIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BuildBadges()
|
||||||
|
{
|
||||||
|
foreach (var child in _badges.GetChildren())
|
||||||
|
{
|
||||||
|
_badges.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
var accent = ActionCharacterUi.AccentColor(Card.Type);
|
||||||
|
_badges.AddChild(ActionCharacterUi.MakeBadge(
|
||||||
|
ActionCharacterUi.Translate(ActionCharacterUi.TypeLabelKey(Card.Type)).ToUpperInvariant(), accent));
|
||||||
|
|
||||||
|
var cost = ActionCharacterUi.CostText(Card);
|
||||||
|
if (!string.IsNullOrEmpty(cost))
|
||||||
|
_badges.AddChild(ActionCharacterUi.MakeBadge(cost, UiTokens.Info));
|
||||||
|
|
||||||
|
var turns = Card.GetInt("Turn", 0);
|
||||||
|
if (turns > 1 && Card.Type == ActionCharacterType.ToEnemy)
|
||||||
|
_badges.AddChild(ActionCharacterUi.MakeBadge(
|
||||||
|
string.Format(Translate("battle/turns_badge"), turns), UiTokens.Cooldown));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildProps()
|
||||||
|
{
|
||||||
|
foreach (var child in _props.GetChildren())
|
||||||
|
{
|
||||||
|
_props.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in ActionCharacterUi.SortedProperties(Card))
|
||||||
|
_props.AddChild(ActionCharacterUi.MakePropertyChip(pair.Key, pair.Value));
|
||||||
|
}
|
||||||
|
|
||||||
private void OnMouseEntered()
|
private void OnMouseEntered()
|
||||||
{
|
{
|
||||||
if (isHovered) return;
|
if (!_disabled)
|
||||||
isHovered = true;
|
_panel.AddThemeStyleboxOverride("panel", _hoverStyle);
|
||||||
baseY = Position.Y;
|
Hovered?.Invoke(this);
|
||||||
hoverTween?.Kill();
|
|
||||||
hoverTween = CreateTween();
|
|
||||||
hoverTween.SetParallel(true);
|
|
||||||
hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
|
||||||
hoverTween.TweenProperty(this, "position:y", baseY - 10f, 0.12f);
|
|
||||||
hoverTween.TweenProperty(this, "scale", _baseScale * 1.15f, 0.12f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseExited()
|
private void OnMouseExited()
|
||||||
{
|
{
|
||||||
if (!isHovered) return;
|
_panel.AddThemeStyleboxOverride("panel", _idleStyle);
|
||||||
isHovered = false;
|
Unhovered?.Invoke(this);
|
||||||
hoverTween?.Kill();
|
|
||||||
hoverTween = CreateTween();
|
|
||||||
hoverTween.SetParallel(true);
|
|
||||||
hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
|
||||||
hoverTween.TweenProperty(this, "position:y", baseY, 0.12f);
|
|
||||||
hoverTween.TweenProperty(this, "scale", _baseScale, 0.12f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private static string Translate(string key)
|
||||||
/// Initializes the card with data from <paramref name="card"/>, caches the base scale
|
=> TranslationServer.Translate(key).ToString();
|
||||||
/// for hover animation, sets the card image texture, and refreshes displayed text.
|
|
||||||
/// Called by <see cref="CardController.CreateAction"/>.
|
|
||||||
/// </summary>
|
|
||||||
public void Init(ActionCharacter card)
|
|
||||||
{
|
|
||||||
Card = card;
|
|
||||||
_baseScale = Scale;
|
|
||||||
|
|
||||||
if (image != null)
|
|
||||||
image.Texture = card.Image;
|
|
||||||
UpdateInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refreshes the displayed name label from the translated <see cref="ActionCharacter.Name"/>.
|
|
||||||
/// </summary>
|
|
||||||
public void UpdateInfo()
|
|
||||||
{
|
|
||||||
if (nameLabel != null && Card != null)
|
|
||||||
nameLabel.Text = TranslationServer.Translate(Card.Name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// "Действия" window from the interface mockup (mockups/card_battle): the
|
||||||
|
/// lower-right panel that opens when a party card is clicked. Layout lives
|
||||||
|
/// in <c>scenes/battle_actions_panel.tscn</c>: a header with the character
|
||||||
|
/// name and action-point pips, a two-column grid that receives the
|
||||||
|
/// <see cref="ActionCardNode"/> instances, a description well and a footer
|
||||||
|
/// hint. The class owns only content: it repaints pips, swaps the empty
|
||||||
|
/// state and prints the hovered action's numbers.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BattleActionsPanel : ModWidget
|
||||||
|
{
|
||||||
|
private const int PipSize = 11;
|
||||||
|
|
||||||
|
private Label _subject;
|
||||||
|
private HBoxContainer _pips;
|
||||||
|
private Label _apHint;
|
||||||
|
private Label _emptyLabel;
|
||||||
|
private GridContainer _grid;
|
||||||
|
private PanelContainer _descWell;
|
||||||
|
private Label _descLabel;
|
||||||
|
|
||||||
|
/// <summary>Container that receives action cards; owned by the input controller.</summary>
|
||||||
|
public Control Body => _grid;
|
||||||
|
|
||||||
|
protected override void Construct()
|
||||||
|
{
|
||||||
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
|
_subject = GetNode<Label>("Window/Column/Header/HeaderRow/SubjectLabel");
|
||||||
|
_pips = GetNode<HBoxContainer>("Window/Column/Header/HeaderRow/Pips");
|
||||||
|
_apHint = GetNode<Label>("Window/Column/Header/HeaderRow/ApHint");
|
||||||
|
_emptyLabel = GetNode<Label>("Window/Column/Body/BodyColumn/EmptyLabel");
|
||||||
|
_grid = GetNode<GridContainer>("Window/Column/Body/BodyColumn/Grid");
|
||||||
|
_descWell = GetNode<PanelContainer>("Window/Column/DescWrap/DescWell");
|
||||||
|
_descLabel = GetNode<Label>("Window/Column/DescWrap/DescWell/DescLabel");
|
||||||
|
|
||||||
|
var windowStyle = UiKit.Frame(UiTokens.RadiusLg, UiTokens.Alpha(UiTokens.Surface0, 0.84f), UiTokens.Line);
|
||||||
|
windowStyle.ContentMarginLeft = 0;
|
||||||
|
windowStyle.ContentMarginRight = 0;
|
||||||
|
windowStyle.ContentMarginTop = 0;
|
||||||
|
windowStyle.ContentMarginBottom = 0;
|
||||||
|
windowStyle.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||||
|
windowStyle.ShadowSize = 18;
|
||||||
|
windowStyle.ShadowOffset = new Vector2(0f, 8f);
|
||||||
|
GetNode<PanelContainer>("Window").AddThemeStyleboxOverride("panel", windowStyle);
|
||||||
|
|
||||||
|
foreach (var path in new[]
|
||||||
|
{
|
||||||
|
"Window/Column/Header/HeaderRow/TitleLabel",
|
||||||
|
"Window/Column/Header/HeaderRow/ApCaption",
|
||||||
|
"Window/Column/Body/BodyColumn/EmptyLabel",
|
||||||
|
"Window/Column/Footer/FooterRow/HintLabel",
|
||||||
|
"Window/Column/Footer/FooterRow/AutoEndLabel",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var label = GetNode<Label>(path);
|
||||||
|
label.Text = Translate(label.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
GetNode<Label>("Window/Column/Header/HeaderRow/TitleLabel").AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
GetNode<Label>("Window/Column/Header/HeaderRow/ApCaption").AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
_apHint.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
_emptyLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
GetNode<Label>("Window/Column/Footer/FooterRow/HintLabel").AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
GetNode<Label>("Window/Column/Footer/FooterRow/AutoEndLabel").AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
|
||||||
|
var descStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Surface2, UiTokens.Line);
|
||||||
|
descStyle.ContentMarginLeft = 12;
|
||||||
|
descStyle.ContentMarginRight = 12;
|
||||||
|
descStyle.ContentMarginTop = 10;
|
||||||
|
descStyle.ContentMarginBottom = 10;
|
||||||
|
_descWell.AddThemeStyleboxOverride("panel", descStyle);
|
||||||
|
ClearSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Shows the selected character's name in the header.</summary>
|
||||||
|
public void SetSubject(string name)
|
||||||
|
{
|
||||||
|
_subject.RemoveThemeColorOverride("font_color");
|
||||||
|
_subject.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
_subject.Text = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores the "no character selected" header and the empty state.</summary>
|
||||||
|
public void ClearSubject()
|
||||||
|
{
|
||||||
|
_subject.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
_subject.Text = Translate("battle/no_character");
|
||||||
|
_pips.Visible = false;
|
||||||
|
_apHint.Text = string.Empty;
|
||||||
|
SetDescriptionDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Repaints the action-point pips for the selected character.</summary>
|
||||||
|
public void SetPoints(int current, int basePoints)
|
||||||
|
{
|
||||||
|
_pips.Visible = true;
|
||||||
|
foreach (var child in _pips.GetChildren())
|
||||||
|
{
|
||||||
|
_pips.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < basePoints; i++)
|
||||||
|
{
|
||||||
|
var on = i < current;
|
||||||
|
var pip = new Panel
|
||||||
|
{
|
||||||
|
MouseFilter = MouseFilterEnum.Ignore,
|
||||||
|
CustomMinimumSize = new Vector2(PipSize, PipSize),
|
||||||
|
};
|
||||||
|
pip.AddThemeStyleboxOverride("panel", UiKit.Frame(
|
||||||
|
PipSize / 2,
|
||||||
|
on ? UiTokens.AccentBright : UiTokens.LineDisabled,
|
||||||
|
on ? UiTokens.Accent : UiTokens.LineFaint));
|
||||||
|
_pips.AddChild(pip);
|
||||||
|
}
|
||||||
|
|
||||||
|
_apHint.Text = current + " / " + basePoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Frees every action card and restores the empty state.</summary>
|
||||||
|
public void ClearBody()
|
||||||
|
{
|
||||||
|
foreach (var child in _grid.GetChildren())
|
||||||
|
{
|
||||||
|
_grid.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
RefreshEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hides the empty state as soon as the grid holds action cards.</summary>
|
||||||
|
public void RefreshEmpty()
|
||||||
|
=> _emptyLabel.Visible = _grid.GetChildCount() == 0;
|
||||||
|
|
||||||
|
/// <summary>Prints the hovered action preview ("Урон 5 · расход 1 ОД") in the well.</summary>
|
||||||
|
public void SetDescription(string text)
|
||||||
|
{
|
||||||
|
_descLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
_descLabel.Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Restores the well's invitation text.</summary>
|
||||||
|
public void SetDescriptionDefault()
|
||||||
|
{
|
||||||
|
_descLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
_descLabel.Text = Translate("battle/desc_default");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Translate(string key)
|
||||||
|
=> TranslationServer.Translate(key).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Centre-screen turn banner ("Ход игрока" / "Ход врага") from the interface
|
||||||
|
/// mockup (mockups/card_battle). The plate lives in
|
||||||
|
/// <c>scenes/battle_banner.tscn</c>; the class only repaints the caption and
|
||||||
|
/// runs the fade-in / hold / fade-out tween. Tweens are driven by the
|
||||||
|
/// process delta, so the battle speed control scales them together with the
|
||||||
|
/// action animations.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BattleBanner : ModWidget
|
||||||
|
{
|
||||||
|
private const float FadeInSeconds = 0.25f;
|
||||||
|
private const float HoldSeconds = 1.0f;
|
||||||
|
private const float FadeOutSeconds = 0.3f;
|
||||||
|
|
||||||
|
private PanelContainer _plate;
|
||||||
|
private Label _label;
|
||||||
|
private Tween _tween;
|
||||||
|
|
||||||
|
protected override void Construct()
|
||||||
|
{
|
||||||
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
|
_plate = GetNode<PanelContainer>("Plate");
|
||||||
|
_label = GetNode<Label>("Plate/Label");
|
||||||
|
|
||||||
|
var style = UiKit.Frame(0, UiTokens.Alpha(UiTokens.Page, 0.62f), new Color(0f, 0f, 0f, 0f));
|
||||||
|
style.BorderWidthTop = 1;
|
||||||
|
style.BorderWidthBottom = 1;
|
||||||
|
style.BorderColor = UiTokens.AccentLine;
|
||||||
|
style.ContentMarginLeft = 44;
|
||||||
|
style.ContentMarginRight = 44;
|
||||||
|
style.ContentMarginTop = 12;
|
||||||
|
style.ContentMarginBottom = 12;
|
||||||
|
_plate.AddThemeStyleboxOverride("panel", style);
|
||||||
|
|
||||||
|
_label.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
_plate.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pops the banner with the given caption, then fades it away.</summary>
|
||||||
|
public void Show(string text)
|
||||||
|
{
|
||||||
|
_label.Text = text;
|
||||||
|
_tween?.Kill();
|
||||||
|
_plate.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
_tween = CreateTween();
|
||||||
|
_tween.TweenProperty(_plate, "modulate:a", 1f, FadeInSeconds).SetTrans(Tween.TransitionType.Cubic);
|
||||||
|
_tween.TweenInterval(HoldSeconds);
|
||||||
|
_tween.TweenProperty(_plate, "modulate:a", 0f, FadeOutSeconds).SetTrans(Tween.TransitionType.Cubic);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hides the banner immediately (battle cleared).</summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_tween?.Kill();
|
||||||
|
_plate.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ namespace Cthangover.CardBattle.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public CharacterCardNode Create(Character card, bool isPlayer, float cardScale)
|
public CharacterCardNode Create(Character card, bool isPlayer, float cardScale)
|
||||||
{
|
{
|
||||||
var behaviour = (CharacterCardNode)TscnScenes.LoadAndBuild("scenes/character_card_node.tscn");
|
var behaviour = (CharacterCardNode)TscnScenes.LoadAndBuild("scenes/character_card.tscn");
|
||||||
behaviour.EnsureConstructed();
|
behaviour.EnsureConstructed();
|
||||||
|
|
||||||
if (content != null)
|
if (content != null)
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Battle header from the interface mockup (mockups/card_battle): a compact
|
||||||
|
/// window in the upper left with the battle title, a phase badge that flips
|
||||||
|
/// between player and enemy turns, the round counter and the number of
|
||||||
|
/// enemies still standing. The node tree lives in
|
||||||
|
/// <c>scenes/battle_hud.tscn</c>; this class only fetches the nodes and
|
||||||
|
/// repaints the badge for the current phase.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BattleHud : ModWidget
|
||||||
|
{
|
||||||
|
private Label _phaseLabel;
|
||||||
|
private PanelContainer _phaseBadge;
|
||||||
|
private Label _roundValue;
|
||||||
|
private Label _enemiesValue;
|
||||||
|
|
||||||
|
protected override void Construct()
|
||||||
|
{
|
||||||
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
|
_phaseBadge = GetNode<PanelContainer>("Panel/Column/TitleRow/PhaseBadge");
|
||||||
|
_phaseLabel = GetNode<Label>("Panel/Column/TitleRow/PhaseBadge/PhaseLabel");
|
||||||
|
_roundValue = GetNode<Label>("Panel/Column/MetaRow/RoundValue");
|
||||||
|
_enemiesValue = GetNode<Label>("Panel/Column/MetaRow/EnemiesValue");
|
||||||
|
|
||||||
|
var panelStyle = UiKit.Frame(UiTokens.RadiusLg, UiTokens.Alpha(UiTokens.Surface0, 0.86f), UiTokens.Line);
|
||||||
|
panelStyle.ContentMarginLeft = 18;
|
||||||
|
panelStyle.ContentMarginRight = 18;
|
||||||
|
panelStyle.ContentMarginTop = 12;
|
||||||
|
panelStyle.ContentMarginBottom = 13;
|
||||||
|
panelStyle.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||||
|
panelStyle.ShadowSize = 18;
|
||||||
|
panelStyle.ShadowOffset = new Vector2(0f, 8f);
|
||||||
|
GetNode<PanelContainer>("Panel").AddThemeStyleboxOverride("panel", panelStyle);
|
||||||
|
|
||||||
|
var title = GetNode<Label>("Panel/Column/TitleRow/TitleLabel");
|
||||||
|
title.Text = Translate(title.Text);
|
||||||
|
title.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
|
||||||
|
foreach (var caption in new[] { "RoundCaption", "EnemiesCaption" })
|
||||||
|
{
|
||||||
|
var label = GetNode<Label>("Panel/Column/MetaRow/" + caption);
|
||||||
|
label.Text = Translate(label.Text);
|
||||||
|
label.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
_phaseLabel.Text = Translate(_phaseLabel.Text);
|
||||||
|
_roundValue.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||||
|
_enemiesValue.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Repaints the phase badge: blue for the player phase, red for the
|
||||||
|
/// enemy phase — the same colour language as the targeting highlights.
|
||||||
|
/// </summary>
|
||||||
|
public void SetTurn(bool player)
|
||||||
|
{
|
||||||
|
var accent = player ? UiTokens.Info : UiTokens.TypeEnemy;
|
||||||
|
var style = UiKit.Frame(UiTokens.RadiusXs, UiTokens.BadgeFill(accent), UiTokens.BadgeLine(accent));
|
||||||
|
style.ContentMarginLeft = 7;
|
||||||
|
style.ContentMarginRight = 7;
|
||||||
|
style.ContentMarginTop = 2;
|
||||||
|
style.ContentMarginBottom = 2;
|
||||||
|
|
||||||
|
_phaseBadge.AddThemeStyleboxOverride("panel", style);
|
||||||
|
_phaseLabel.AddThemeColorOverride("font_color", accent);
|
||||||
|
_phaseLabel.Text = Translate(player ? "battle/turn_player" : "battle/turn_enemy");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes the round number into the meta row.</summary>
|
||||||
|
public void SetRound(int round) => _roundValue.Text = round.ToString();
|
||||||
|
|
||||||
|
/// <summary>Writes the number of enemies still alive into the meta row.</summary>
|
||||||
|
public void SetEnemiesAlive(int count) => _enemiesValue.Text = count.ToString();
|
||||||
|
|
||||||
|
private static string Translate(string key)
|
||||||
|
=> TranslationServer.Translate(key).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Semantic category of a battle journal row, mirrored from the mockup's
|
||||||
|
/// colour scheme (mockups/card_battle). The panel maps each kind to a
|
||||||
|
/// left border accent, a background tint and a text colour, so callers
|
||||||
|
/// only decide what happened, never how it is painted.
|
||||||
|
/// </summary>
|
||||||
|
public enum BattleLogKind
|
||||||
|
{
|
||||||
|
/// <summary>Neutral note: enemy intent, hints, odds and ends.</summary>
|
||||||
|
Info,
|
||||||
|
/// <summary>Turn or battle boundary (gold, highlighted row).</summary>
|
||||||
|
Turn,
|
||||||
|
/// <summary>Health loss (red).</summary>
|
||||||
|
Damage,
|
||||||
|
/// <summary>Shield gain or absorption (blue).</summary>
|
||||||
|
Shield,
|
||||||
|
/// <summary>Support buff (green).</summary>
|
||||||
|
Buff,
|
||||||
|
/// <summary>Status effect such as stun (warm).</summary>
|
||||||
|
Status,
|
||||||
|
/// <summary>Character defeat (faint).</summary>
|
||||||
|
Dead,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Battle journal docked between the player and action panels — the
|
||||||
|
/// "Журнал боя" window from the interface mockup (mockups/card_battle).
|
||||||
|
/// The node tree lives in <c>scenes/battle_log_panel.tscn</c>; rows are
|
||||||
|
/// created at runtime because they are data, not layout, and are capped to
|
||||||
|
/// <see cref="MaxRows"/> so a long battle cannot grow unbounded.
|
||||||
|
/// <para/>
|
||||||
|
/// The panel is the single subscriber of <see cref="BattleLogHub"/>, so any
|
||||||
|
/// battle code can publish entries without knowing about the UI. The
|
||||||
|
/// subscription is dropped on <see cref="Destruct"/> to keep the static hub
|
||||||
|
/// clean between battles.
|
||||||
|
/// </summary>
|
||||||
|
public partial class BattleLogPanel : ModWidget
|
||||||
|
{
|
||||||
|
private const int MaxRows = 80;
|
||||||
|
|
||||||
|
private Label _title;
|
||||||
|
private Label _roundLabel;
|
||||||
|
private ScrollContainer _scroll;
|
||||||
|
private VBoxContainer _rows;
|
||||||
|
|
||||||
|
protected override void Construct()
|
||||||
|
{
|
||||||
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
|
_title = GetNode<Label>("Window/Column/Header/HeaderRow/TitleLabel");
|
||||||
|
_roundLabel = GetNode<Label>("Window/Column/Header/HeaderRow/RoundLabel");
|
||||||
|
_scroll = GetNode<ScrollContainer>("Window/Column/Scroll");
|
||||||
|
_rows = GetNode<VBoxContainer>("Window/Column/Scroll/Rows");
|
||||||
|
|
||||||
|
var windowStyle = UiKit.Frame(UiTokens.RadiusLg, UiTokens.Alpha(UiTokens.Surface0, 0.84f), UiTokens.Line);
|
||||||
|
windowStyle.ContentMarginLeft = 0;
|
||||||
|
windowStyle.ContentMarginRight = 0;
|
||||||
|
windowStyle.ContentMarginTop = 0;
|
||||||
|
windowStyle.ContentMarginBottom = 0;
|
||||||
|
windowStyle.ShadowColor = new Color(0f, 0f, 0f, 0.6f);
|
||||||
|
windowStyle.ShadowSize = 18;
|
||||||
|
windowStyle.ShadowOffset = new Vector2(0f, 8f);
|
||||||
|
GetNode<PanelContainer>("Window").AddThemeStyleboxOverride("panel", windowStyle);
|
||||||
|
|
||||||
|
_title.Text = Translate(_title.Text);
|
||||||
|
_title.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
_roundLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||||
|
|
||||||
|
BattleLogHub.Entry += Append;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Destruct()
|
||||||
|
{
|
||||||
|
BattleLogHub.Entry -= Append;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Updates the "раунд N" caption in the header.</summary>
|
||||||
|
public void SetRound(int round)
|
||||||
|
=> _roundLabel.Text = string.Format(Translate("battle/log_round"), round);
|
||||||
|
|
||||||
|
/// <summary>Drops every row; called when the battle is cleared or restarted.</summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
foreach (var child in _rows.GetChildren())
|
||||||
|
{
|
||||||
|
_rows.RemoveChild(child);
|
||||||
|
child.QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Appends one colour-coded row and trims the oldest entries past the cap.
|
||||||
|
/// The scroll is pushed to the bottom so the newest event stays visible.
|
||||||
|
/// </summary>
|
||||||
|
public void Append(string text, BattleLogKind kind)
|
||||||
|
{
|
||||||
|
var style = UiKit.Frame(0, BackgroundOf(kind), new Color(0f, 0f, 0f, 0f));
|
||||||
|
style.BorderWidthLeft = 2;
|
||||||
|
style.BorderColor = BorderOf(kind);
|
||||||
|
style.ContentMarginLeft = 12;
|
||||||
|
style.ContentMarginRight = 12;
|
||||||
|
style.ContentMarginTop = 7;
|
||||||
|
style.ContentMarginBottom = 7;
|
||||||
|
|
||||||
|
var row = new PanelContainer { MouseFilter = MouseFilterEnum.Ignore };
|
||||||
|
row.AddThemeStyleboxOverride("panel", style);
|
||||||
|
|
||||||
|
var label = UiKit.Label(text, UiTokens.FontBodySmall, TextOf(kind));
|
||||||
|
label.AutowrapMode = TextServer.AutowrapMode.WordSmart;
|
||||||
|
row.AddChild(label);
|
||||||
|
|
||||||
|
_rows.AddChild(row);
|
||||||
|
|
||||||
|
while (_rows.GetChildCount() > MaxRows)
|
||||||
|
{
|
||||||
|
var first = _rows.GetChild(0);
|
||||||
|
_rows.RemoveChild(first);
|
||||||
|
first.QueueFree();
|
||||||
|
}
|
||||||
|
|
||||||
|
_scroll.ScrollVertical = (int)_scroll.GetVScrollBar().MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color BackgroundOf(BattleLogKind kind)
|
||||||
|
=> kind == BattleLogKind.Turn ? UiTokens.AccentSoft : new Color(0f, 0f, 0f, 0f);
|
||||||
|
|
||||||
|
private static Color BorderOf(BattleLogKind kind)
|
||||||
|
{
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case BattleLogKind.Turn: return UiTokens.Accent;
|
||||||
|
case BattleLogKind.Damage: return UiTokens.Danger;
|
||||||
|
case BattleLogKind.Shield: return UiTokens.TypeResource;
|
||||||
|
case BattleLogKind.Buff: return UiTokens.TypePassive;
|
||||||
|
case BattleLogKind.Status: return UiTokens.Cooldown;
|
||||||
|
case BattleLogKind.Dead: return UiTokens.LineFaint;
|
||||||
|
default: return UiTokens.Line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color TextOf(BattleLogKind kind)
|
||||||
|
{
|
||||||
|
switch (kind)
|
||||||
|
{
|
||||||
|
case BattleLogKind.Turn: return UiTokens.AccentBright;
|
||||||
|
case BattleLogKind.Damage: return UiTokens.DangerText;
|
||||||
|
case BattleLogKind.Shield: return UiTokens.Info;
|
||||||
|
case BattleLogKind.Buff: return UiTokens.SuccessText;
|
||||||
|
case BattleLogKind.Status: return UiTokens.Cooldown;
|
||||||
|
case BattleLogKind.Dead: return UiTokens.Faint;
|
||||||
|
default: return UiTokens.Muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Translate(string key)
|
||||||
|
=> TranslationServer.Translate(key).ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.Theming;
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Cthangover.CardBattle.UI
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Root of the card battle interface rebuilt from the mockup
|
||||||
|
/// (mockups/card_battle). The whole layout lives in
|
||||||
|
/// <c>scenes/card_battle_screen.tscn</c> and is composed of the mockup's
|
||||||
|
/// parts: the HUD window, the enemy and party card panels, the "Действия"
|
||||||
|
/// window, the battle journal, the turn banner and the end-turn button.
|
||||||
|
/// This class is the single assembly point: it constructs every child widget
|
||||||
|
/// once (mod widgets are not auto-constructed by Godot) and exposes them to
|
||||||
|
/// the battle core and the input controller, so neither has to walk the
|
||||||
|
/// scene tree by hand.
|
||||||
|
/// </summary>
|
||||||
|
public partial class CardBattleScreen : ModWidget
|
||||||
|
{
|
||||||
|
/// <summary>Top-left window with the phase badge, round and enemy count.</summary>
|
||||||
|
public BattleHud Hud { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Panel that manages the enemy card row(s); aligned right.</summary>
|
||||||
|
public BattleCardPanel EnemyPanel { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Panel that manages the party card row; aligned left.</summary>
|
||||||
|
public BattleCardPanel PlayerPanel { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Lower-right "Действия" window with the action card grid.</summary>
|
||||||
|
public BattleActionsPanel ActionsPanel { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Battle journal window that renders <see cref="BattleLogHub"/> entries.</summary>
|
||||||
|
public BattleLogPanel LogPanel { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Centre-screen turn banner.</summary>
|
||||||
|
public BattleBanner Banner { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>End-turn button in the top-right corner, next to the battle speed bar.</summary>
|
||||||
|
public EndTurnButton EndTurnButton { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Global input handler that drives selection and drag-and-drop.</summary>
|
||||||
|
public CardController Controller { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Canvas layer above the whole battle UI (layer 100). The dragged action
|
||||||
|
/// card is temporarily reparented here, so it always renders over enemy
|
||||||
|
/// and party cards regardless of their Z-index inside the panels.
|
||||||
|
/// </summary>
|
||||||
|
public CanvasLayer DragLayer { get; private set; }
|
||||||
|
|
||||||
|
protected override void Construct()
|
||||||
|
{
|
||||||
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
|
Hud = GetNode<BattleHud>("Hud");
|
||||||
|
Hud.EnsureConstructed();
|
||||||
|
|
||||||
|
EnemyPanel = GetNode<BattleCardPanel>("EnemyPanel");
|
||||||
|
EnemyPanel.EnsureConstructed();
|
||||||
|
EnemyPanel.AlignType = "Right";
|
||||||
|
|
||||||
|
PlayerPanel = GetNode<BattleCardPanel>("PlayerPanel");
|
||||||
|
PlayerPanel.EnsureConstructed();
|
||||||
|
|
||||||
|
ActionsPanel = GetNode<BattleActionsPanel>("ActionsPanel");
|
||||||
|
ActionsPanel.EnsureConstructed();
|
||||||
|
|
||||||
|
LogPanel = GetNode<BattleLogPanel>("LogPanel");
|
||||||
|
LogPanel.EnsureConstructed();
|
||||||
|
|
||||||
|
Banner = GetNode<BattleBanner>("Banner");
|
||||||
|
Banner.EnsureConstructed();
|
||||||
|
|
||||||
|
EndTurnButton = GetNode<EndTurnButton>("EndTurnButton");
|
||||||
|
EndTurnButton.EnsureConstructed();
|
||||||
|
|
||||||
|
Controller = GetNode<CardController>("CardController");
|
||||||
|
|
||||||
|
DragLayer = GetNode<CanvasLayer>("DragLayer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Releases every widget when the battle scene is cleared: cards, action
|
||||||
|
/// deck, journal rows, banner and the end-turn button. Called by the
|
||||||
|
/// battle core on <c>OnBattleCleared</c>.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
EnemyPanel.ClearAll();
|
||||||
|
PlayerPanel.ClearAll();
|
||||||
|
ActionsPanel.ClearBody();
|
||||||
|
ActionsPanel.ClearSubject();
|
||||||
|
LogPanel.Clear();
|
||||||
|
Banner.Clear();
|
||||||
|
EndTurnButton.SetVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+262
-308
@@ -1,182 +1,162 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Cthangover.Core.Cards;
|
using Cthangover.Core.Cards;
|
||||||
using Cthangover.Core.Audio;
|
using Cthangover.Core.Audio;
|
||||||
using Cthangover.Core.Scenes;
|
using Cthangover.Core.Scenes;
|
||||||
using Cthangover.Core.Utils;
|
using Cthangover.Core.Utils;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
using Cthangover.Core.UI;
|
using Cthangover.Core.UI;
|
||||||
|
using Cthangover.Core.UI.CharacterPanel;
|
||||||
using Cthangover.Core.UI.Theming;
|
using Cthangover.Core.UI.Theming;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
namespace Cthangover.CardBattle.UI
|
namespace Cthangover.CardBattle.UI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Central input handler for the card battle UI. Processes mouse clicks and drags to implement
|
/// Central input handler for the card battle UI rebuilt from the mockup
|
||||||
/// the full player interaction flow: click a character card to show its action cards in the
|
/// (mockups/card_battle). Processes mouse clicks and drags: click a party
|
||||||
/// action panel, drag an action card onto a valid target (enemy for attacks, ally for support,
|
/// card to open its action deck in the "Действия" window, drag an action
|
||||||
/// self for self-buffs), and execute the action on drop. Valid targets are determined by
|
/// card onto a valid target (enemy for attacks, ally for support, self for
|
||||||
/// <see cref="CardActionStrategyFactory.Get"/> dispatching the appropriate <see cref="ICardActionStrategy"/>.
|
/// self-buffs) and execute it on drop. Valid targets are decided by
|
||||||
/// Fires <see cref="OnActionExecuted"/> after each action, which <see cref="CardBattleCore"/>
|
/// <see cref="Player.CardActionStrategyFactory"/>; after every executed
|
||||||
/// uses to detect automatic turn end when all points are exhausted.
|
/// action the controller fires <see cref="OnActionExecuted"/> so the core can
|
||||||
|
/// end the turn when all action points are spent.
|
||||||
|
/// <para/>
|
||||||
|
/// Actions are instantiated from <c>scenes/action_card.tscn</c> and parented
|
||||||
|
/// into the grid of <see cref="BattleActionsPanel"/>, which owns the empty
|
||||||
|
/// state, the point pips and the effect preview well; the controller only
|
||||||
|
/// feeds it. Hit-testing uses the card root rects exposed through
|
||||||
|
/// <see cref="ICard.GetControlNode"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CardController : InputHandlerNode
|
public partial class CardController : InputHandlerNode
|
||||||
{
|
{
|
||||||
private Control actionPanel;
|
private CardBattleScreen _screen;
|
||||||
private BattleCardPanel[] panels;
|
private BattleCardPanel[] _panels;
|
||||||
|
|
||||||
private List<CharacterCardNode> characterCards = new();
|
private readonly List<CharacterCardNode> _characterCards = new();
|
||||||
private List<ActionCardNode> actionCards = new();
|
private readonly List<ActionCardNode> _actionCards = new();
|
||||||
|
|
||||||
private CharacterCardNode selectedCharacter;
|
private CharacterCardNode _selectedCharacter;
|
||||||
private CharacterCardNode targetCharacter;
|
private CharacterCardNode _targetCharacter;
|
||||||
private ActionCardNode currentActionCharacter;
|
private ActionCardNode _currentActionCard;
|
||||||
|
|
||||||
private bool isDragging;
|
private bool _isDragging;
|
||||||
private bool dragValid;
|
private bool _dragValid;
|
||||||
private float dragThreshold = 15f;
|
private const float DragThreshold = 15f;
|
||||||
private Vector2 pressPosition;
|
private Vector2 _pressPosition;
|
||||||
private Node _originalActionCardParent;
|
private Node _originalActionCardParent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Locates the action panel and both battle card panels (<see cref="BattleCardPanel"/>) in the
|
/// Uniform scale factor applied to all action cards created by this
|
||||||
/// scene tree relative to this node. Called once by <see cref="CardBattleCore.Init"/>
|
/// controller. Set by the battle core from the viewport fitting
|
||||||
/// after the controller is added to the scene.
|
/// calculation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FindPanels()
|
public float CardScale { get; set; } = 1f;
|
||||||
{
|
|
||||||
actionPanel = GetNodeOrNull<Control>("../ActionPanel");
|
|
||||||
panels = new[]
|
|
||||||
{
|
|
||||||
GetNodeOrNull<BattleCardPanel>("../PlayerPanel"),
|
|
||||||
GetNodeOrNull<BattleCardPanel>("../EnemyPanel"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnInput(InputEvent @event)
|
|
||||||
{
|
|
||||||
if (@event is InputEventMouseButton mb1 && mb1.Pressed && mb1.ButtonIndex == MouseButton.Left)
|
|
||||||
GameLogger.Log("CARD_CTRL", "_Input received mouse press", LogLevel.Debug);
|
|
||||||
|
|
||||||
if (Cthangover.Core.Battle.BattleSceneContext.Instance != null &&
|
|
||||||
(Cthangover.Core.Battle.BattleSceneContext.Instance.IsWait ||
|
|
||||||
Cthangover.Core.Battle.BattleSceneContext.Instance.IsDestroyed))
|
|
||||||
{
|
|
||||||
if (@event is InputEventMouseButton mb2 && !mb2.Pressed)
|
|
||||||
GameLogger.Log("CARD_CTRL",
|
|
||||||
$"Input blocked: IsWait={Cthangover.Core.Battle.BattleSceneContext.Instance.IsWait} IsDestroyed={Cthangover.Core.Battle.BattleSceneContext.Instance.IsDestroyed}",
|
|
||||||
LogLevel.Debug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@event is InputEventMouseButton mouseButton)
|
|
||||||
{
|
|
||||||
if (mouseButton.ButtonIndex == MouseButton.Left)
|
|
||||||
{
|
|
||||||
if (mouseButton.Pressed)
|
|
||||||
{
|
|
||||||
pressPosition = mouseButton.GlobalPosition;
|
|
||||||
isDragging = false;
|
|
||||||
dragValid = false;
|
|
||||||
OnBeginDrag(mouseButton.GlobalPosition);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (isDragging && dragValid)
|
|
||||||
OnEndDrag(mouseButton.GlobalPosition);
|
|
||||||
else
|
|
||||||
OnPointerClick(mouseButton.GlobalPosition);
|
|
||||||
isDragging = false;
|
|
||||||
dragValid = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (@event is InputEventMouseMotion mouseMotion)
|
|
||||||
{
|
|
||||||
if (mouseMotion.ButtonMask.HasFlag(MouseButtonMask.Left))
|
|
||||||
{
|
|
||||||
if (dragValid && !isDragging && (mouseMotion.GlobalPosition - pressPosition).Length() > dragThreshold)
|
|
||||||
isDragging = true;
|
|
||||||
if (isDragging && dragValid)
|
|
||||||
OnDrag(mouseMotion.GlobalPosition);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float cardScale = 1f;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Uniform scale factor applied to all action cards created by this controller.
|
|
||||||
/// Set by <see cref="CardBattleCore.Start"/> based on viewport fitting calculations.
|
|
||||||
/// </summary>
|
|
||||||
public float CardScale
|
|
||||||
{
|
|
||||||
get => cardScale;
|
|
||||||
set => cardScale = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fired after a successful drag-and-drop action execution. Subscribed by
|
/// Fired after a successful drag-and-drop action execution. Subscribed by
|
||||||
/// <see cref="CardBattleCore.CheckPlayerTurnEnd"/> to auto-end the player's turn
|
/// the battle core to auto-end the player's turn when all points are spent.
|
||||||
/// when all action points are spent.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event System.Action OnActionExecuted;
|
public event System.Action OnActionExecuted;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears previous action cards and creates new <see cref="ActionCardNode"/> instances
|
/// Binds the controller to the assembled battle screen: keeps the panels
|
||||||
/// for every action in <paramref name="character"/>'s deck. Action cards fade in and are
|
/// for hit-testing and redraws and the actions panel for the deck grid and
|
||||||
/// laid out horizontally in the action panel. Called when the player clicks a character card.
|
/// the hover preview. Called once by the battle core right after the
|
||||||
|
/// screen is added to the battle scene.
|
||||||
|
/// </summary>
|
||||||
|
public void Setup(CardBattleScreen screen)
|
||||||
|
{
|
||||||
|
_screen = screen;
|
||||||
|
_panels = new[] { screen.PlayerPanel, screen.EnemyPanel };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnInput(InputEvent @event)
|
||||||
|
{
|
||||||
|
if (Cthangover.Core.Battle.BattleSceneContext.Instance != null &&
|
||||||
|
(Cthangover.Core.Battle.BattleSceneContext.Instance.IsWait ||
|
||||||
|
Cthangover.Core.Battle.BattleSceneContext.Instance.IsDestroyed))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event is InputEventMouseButton mouseButton && mouseButton.ButtonIndex == MouseButton.Left)
|
||||||
|
{
|
||||||
|
if (mouseButton.Pressed)
|
||||||
|
{
|
||||||
|
_pressPosition = mouseButton.GlobalPosition;
|
||||||
|
_isDragging = false;
|
||||||
|
_dragValid = false;
|
||||||
|
OnBeginDrag(mouseButton.GlobalPosition);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (_isDragging && _dragValid)
|
||||||
|
OnEndDrag(mouseButton.GlobalPosition);
|
||||||
|
else
|
||||||
|
OnPointerClick(mouseButton.GlobalPosition);
|
||||||
|
_isDragging = false;
|
||||||
|
_dragValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@event is InputEventMouseMotion mouseMotion &&
|
||||||
|
mouseMotion.ButtonMask.HasFlag(MouseButtonMask.Left) &&
|
||||||
|
_dragValid)
|
||||||
|
{
|
||||||
|
if (!_isDragging && (mouseMotion.GlobalPosition - _pressPosition).Length() > DragThreshold)
|
||||||
|
_isDragging = true;
|
||||||
|
if (_isDragging)
|
||||||
|
OnDrag(mouseMotion.GlobalPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears previous action cards and fades in a fresh deck for
|
||||||
|
/// <paramref name="character"/>: one card per battle-usable action, dimmed
|
||||||
|
/// when the character cannot pay its cost. Called when a party card is
|
||||||
|
/// clicked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ShowActions(CharacterCardNode character)
|
public void ShowActions(CharacterCardNode character)
|
||||||
{
|
{
|
||||||
if (character?.Card?.Actions == null)
|
if (character?.Card?.Actions == null)
|
||||||
{
|
{
|
||||||
GameLogger.Log("CARD_CTRL",
|
GameLogger.Log("CARD_CTRL",
|
||||||
$"ShowActions: NULL actions for character {(character?.Card?.Name ?? "null")}",
|
"ShowActions: NULL actions for character " + (character?.Card?.Name ?? "null"),
|
||||||
LogLevel.Error);
|
LogLevel.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
GameLogger.Log("CARD_CTRL",
|
|
||||||
$"ShowActions: {character.Card.Name} ({character.Card.Actions.Count} actions)",
|
|
||||||
LogLevel.Debug);
|
|
||||||
|
|
||||||
DiscardActionCharacters();
|
DiscardActionCharacters();
|
||||||
|
|
||||||
var originPos = character.GlobalPosition * actionPanel.GetGlobalTransformWithCanvas().AffineInverse();
|
var points = character.Card.Attributes.Point.Value;
|
||||||
var count = character.Card.Actions.Count;
|
foreach (var action in character.Card.Actions)
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
{
|
||||||
var action = character.Card.Actions[i];
|
|
||||||
if (!action.Type.UseInBattle())
|
if (!action.Type.UseInBattle())
|
||||||
continue;
|
continue;
|
||||||
var cardNode = CreateAction(action, cardScale);
|
|
||||||
cardNode.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
|
||||||
cardNode.Position = originPos - cardNode.Size / 2;
|
|
||||||
actionCards.Add(cardNode);
|
|
||||||
|
|
||||||
var targetPos = new Vector2(cardNode.Size.X * i, 0);
|
var cardNode = CreateAction(action, CardScale);
|
||||||
|
cardNode.SetDisabled(points < ActionCharacterUi.CostOf(action));
|
||||||
|
cardNode.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
cardNode.Hovered += OnActionCardHovered;
|
||||||
|
cardNode.Unhovered += OnActionCardUnhovered;
|
||||||
|
_actionCards.Add(cardNode);
|
||||||
|
|
||||||
var tween = CreateTween();
|
var tween = CreateTween();
|
||||||
tween.SetParallel(true);
|
|
||||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||||
tween.TweenProperty(cardNode, "modulate:a", 1f, 0.5f);
|
tween.TweenProperty(cardNode, "modulate:a", 1f, 0.25f);
|
||||||
tween.TweenProperty(cardNode, "position", targetPos, 0.5f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_screen.ActionsPanel.RefreshEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Fades the current deck out and frees it.</summary>
|
||||||
/// Animates existing action cards fading out and dropping down, then frees them.
|
|
||||||
/// Called before showing new action cards for a different character.
|
|
||||||
/// </summary>
|
|
||||||
public void DiscardActionCharacters()
|
public void DiscardActionCharacters()
|
||||||
{
|
{
|
||||||
if (actionCards.Count == 0)
|
if (_actionCards.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var oldCards = new List<ActionCardNode>(actionCards);
|
var oldCards = new List<ActionCardNode>(_actionCards);
|
||||||
actionCards.Clear();
|
_actionCards.Clear();
|
||||||
|
|
||||||
foreach (var card in oldCards)
|
foreach (var card in oldCards)
|
||||||
{
|
{
|
||||||
@@ -184,257 +164,232 @@ namespace Cthangover.CardBattle.UI
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
card.MouseFilter = Control.MouseFilterEnum.Ignore;
|
card.MouseFilter = Control.MouseFilterEnum.Ignore;
|
||||||
|
card.Hovered -= OnActionCardHovered;
|
||||||
|
card.Unhovered -= OnActionCardUnhovered;
|
||||||
|
|
||||||
var tween = CreateTween();
|
var tween = CreateTween();
|
||||||
tween.SetParallel(true);
|
|
||||||
tween.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Cubic);
|
tween.SetEase(Tween.EaseType.In).SetTrans(Tween.TransitionType.Cubic);
|
||||||
tween.TweenProperty(card, "modulate:a", 0f, 0.15f);
|
tween.TweenProperty(card, "modulate:a", 0f, 0.15f);
|
||||||
tween.TweenProperty(card, "position:y", card.Position.Y + 40f, 0.15f);
|
tween.TweenCallback(Callable.From(card.QueueFree));
|
||||||
tween.Chain().TweenCallback(Callable.From(card.QueueFree));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_screen.ActionsPanel.RefreshEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deselects the current action card, source character, and target character, clearing
|
/// Drops the source and target highlights, optionally resets the actions
|
||||||
/// their visual highlights and optionally discarding action cards. When
|
/// window back to its empty state and redraws the panels so the Z-order
|
||||||
/// <paramref name="clearSource"/> is <c>true</c>, the action panel is also cleared. Triggers
|
/// returns to normal. Called after each executed action, on empty clicks
|
||||||
/// a redraw of all battle panels to restore normal Z-ordering.
|
/// and when a drag is cancelled.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void ClearSelections(bool clearSource = true)
|
public void ClearSelections(bool clearSource = true)
|
||||||
{
|
{
|
||||||
if (currentActionCharacter != null)
|
if (_currentActionCard != null)
|
||||||
{
|
{
|
||||||
currentActionCharacter.Unselect();
|
_currentActionCard.Unselect();
|
||||||
currentActionCharacter = null;
|
_currentActionCard = null;
|
||||||
}
|
|
||||||
if (selectedCharacter != null && clearSource)
|
|
||||||
{
|
|
||||||
selectedCharacter.Unselect();
|
|
||||||
selectedCharacter = null;
|
|
||||||
DiscardActionCharacters();
|
|
||||||
}
|
|
||||||
if (targetCharacter != null)
|
|
||||||
{
|
|
||||||
targetCharacter.Unselect();
|
|
||||||
targetCharacter = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panels != null)
|
if (_selectedCharacter != null && clearSource)
|
||||||
{
|
{
|
||||||
foreach (var panel in panels)
|
_selectedCharacter.Unselect();
|
||||||
|
_selectedCharacter = null;
|
||||||
|
DiscardActionCharacters();
|
||||||
|
_screen.ActionsPanel.ClearSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
ClearTargetMarkers();
|
||||||
|
|
||||||
|
if (_panels != null)
|
||||||
|
{
|
||||||
|
foreach (var panel in _panels)
|
||||||
panel?.Redraw();
|
panel?.Redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immediately frees all action card nodes without animation. Called at battle cleanup
|
/// Removes the targeting frame from the current target and forgets it.
|
||||||
/// by <see cref="CardBattleCore.OnBattleCleared"/>.
|
/// Every path that changes or abandons a target must go through here:
|
||||||
|
/// dragging over the gap between cards first nulls the target, and
|
||||||
|
/// without the explicit <see cref="CharacterCardNode.Unselect"/> the
|
||||||
|
/// discarded card would keep its red or grey marker forever.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
private void ClearTargetMarkers()
|
||||||
|
{
|
||||||
|
if (_targetCharacter != null && GodotObject.IsInstanceValid(_targetCharacter))
|
||||||
|
_targetCharacter.Unselect();
|
||||||
|
_targetCharacter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Immediately frees all action card nodes without animation (battle cleanup).</summary>
|
||||||
public void ClearActions()
|
public void ClearActions()
|
||||||
{
|
{
|
||||||
if (actionCards.Count > 0)
|
foreach (var card in _actionCards)
|
||||||
{
|
card.QueueFree();
|
||||||
foreach (var card in actionCards)
|
_actionCards.Clear();
|
||||||
card.QueueFree();
|
|
||||||
actionCards.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Re-positions all action cards in a horizontal row within the action panel.
|
/// Instantiates an <see cref="ActionCardNode"/> inside the actions grid
|
||||||
/// When <paramref name="animate"/> is <c>true</c>, cards tween smoothly to their targets.
|
/// and fills it from the action definition.
|
||||||
/// </summary>
|
|
||||||
public void Redraw(bool animate = true)
|
|
||||||
{
|
|
||||||
var size = actionCards.Count;
|
|
||||||
for (int i = 0; i < size; i++)
|
|
||||||
SetActionCharacterPosition(actionCards[i], i, size, animate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetActionCharacterPosition(ActionCardNode card, int index, int count, bool animate)
|
|
||||||
{
|
|
||||||
var rect = card.GetControlNode();
|
|
||||||
var targetPos = new Vector2(rect.Size.X * index, 0);
|
|
||||||
|
|
||||||
if (animate)
|
|
||||||
{
|
|
||||||
var tween = CreateTween();
|
|
||||||
tween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
|
||||||
tween.TweenProperty(rect, "position", targetPos, 0.25f);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
rect.Position = targetPos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Instantiates an <see cref="ActionCardNode"/>, adds it to the action panel, applies
|
|
||||||
/// the given <paramref name="cardScale"/>, and initializes it with <paramref name="actionCard"/> data.
|
|
||||||
/// Called by <see cref="ShowActions"/> for each action in a character's deck.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ActionCardNode CreateAction(ActionCharacter actionCard, float cardScale)
|
public ActionCardNode CreateAction(ActionCharacter actionCard, float cardScale)
|
||||||
{
|
{
|
||||||
var card = (ActionCardNode)TscnScenes.LoadAndBuild("scenes/action_card_node.tscn");
|
var card = (ActionCardNode)TscnScenes.LoadAndBuild("scenes/action_card.tscn");
|
||||||
card.EnsureConstructed();
|
card.EnsureConstructed();
|
||||||
actionPanel.AddChild(card);
|
_screen.ActionsPanel.Body.AddChild(card);
|
||||||
card.Scale = new Vector2(cardScale, cardScale);
|
card.Scale = new Vector2(cardScale, cardScale);
|
||||||
card.Init(actionCard);
|
card.Init(actionCard);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the currently selected action against the selected target using the
|
/// Resolves the currently selected action against the selected target
|
||||||
/// appropriate <see cref="ICardActionStrategy"/>. Called by <see cref="OnEndDrag"/>
|
/// through the matching <see cref="Player.ICardActionStrategy"/>.
|
||||||
/// when a valid drop is confirmed.
|
/// Called by <see cref="OnEndDrag"/> when a valid drop is confirmed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void DoActionToTarget()
|
public void DoActionToTarget()
|
||||||
{
|
{
|
||||||
var strategy = Cthangover.CardBattle.Player.CardActionStrategyFactory.Get(
|
var strategy = Player.CardActionStrategyFactory.Get(
|
||||||
currentActionCharacter.Card, selectedCharacter, targetCharacter);
|
_currentActionCard.Card, _selectedCharacter, _targetCharacter);
|
||||||
strategy?.Execute(selectedCharacter, targetCharacter, currentActionCharacter.Card);
|
strategy?.Execute(_selectedCharacter, _targetCharacter, _currentActionCard.Card);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnActionCardHovered(ActionCardNode card)
|
||||||
|
=> _screen.ActionsPanel.SetDescription(card.DescribeFrom(_selectedCharacter?.Card));
|
||||||
|
|
||||||
|
private void OnActionCardUnhovered(ActionCardNode card)
|
||||||
|
=> _screen.ActionsPanel.SetDescriptionDefault();
|
||||||
|
|
||||||
private void OnPointerClick(Vector2 screenPosition)
|
private void OnPointerClick(Vector2 screenPosition)
|
||||||
{
|
{
|
||||||
characterCards = GetAllCharacters();
|
var source = TryFindCardByPosition(screenPosition, CollectCharacters());
|
||||||
var source = TryFindCardByPosition(screenPosition, characterCards);
|
|
||||||
|
|
||||||
if (source != null)
|
if (source == null || !source.IsPlayer || source.IsDead)
|
||||||
GameLogger.Log("CARD_CTRL",
|
|
||||||
$"Click on card: {source.Card.Name} isPlayer={source.IsPlayer} actionsCount={source.Card.Actions?.Count ?? 0}",
|
|
||||||
LogLevel.Debug);
|
|
||||||
else
|
|
||||||
GameLogger.Log("CARD_CTRL", "Click on empty space", LogLevel.Debug);
|
|
||||||
|
|
||||||
if (source != selectedCharacter && selectedCharacter != null)
|
|
||||||
selectedCharacter.Unselect();
|
|
||||||
|
|
||||||
if (source != null && source.IsPlayer)
|
|
||||||
{
|
|
||||||
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
|
||||||
audioService?.PlaySound("battle/card_show_actions", Cthangover.Core.Audio.SoundType.CardEffect);
|
|
||||||
selectedCharacter = source;
|
|
||||||
ShowActions(selectedCharacter);
|
|
||||||
selectedCharacter.Select();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
ClearSelections();
|
ClearSelections();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source != _selectedCharacter && _selectedCharacter != null)
|
||||||
|
_selectedCharacter.Unselect();
|
||||||
|
|
||||||
|
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
||||||
|
audioService?.PlaySound("battle/card_show_actions", SoundType.CardEffect);
|
||||||
|
|
||||||
|
_selectedCharacter = source;
|
||||||
|
_screen.ActionsPanel.SetSubject(TranslationServer.Translate(source.Card.Name).ToString());
|
||||||
|
_screen.ActionsPanel.SetPoints(source.Card.Attributes.Point.Value, source.Card.Attributes.Point.BaseValue);
|
||||||
|
ShowActions(_selectedCharacter);
|
||||||
|
_selectedCharacter.Select();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnBeginDrag(Vector2 screenPosition)
|
private void OnBeginDrag(Vector2 screenPosition)
|
||||||
{
|
{
|
||||||
var source = TryFindCardByPosition(screenPosition, actionCards);
|
var source = TryFindCardByPosition(screenPosition, _actionCards);
|
||||||
if (currentActionCharacter != null && source != currentActionCharacter)
|
|
||||||
currentActionCharacter.Unselect();
|
|
||||||
|
|
||||||
if (source != null)
|
if (source == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (source.IsDisabled)
|
||||||
{
|
{
|
||||||
dragValid = true;
|
BattleLogHub.Log(
|
||||||
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
string.Format(Translate("battle/log_not_enough_points"), ActionCharacterUi.Translate(source.Card.Name)),
|
||||||
audioService?.PlaySound("battle/card_pick", 4, Cthangover.Core.Audio.SoundType.CardEffect);
|
BattleLogKind.Status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GameLogger.Log("CARD_CTRL",
|
if (_currentActionCard != null && source != _currentActionCard)
|
||||||
$"Begin drag: {source.Card.Name}",
|
_currentActionCard.Unselect();
|
||||||
LogLevel.Debug);
|
|
||||||
|
|
||||||
currentActionCharacter = source;
|
_dragValid = true;
|
||||||
var control = currentActionCharacter.GetControlNode();
|
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
||||||
_originalActionCardParent = control.GetParent();
|
audioService?.PlaySound("battle/card_pick", 4, SoundType.CardEffect);
|
||||||
var globalPos = control.GlobalPosition;
|
|
||||||
control.Reparent(GetTree().Root);
|
|
||||||
control.GlobalPosition = globalPos;
|
|
||||||
control.ZIndex = 4096;
|
|
||||||
var pickTween = CreateTween();
|
|
||||||
pickTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
|
|
||||||
pickTween.TweenProperty(control, "scale", new Vector2(1.1f, 1.1f), 0.1f);
|
|
||||||
|
|
||||||
foreach (var card in actionCards)
|
_currentActionCard = source;
|
||||||
{
|
var control = _currentActionCard.GetControlNode();
|
||||||
if (card != currentActionCharacter && GodotObject.IsInstanceValid(card))
|
_originalActionCardParent = control.GetParent();
|
||||||
card.MouseFilter = Control.MouseFilterEnum.Ignore;
|
var globalPos = control.GlobalPosition;
|
||||||
}
|
// Отдельный CanvasLayer поверх всего боя: иначе карта рисуется
|
||||||
|
// под ветками панелей врагов и союзников.
|
||||||
|
control.Reparent(_screen.DragLayer);
|
||||||
|
control.GlobalPosition = globalPos;
|
||||||
|
|
||||||
|
var pickTween = CreateTween();
|
||||||
|
pickTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Back);
|
||||||
|
pickTween.TweenProperty(control, "scale", new Vector2(1.1f, 1.1f), 0.1f);
|
||||||
|
|
||||||
|
foreach (var card in _actionCards)
|
||||||
|
{
|
||||||
|
if (card != _currentActionCard && GodotObject.IsInstanceValid(card))
|
||||||
|
card.MouseFilter = Control.MouseFilterEnum.Ignore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDrag(Vector2 screenPosition)
|
private void OnDrag(Vector2 screenPosition)
|
||||||
{
|
{
|
||||||
if (currentActionCharacter == null)
|
if (_currentActionCard == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var cardControl = currentActionCharacter.GetControlNode();
|
var cardControl = _currentActionCard.GetControlNode();
|
||||||
cardControl.GlobalPosition = screenPosition - cardControl.Size / 2;
|
cardControl.GlobalPosition = screenPosition - cardControl.Size / 2;
|
||||||
|
|
||||||
var target = TryFindCardByRect(cardControl, characterCards);
|
var target = TryFindCardByRect(cardControl, _characterCards);
|
||||||
|
if (target == _targetCharacter)
|
||||||
|
return;
|
||||||
|
|
||||||
if (target != targetCharacter && targetCharacter != null)
|
ClearTargetMarkers();
|
||||||
targetCharacter.Unselect();
|
|
||||||
|
|
||||||
if (target != null)
|
if (target == null)
|
||||||
{
|
{
|
||||||
targetCharacter = target;
|
_currentActionCard.Unselect();
|
||||||
var strategy = Cthangover.CardBattle.Player.CardActionStrategyFactory.Get(
|
return;
|
||||||
currentActionCharacter.Card, selectedCharacter, target);
|
}
|
||||||
|
|
||||||
var valid = strategy != null && strategy.Check(currentActionCharacter.Card, selectedCharacter, target);
|
_targetCharacter = target;
|
||||||
|
var strategy = Player.CardActionStrategyFactory.Get(
|
||||||
|
_currentActionCard.Card, _selectedCharacter, target);
|
||||||
|
|
||||||
GameLogger.Log("CARD_CTRL",
|
if (strategy != null && strategy.Check(_currentActionCard.Card, _selectedCharacter, target))
|
||||||
$"Drag over: {target.Card.Name} valid={valid}",
|
{
|
||||||
LogLevel.Debug);
|
strategy.HighlightTarget(_currentActionCard, _targetCharacter);
|
||||||
|
|
||||||
if (valid)
|
|
||||||
{
|
|
||||||
strategy.HighlightTarget(currentActionCharacter, targetCharacter);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
targetCharacter.Invalid();
|
|
||||||
currentActionCharacter.Invalid();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
targetCharacter = null;
|
_targetCharacter.Invalid();
|
||||||
currentActionCharacter.Unselect();
|
_currentActionCard.Invalid();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEndDrag(Vector2 screenPosition)
|
private void OnEndDrag(Vector2 screenPosition)
|
||||||
{
|
{
|
||||||
foreach (var card in actionCards)
|
foreach (var card in _actionCards)
|
||||||
{
|
{
|
||||||
if (card != null && GodotObject.IsInstanceValid(card))
|
if (card != null && GodotObject.IsInstanceValid(card))
|
||||||
card.MouseFilter = Control.MouseFilterEnum.Stop;
|
card.MouseFilter = Control.MouseFilterEnum.Stop;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedCharacter != null && currentActionCharacter != null && targetCharacter != null)
|
if (_selectedCharacter != null && _currentActionCard != null && _targetCharacter != null)
|
||||||
{
|
{
|
||||||
var strategy = Cthangover.CardBattle.Player.CardActionStrategyFactory.Get(
|
var strategy = Player.CardActionStrategyFactory.Get(
|
||||||
currentActionCharacter.Card, selectedCharacter, targetCharacter);
|
_currentActionCard.Card, _selectedCharacter, _targetCharacter);
|
||||||
|
|
||||||
var valid = strategy != null && strategy.Check(currentActionCharacter.Card, selectedCharacter, targetCharacter);
|
if (strategy != null && strategy.Check(_currentActionCard.Card, _selectedCharacter, _targetCharacter))
|
||||||
|
|
||||||
GameLogger.Log("CARD_CTRL",
|
|
||||||
$"End drag: {currentActionCharacter.Card.Name} from {selectedCharacter.Card.Name} to {targetCharacter.Card.Name} valid={valid}",
|
|
||||||
LogLevel.Debug);
|
|
||||||
|
|
||||||
if (valid)
|
|
||||||
{
|
{
|
||||||
if (_originalActionCardParent != null)
|
if (_originalActionCardParent != null)
|
||||||
{
|
{
|
||||||
currentActionCharacter.GetControlNode().Reparent(_originalActionCardParent);
|
_currentActionCard.GetControlNode().Reparent(_originalActionCardParent);
|
||||||
_originalActionCardParent = null;
|
_originalActionCardParent = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
var audioService = SceneContextNode.Instance?.GetSceneRoot<AudioService>("AudioService");
|
||||||
audioService?.PlaySound("battle/card_drop", 4, Cthangover.Core.Audio.SoundType.CardEffect);
|
audioService?.PlaySound("battle/card_drop", 4, SoundType.CardEffect);
|
||||||
|
|
||||||
DoActionToTarget();
|
DoActionToTarget();
|
||||||
|
|
||||||
if (panels != null)
|
if (_panels != null)
|
||||||
{
|
{
|
||||||
foreach (var panel in panels)
|
foreach (var panel in _panels)
|
||||||
panel?.Redraw();
|
panel?.Redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,9 +399,9 @@ namespace Cthangover.CardBattle.UI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentActionCharacter != null)
|
if (_currentActionCard != null)
|
||||||
{
|
{
|
||||||
var control = currentActionCharacter.GetControlNode();
|
var control = _currentActionCard.GetControlNode();
|
||||||
if (_originalActionCardParent != null)
|
if (_originalActionCardParent != null)
|
||||||
{
|
{
|
||||||
control.Reparent(_originalActionCardParent);
|
control.Reparent(_originalActionCardParent);
|
||||||
@@ -455,57 +410,56 @@ namespace Cthangover.CardBattle.UI
|
|||||||
var resetTween = CreateTween();
|
var resetTween = CreateTween();
|
||||||
resetTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
resetTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||||
resetTween.TweenProperty(control, "scale", Vector2.One, 0.15f);
|
resetTween.TweenProperty(control, "scale", Vector2.One, 0.15f);
|
||||||
control.ZIndex = 0;
|
|
||||||
|
BattleLogHub.Log(
|
||||||
|
string.Format(Translate("battle/log_invalid_target"), ActionCharacterUi.Translate(_currentActionCard.Card.Name)),
|
||||||
|
BattleLogKind.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
Redraw();
|
|
||||||
ClearSelections(false);
|
ClearSelections(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private T TryFindCardByPosition<T>(Vector2 screenPosition, IList<T> cards, ICard exclude = null) where T : class, ICard
|
private T TryFindCardByPosition<T>(Vector2 screenPosition, IList<T> cards) where T : class, ICard
|
||||||
{
|
{
|
||||||
if (cards == null)
|
|
||||||
return null;
|
|
||||||
foreach (var card in cards)
|
foreach (var card in cards)
|
||||||
{
|
{
|
||||||
if (card == exclude || card == null)
|
if (card == null || !GodotObject.IsInstanceValid(card.GetControlNode()))
|
||||||
continue;
|
continue;
|
||||||
if (card.Frame != null && card.Frame.GetGlobalRect().HasPoint(screenPosition))
|
var rect = card.GetControlNode();
|
||||||
|
if (rect.GetGlobalRect().HasPoint(screenPosition))
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private T TryFindCardByRect<T>(Control draggingRect, IList<T> cards, ICard exclude = null) where T : class, ICard
|
private T TryFindCardByRect<T>(Control draggingRect, IList<T> cards) where T : class, ICard
|
||||||
{
|
{
|
||||||
Vector2 screenPoint = draggingRect.GlobalPosition + draggingRect.Size / 2;
|
var screenPoint = draggingRect.GlobalPosition + draggingRect.Size / 2;
|
||||||
foreach (var card in cards)
|
foreach (var card in cards)
|
||||||
{
|
{
|
||||||
if (card == exclude || card == null)
|
if (card == null || !GodotObject.IsInstanceValid(card.GetControlNode()))
|
||||||
continue;
|
continue;
|
||||||
if (card.Frame != null && card.Frame.GetGlobalRect().HasPoint(screenPoint))
|
var rect = card.GetControlNode();
|
||||||
|
if (rect.GetGlobalRect().HasPoint(screenPoint))
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CharacterCardNode> GetAllCharacters()
|
private List<CharacterCardNode> CollectCharacters()
|
||||||
{
|
{
|
||||||
var list = new List<CharacterCardNode>();
|
_characterCards.Clear();
|
||||||
var root = GetParent();
|
foreach (var panel in _panels)
|
||||||
if (root != null)
|
{
|
||||||
CollectNodes<CharacterCardNode>(root, list);
|
if (panel == null)
|
||||||
return list;
|
continue;
|
||||||
|
foreach (var card in panel.CardList)
|
||||||
|
_characterCards.Add(card);
|
||||||
|
}
|
||||||
|
return _characterCards;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CollectNodes<T>(Node parent, List<T> results) where T : Node
|
private static string Translate(string key)
|
||||||
{
|
=> TranslationServer.Translate(key).ToString();
|
||||||
foreach (Node child in parent.GetChildren())
|
|
||||||
{
|
|
||||||
if (child is T t)
|
|
||||||
results.Add(t);
|
|
||||||
CollectNodes(child, results);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+266
-168
@@ -1,5 +1,5 @@
|
|||||||
using Cthangover.Core.Cards;
|
using System.Linq;
|
||||||
using Cthangover.Core.Mods;
|
using Cthangover.Core.Cards.StatusEffect;
|
||||||
using Cthangover.Core.Mods.Resolvers;
|
using Cthangover.Core.Mods.Resolvers;
|
||||||
using Cthangover.Core.Characters;
|
using Cthangover.Core.Characters;
|
||||||
using Cthangover.Core.UI;
|
using Cthangover.Core.UI;
|
||||||
@@ -9,150 +9,192 @@ using Godot;
|
|||||||
namespace Cthangover.CardBattle.UI
|
namespace Cthangover.CardBattle.UI
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI node representing a single character (player or enemy) on the battle field.
|
/// Party or enemy card on the battle field, rebuilt from the interface
|
||||||
/// Displays the character portrait, name, health bar, and stat labels (defence S, attack A, action points P).
|
/// mockup (mockups/card_battle): full-bleed portrait, team-coloured frame,
|
||||||
/// Implements <see cref="ICard"/> for hit-testing during drag-and-drop targeting.
|
/// bottom info block with the name, four stat chips (HP/ЗАЩ/АТК/ОД) and an
|
||||||
/// Supports team-colored outlines (blue for player, yellow for enemy), hover scale-up animation,
|
/// HP bar, plus stun badge, zap flash and selection frame in the upper right.
|
||||||
/// and multiple selection highlight modes (<see cref="Select"/>, <see cref="Attack"/>, <see cref="Invalid"/>)
|
/// The node tree lives in <c>scenes/character_card.tscn</c>; this class
|
||||||
/// driven by the <see cref="ICardActionStrategy"/> flow. Manages Z-index layering for selected,
|
/// fetches the nodes once, dresses them with <see cref="UiTokens"/> styles and
|
||||||
/// hovered, and normal states.
|
/// exposes the battle-facing API: <see cref="UpdateInfo"/> is the single
|
||||||
|
/// refresh entry point for mutable stats, <see cref="Select"/> /
|
||||||
|
/// <see cref="Attack"/> / <see cref="Invalid"/> paint target feedback for the
|
||||||
|
/// drag-and-drop flow, <see cref="PlayZap"/> and <see cref="Flash"/> give
|
||||||
|
/// impact feedback, <see cref="AllImages"/> feeds the dissolve shader of
|
||||||
|
/// <see cref="CardDeathAnimation"/>.
|
||||||
|
/// <para/>
|
||||||
|
/// Implements <see cref="ICard"/> for hit-testing during drag-and-drop
|
||||||
|
/// targeting and keeps the hover lift / Z-index layering rules of the
|
||||||
|
/// original widget so overlapping cards stay readable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class CharacterCardNode : ModWidget, ICard
|
public partial class CharacterCardNode : ModWidget, ICard
|
||||||
{
|
{
|
||||||
private ColorRect teamOutline;
|
private const float HoverLift = 10f;
|
||||||
private TextureRect frame;
|
private const float HoverScale = 1.12f;
|
||||||
private TextureRect image;
|
|
||||||
private TextureRect selection;
|
|
||||||
|
|
||||||
private ColorRect healthRect;
|
private TextureRect _art;
|
||||||
private Label nameLabel;
|
private Panel _teamFrame;
|
||||||
private Label defenceLabel;
|
private Label _nameLabel;
|
||||||
private Label attackLabel;
|
private ProgressBar _hpBar;
|
||||||
private Label pointsLabel;
|
private Label _hpValue;
|
||||||
|
private Label _defValue;
|
||||||
|
private Label _atkValue;
|
||||||
|
private Label _apValue;
|
||||||
|
private VBoxContainer _status;
|
||||||
|
private PanelContainer _stunBadge;
|
||||||
|
private TextureRect _stunIcon;
|
||||||
|
private Label _stunLabel;
|
||||||
|
private TextureRect _zap;
|
||||||
|
private Panel _fx;
|
||||||
|
private TextureRect _selection;
|
||||||
|
|
||||||
private Color selectionState;
|
private Color _selectionState = Colors.Transparent;
|
||||||
private float defaultWidth;
|
private float _baseY;
|
||||||
private float baseY;
|
private bool _isHovered;
|
||||||
private bool isHovered;
|
private bool _selected;
|
||||||
private Tween hoverTween;
|
private int _baseZIndex;
|
||||||
private Tween selectionTween;
|
private Vector2 _baseScale = Vector2.One;
|
||||||
private static Texture2D cachedSelectTex;
|
private Tween _hoverTween;
|
||||||
private static bool selectTexLoaded;
|
private Tween _selectionTween;
|
||||||
|
|
||||||
|
private static Texture2D _cachedSelectTex;
|
||||||
|
private static bool _texturesLoaded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Distinguishes player-owned cards from enemy cards. Affects team outline color in
|
/// Distinguishes player-owned cards from enemy cards. Must be assigned by
|
||||||
/// <see cref="SetTeamColors"/> and determines whether the card is clickable in
|
/// <see cref="BattleCardPanel"/> before <see cref="Init"/>, because the
|
||||||
/// <see cref="CardController.OnPointerClick"/>.
|
/// team frame colour is applied during initialisation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsPlayer { get; set; }
|
public bool IsPlayer { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Marks the card as dead. Set by <see cref="BattleCardPanel.Dead"/>, checked by
|
/// Marks the card as dead. Set by <see cref="BattleCardPanel.Dead"/>,
|
||||||
/// <see cref="CardBattleCore"/> when counting alive cards and by
|
/// checked by the battle core when counting alive cards and when the
|
||||||
/// <see cref="CardBattleCore.RunEnemyTurn"/> when iterating for enemy actions.
|
/// enemy AI picks targets.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDead { get; set; }
|
public bool IsDead { get; set; }
|
||||||
|
|
||||||
private int _baseZIndex;
|
/// <summary>The character data model backing this card: stats, action deck, status effects.</summary>
|
||||||
private bool _selected;
|
|
||||||
private Vector2 _baseScale;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TextureRect Frame => frame;
|
|
||||||
/// <inheritdoc />
|
|
||||||
public TextureRect Image => image;
|
|
||||||
/// <summary>
|
|
||||||
/// All <see cref="TextureRect"/> nodes on this card (frame, image, selection).
|
|
||||||
/// Consumed by <see cref="CardDeathAnimation"/> to apply the dissolve shader to every visual element.
|
|
||||||
/// </summary>
|
|
||||||
public TextureRect[] AllImages => _allImages ??= new[] { frame, image, selection };
|
|
||||||
private TextureRect[] _allImages;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="Character"/> data model backing this card. Contains attributes
|
|
||||||
/// (health, attack, defence, points) and the action deck.
|
|
||||||
/// </summary>
|
|
||||||
public Character Card { get; set; }
|
public Character Card { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns <c>this</c> — character cards are self-contained <see cref="Control"/> nodes.
|
/// Textures that receive the dissolve shader during the death animation:
|
||||||
|
/// the portrait and the selection frame. The team frame and the info
|
||||||
|
/// block are simple styleboxes and fade with the whole card node.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public TextureRect[] AllImages => _allImages ??= new[] { _art, _selection };
|
||||||
|
private TextureRect[] _allImages;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Control GetControlNode() => this;
|
public Control GetControlNode() => this;
|
||||||
|
|
||||||
protected override void Construct()
|
protected override void Construct()
|
||||||
{
|
{
|
||||||
CthangoverTheme.Apply(this);
|
CthangoverTheme.Apply(this);
|
||||||
|
|
||||||
teamOutline = GetNode<ColorRect>("TeamOutline");
|
_art = GetNode<TextureRect>("Art");
|
||||||
image = GetNode<TextureRect>("Image");
|
_teamFrame = GetNode<Panel>("TeamFrame");
|
||||||
frame = GetNode<TextureRect>("Frame");
|
_nameLabel = GetNode<Label>("Info/Column/NameLabel");
|
||||||
selection = GetNode<TextureRect>("Selection");
|
_hpBar = GetNode<ProgressBar>("Info/Column/HpBar");
|
||||||
nameLabel = GetNode<Label>("InfoPanel/NameLabel");
|
_hpValue = GetNode<Label>("Info/Column/Stats/HpChip/Row/Value");
|
||||||
healthRect = GetNode<ColorRect>("InfoPanel/HealthBar/HealthRect");
|
_defValue = GetNode<Label>("Info/Column/Stats/DefChip/Row/Value");
|
||||||
defenceLabel = GetNode<Label>("InfoPanel/Labels/DefenceLabel");
|
_atkValue = GetNode<Label>("Info/Column/Stats/AtkChip/Row/Value");
|
||||||
attackLabel = GetNode<Label>("InfoPanel/Labels/AttackLabel");
|
_apValue = GetNode<Label>("Info/Column/Stats/ApChip/Row/Value");
|
||||||
pointsLabel = GetNode<Label>("InfoPanel/Labels/PointsLabel");
|
_status = GetNode<VBoxContainer>("Status");
|
||||||
|
_stunBadge = GetNode<PanelContainer>("Status/StunBadge");
|
||||||
|
_stunIcon = GetNode<TextureRect>("Status/StunBadge/Row/StunIcon");
|
||||||
|
_stunLabel = GetNode<Label>("Status/StunBadge/Row/StunLabel");
|
||||||
|
_zap = GetNode<TextureRect>("Zap");
|
||||||
|
_fx = GetNode<Panel>("Fx");
|
||||||
|
_selection = GetNode<TextureRect>("Selection");
|
||||||
|
|
||||||
Size = CustomMinimumSize;
|
Size = CustomMinimumSize;
|
||||||
|
_baseScale = Scale;
|
||||||
|
|
||||||
defaultWidth = 230f;
|
if (!_texturesLoaded)
|
||||||
selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
{
|
||||||
|
_cachedSelectTex = Textures.Resolve("select");
|
||||||
|
_texturesLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!selectTexLoaded)
|
if (_cachedSelectTex != null)
|
||||||
{
|
_selection.Texture = _cachedSelectTex;
|
||||||
cachedSelectTex = Textures.Resolve("select");
|
|
||||||
selectTexLoaded = true;
|
ApplyStyles();
|
||||||
}
|
_selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
if (cachedSelectTex != null)
|
|
||||||
{
|
|
||||||
selection.Texture = cachedSelectTex;
|
|
||||||
selection.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize;
|
|
||||||
selection.StretchMode = TextureRect.StretchModeEnum.Scale;
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseEntered += OnMouseEntered;
|
MouseEntered += OnMouseEntered;
|
||||||
MouseExited += OnMouseExited;
|
MouseExited += OnMouseExited;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the card with the given <paramref name="characterCard"/> data, sets the
|
/// Binds the card to a character: sets the portrait, resets selection
|
||||||
/// portrait and frame textures, resets selection visuals, and refreshes stat labels.
|
/// visuals and refreshes every stat label through <see cref="UpdateInfo"/>.
|
||||||
/// Called by <see cref="BattleCardPanel.Create"/>.
|
/// Called by <see cref="BattleCardPanel.Create"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Init(Character characterCard)
|
public void Init(Character characterCard)
|
||||||
{
|
{
|
||||||
Card = characterCard;
|
Card = characterCard;
|
||||||
_baseScale = Scale;
|
_baseScale = Scale;
|
||||||
|
IsDead = false;
|
||||||
|
|
||||||
if (image != null)
|
if (_art != null && characterCard?.Image != null)
|
||||||
image.Texture = characterCard.Image;
|
_art.Texture = characterCard.Image;
|
||||||
|
|
||||||
if (frame != null && characterCard?.Image != null)
|
if (_selection != null)
|
||||||
frame.Texture = characterCard.Image;
|
_selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
|
||||||
if (selection != null)
|
if (_fx != null)
|
||||||
selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
_fx.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
if (_zap != null)
|
||||||
selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
_zap.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
|
||||||
|
_selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
UpdateInfo();
|
UpdateInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Applies a team-colored outline: blue for player cards (<see cref="IsPlayer"/> = <c>true</c>),
|
/// Paints the team frame: blue for the party, gold for enemies — the
|
||||||
/// yellow for enemy cards. Called by <see cref="BattleCardPanel.Create"/> after initialization.
|
/// same accent language the mockup uses for card outlines. Called by
|
||||||
|
/// <see cref="BattleCardPanel.Create"/> after <see cref="Init"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetTeamColors()
|
public void SetTeamColors()
|
||||||
{
|
{
|
||||||
if (teamOutline != null)
|
var accent = IsPlayer ? UiTokens.Info : UiTokens.Accent;
|
||||||
teamOutline.Color = IsPlayer
|
var style = UiKit.Frame(UiTokens.RadiusMd, new Color(0f, 0f, 0f, 0f), UiTokens.Alpha(accent, 0.5f), 2);
|
||||||
? UiTokens.Alpha(UiTokens.Info, 0.5f)
|
_teamFrame.AddThemeStyleboxOverride("panel", style);
|
||||||
: UiTokens.Alpha(UiTokens.Accent, 0.5f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the base Z-index used for layering cards. When not selected, the card renders at
|
/// Refreshes every mutable value from the character model: name, HP
|
||||||
/// this Z-index. Selected cards jump to Z-index 4096 to render above all others.
|
/// number and bar (turning red below 40%), defence, attack, action
|
||||||
/// Called by <see cref="BattleCardPanel.SetCardPosition"/> during layout.
|
/// points and the stun badge with its remaining turns. Called after each
|
||||||
|
/// action, on turn start, and by the panel after re-gridding.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateInfo()
|
||||||
|
{
|
||||||
|
if (Card?.Attributes == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_nameLabel.Text = TranslationServer.Translate(Card.Name ?? string.Empty).ToString();
|
||||||
|
|
||||||
|
var health = Card.Attributes.Health;
|
||||||
|
var percent = Mathf.Clamp(health.Percent, 0f, 1f);
|
||||||
|
_hpBar.Value = percent * 100f;
|
||||||
|
_hpBar.AddThemeStyleboxOverride("background", UiKit.Frame(UiTokens.RadiusSm, UiTokens.LineDisabled));
|
||||||
|
_hpBar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, percent < 0.4f ? UiTokens.HpLow : UiTokens.HpOk));
|
||||||
|
|
||||||
|
_hpValue.Text = health.Value + "/" + health.BaseValue;
|
||||||
|
_defValue.Text = Card.Attributes.Defence.Value.ToString();
|
||||||
|
_atkValue.Text = Card.Attributes.Attack.Value.ToString();
|
||||||
|
_apValue.Text = Card.Attributes.Point.Value.ToString();
|
||||||
|
|
||||||
|
RefreshStun();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the base Z-index used for layering cards. Selected cards jump to
|
||||||
|
/// Z-index 4096 in <see cref="Select"/>; hovered cards rise by 100 so
|
||||||
|
/// they cover their neighbours.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetBaseZIndex(int z)
|
public void SetBaseZIndex(int z)
|
||||||
{
|
{
|
||||||
@@ -161,120 +203,176 @@ namespace Cthangover.CardBattle.UI
|
|||||||
ZIndex = z;
|
ZIndex = z;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Highlights the card as the chosen source or a valid support target (teal).</summary>
|
||||||
/// Applies a target-accent selection highlight, raises Z-index to 4096 to ensure the card
|
|
||||||
/// is clickable above other cards during drag-and-drop targeting, and animates
|
|
||||||
/// the selection overlay to <see cref="UiTokens.TypeTarget"/>. Called when the player clicks a character card
|
|
||||||
/// or when a valid support/self target is dragged over.
|
|
||||||
/// </summary>
|
|
||||||
public void Select()
|
public void Select()
|
||||||
{
|
{
|
||||||
_selected = true;
|
_selected = true;
|
||||||
selectionState = UiTokens.TypeTarget;
|
_selectionState = UiTokens.TypeTarget;
|
||||||
AnimateSelection(selectionState);
|
AnimateSelection(_selectionState);
|
||||||
if (selection != null) selection.ZIndex = 1;
|
|
||||||
ZIndex = 4096;
|
ZIndex = 4096;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Clears the selection highlight and restores the base Z-index.</summary>
|
||||||
/// Clears the selection highlight, restores Z-index to <see cref="SetBaseZIndex"/> value,
|
|
||||||
/// and fades the selection overlay to transparent.
|
|
||||||
/// </summary>
|
|
||||||
public void Unselect()
|
public void Unselect()
|
||||||
{
|
{
|
||||||
_selected = false;
|
_selected = false;
|
||||||
selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
_selectionState = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
AnimateSelection(selectionState);
|
AnimateSelection(_selectionState);
|
||||||
if (selection != null) selection.ZIndex = 0;
|
|
||||||
ZIndex = _baseZIndex;
|
ZIndex = _baseZIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Highlights a valid enemy target for an attack (red).</summary>
|
||||||
/// Applies an enemy-accent highlight indicating this card is being targeted for an attack.
|
|
||||||
/// Called by <see cref="ICardActionStrategy.HighlightTarget"/> during drag-over.
|
|
||||||
/// </summary>
|
|
||||||
public void Attack()
|
public void Attack()
|
||||||
{
|
{
|
||||||
selectionState = UiTokens.TypeEnemy;
|
_selectionState = UiTokens.TypeEnemy;
|
||||||
AnimateSelection(selectionState);
|
AnimateSelection(_selectionState);
|
||||||
if (selection != null) selection.ZIndex = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Grays out the card as an invalid target for the dragged action.</summary>
|
||||||
/// Applies a gray dimming highlight indicating this card is an invalid target
|
|
||||||
/// for the currently dragged action card. Called by <see cref="CardController.OnDrag"/>
|
|
||||||
/// when the strategy's <c>Check</c> returns <c>false</c>.
|
|
||||||
/// </summary>
|
|
||||||
public void Invalid()
|
public void Invalid()
|
||||||
{
|
{
|
||||||
selectionState = UiTokens.Alpha(UiTokens.Muted, 0.8f);
|
_selectionState = UiTokens.Alpha(UiTokens.Muted, 0.8f);
|
||||||
AnimateSelection(selectionState);
|
AnimateSelection(_selectionState);
|
||||||
if (selection != null) selection.ZIndex = 1;
|
}
|
||||||
|
|
||||||
|
/// <summary>Plays the electric stun flash over the portrait (additive blend).</summary>
|
||||||
|
public void PlayZap()
|
||||||
|
{
|
||||||
|
if (_zap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_zap.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
var tween = CreateTween();
|
||||||
|
tween.TweenProperty(_zap, "modulate:a", 1f, 0.12f);
|
||||||
|
tween.TweenProperty(_zap, "modulate:a", 0f, 0.45f).SetDelay(0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pulses the whole card with a colour (damage red, shield blue).</summary>
|
||||||
|
public void Flash(Color color)
|
||||||
|
{
|
||||||
|
_fx.Modulate = UiTokens.Alpha(color, 0f);
|
||||||
|
var tween = CreateTween();
|
||||||
|
tween.TweenProperty(_fx, "modulate:a", 0.55f, 0.12f);
|
||||||
|
tween.TweenProperty(_fx, "modulate:a", 0f, 0.38f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyStyles()
|
||||||
|
{
|
||||||
|
var infoStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface2, 0.93f), UiTokens.Line);
|
||||||
|
infoStyle.CornerRadiusTopLeft = 0;
|
||||||
|
infoStyle.CornerRadiusTopRight = 0;
|
||||||
|
infoStyle.ContentMarginLeft = 8;
|
||||||
|
infoStyle.ContentMarginRight = 8;
|
||||||
|
infoStyle.ContentMarginTop = 6;
|
||||||
|
infoStyle.ContentMarginBottom = 8;
|
||||||
|
GetNode<PanelContainer>("Info").AddThemeStyleboxOverride("panel", infoStyle);
|
||||||
|
|
||||||
|
_nameLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||||
|
|
||||||
|
StyleChip("Info/Column/Stats/HpChip", UiTokens.HpText);
|
||||||
|
StyleChip("Info/Column/Stats/DefChip", UiTokens.Ink);
|
||||||
|
StyleChip("Info/Column/Stats/AtkChip", UiTokens.Ink);
|
||||||
|
StyleChip("Info/Column/Stats/ApChip", UiTokens.AccentBright);
|
||||||
|
|
||||||
|
var stunStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.CooldownSoft, UiTokens.CooldownLine);
|
||||||
|
stunStyle.ContentMarginLeft = 5;
|
||||||
|
stunStyle.ContentMarginRight = 8;
|
||||||
|
stunStyle.ContentMarginTop = 3;
|
||||||
|
stunStyle.ContentMarginBottom = 3;
|
||||||
|
_stunBadge.AddThemeStyleboxOverride("panel", stunStyle);
|
||||||
|
_stunLabel.AddThemeColorOverride("font_color", UiTokens.Cooldown);
|
||||||
|
|
||||||
|
_zap.Material = new CanvasItemMaterial { BlendMode = CanvasItemMaterial.BlendModeEnum.Add };
|
||||||
|
_zap.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
_fx.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
_selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StyleChip(string path, Color valueColor)
|
||||||
|
{
|
||||||
|
var chip = GetNode<PanelContainer>(path);
|
||||||
|
var style = UiKit.Frame(UiTokens.RadiusXs, UiTokens.StatBg, UiTokens.StatLine);
|
||||||
|
style.ContentMarginLeft = 6;
|
||||||
|
style.ContentMarginRight = 6;
|
||||||
|
style.ContentMarginTop = 2;
|
||||||
|
style.ContentMarginBottom = 2;
|
||||||
|
chip.AddThemeStyleboxOverride("panel", style);
|
||||||
|
|
||||||
|
var row = chip.GetNode<HBoxContainer>("Row");
|
||||||
|
var key = row.GetNode<Label>("Key");
|
||||||
|
key.Text = TranslationServer.Translate(key.Text).ToString();
|
||||||
|
key.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||||
|
row.GetNode<Label>("Value").AddThemeColorOverride("font_color", valueColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshStun()
|
||||||
|
{
|
||||||
|
var stun = StatusEffectService.Of(Card)?.ActiveStatusEffects?
|
||||||
|
.FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun);
|
||||||
|
|
||||||
|
if (stun == null)
|
||||||
|
{
|
||||||
|
_stunBadge.Visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stunBadge.Visible = true;
|
||||||
|
if (stun.Icon != null)
|
||||||
|
{
|
||||||
|
// Текстура приходит из данных эффекта: группа "characters" не
|
||||||
|
// входит в texture_groups, поэтому Textures.Resolve("stun") её
|
||||||
|
// не видит, а StatusEffectInfo.Icon грузится через TextureUtils.
|
||||||
|
_stunIcon.Texture = stun.Icon;
|
||||||
|
_zap.Texture = stun.Icon;
|
||||||
|
}
|
||||||
|
_stunLabel.Text = string.Format(
|
||||||
|
TranslationServer.Translate("battle/stun_status").ToString(), stun.RemainingTurns);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AnimateSelection(Color target)
|
private void AnimateSelection(Color target)
|
||||||
{
|
{
|
||||||
if (selection == null) return;
|
if (_selection == null)
|
||||||
selectionTween?.Kill();
|
return;
|
||||||
selectionTween = CreateTween();
|
|
||||||
selectionTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Quad);
|
|
||||||
selectionTween.TweenProperty(selection, "modulate", target, 0.15f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
_selectionTween?.Kill();
|
||||||
/// Refreshes all displayed stats from the underlying <see cref="Character"/> data:
|
_selectionTween = CreateTween();
|
||||||
/// name, defence, attack, action points, and health bar width (based on health percent).
|
_selectionTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Quad);
|
||||||
/// Called after every action execution and during initial setup.
|
_selectionTween.TweenProperty(_selection, "modulate", target, 0.15f);
|
||||||
/// </summary>
|
|
||||||
public void UpdateInfo()
|
|
||||||
{
|
|
||||||
if (healthRect == null || Card == null) return;
|
|
||||||
|
|
||||||
if (nameLabel != null)
|
|
||||||
nameLabel.Text = TranslationServer.Translate(Card.Name);
|
|
||||||
if (defenceLabel != null)
|
|
||||||
defenceLabel.Text = Card.Attributes.Defence.Value.ToString();
|
|
||||||
if (attackLabel != null)
|
|
||||||
attackLabel.Text = Card.Attributes.Attack.Value.ToString();
|
|
||||||
if (pointsLabel != null)
|
|
||||||
pointsLabel.Text = Card.Attributes.Point.Value.ToString();
|
|
||||||
|
|
||||||
var sizeDelta = healthRect.Size;
|
|
||||||
sizeDelta.X = Card.Attributes.Health.Percent * defaultWidth;
|
|
||||||
healthRect.SetDeferred(Control.PropertyName.Size, sizeDelta);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseEntered()
|
private void OnMouseEntered()
|
||||||
{
|
{
|
||||||
if (isHovered) return;
|
if (_isHovered)
|
||||||
isHovered = true;
|
return;
|
||||||
baseY = Position.Y;
|
_isHovered = true;
|
||||||
|
_baseY = Position.Y;
|
||||||
|
|
||||||
if (!_selected)
|
if (!_selected)
|
||||||
ZIndex = _baseZIndex + 100;
|
ZIndex = _baseZIndex + 100;
|
||||||
|
|
||||||
hoverTween?.Kill();
|
_hoverTween?.Kill();
|
||||||
hoverTween = CreateTween();
|
_hoverTween = CreateTween();
|
||||||
hoverTween.SetParallel(true);
|
_hoverTween.SetParallel(true);
|
||||||
hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
_hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||||
hoverTween.TweenProperty(this, "position:y", baseY - 10f, 0.15f);
|
_hoverTween.TweenProperty(this, "position:y", _baseY - HoverLift, 0.15f);
|
||||||
hoverTween.TweenProperty(this, "scale", _baseScale * 1.15f, 0.15f);
|
_hoverTween.TweenProperty(this, "scale", _baseScale * HoverScale, 0.15f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnMouseExited()
|
private void OnMouseExited()
|
||||||
{
|
{
|
||||||
if (!isHovered) return;
|
if (!_isHovered)
|
||||||
isHovered = false;
|
return;
|
||||||
|
_isHovered = false;
|
||||||
|
|
||||||
if (!_selected)
|
if (!_selected)
|
||||||
ZIndex = _baseZIndex;
|
ZIndex = _baseZIndex;
|
||||||
|
|
||||||
hoverTween?.Kill();
|
_hoverTween?.Kill();
|
||||||
hoverTween = CreateTween();
|
_hoverTween = CreateTween();
|
||||||
hoverTween.SetParallel(true);
|
_hoverTween.SetParallel(true);
|
||||||
hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
_hoverTween.SetEase(Tween.EaseType.Out).SetTrans(Tween.TransitionType.Cubic);
|
||||||
hoverTween.TweenProperty(this, "position:y", baseY, 0.15f);
|
_hoverTween.TweenProperty(this, "position:y", _baseY, 0.15f);
|
||||||
hoverTween.TweenProperty(this, "scale", _baseScale, 0.15f);
|
_hoverTween.TweenProperty(this, "scale", _baseScale, 0.15f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,10 +28,11 @@ namespace Cthangover.CardBattle.UI
|
|||||||
|
|
||||||
Size = CustomMinimumSize;
|
Size = CustomMinimumSize;
|
||||||
|
|
||||||
_button.Text = TranslationServer.Translate("battle/end_turn");
|
_button.Pressed += RaisePressed;
|
||||||
_button.Pressed += () => OnPressed?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RaisePressed() => OnPressed?.Invoke();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Shows or hides the underlying <see cref="Button"/> control.
|
/// Shows or hides the underlying <see cref="Button"/> control.
|
||||||
/// Called by <see cref="CardBattleCore"/> during turn transitions and battle cleanup.
|
/// Called by <see cref="CardBattleCore"/> during turn transitions and battle cleanup.
|
||||||
|
|||||||
Reference in new issue
Block a user