new battle gui
This commit is contained in:
1 parent
86e3de904e
commit
ba2c08b1dc
22 files changed
+1613
-120
No files matched your search
@@ -12,3 +12,49 @@ ff_battle/select_target = Select a target
|
||||
ff_battle/escape_fail = Escape failed!
|
||||
ff_battle/escape_success = You escaped!
|
||||
ff_battle/no_actions = No actions
|
||||
|
||||
ff_battle/hud_title = Battle
|
||||
ff_battle/round = Round
|
||||
ff_battle/enemies = Enemies
|
||||
ff_battle/allies = Party
|
||||
ff_battle/phase_player = Player turn
|
||||
ff_battle/phase_enemy = Enemy turn
|
||||
ff_battle/banner_player = Player turn
|
||||
ff_battle/banner_enemy = Enemy turn
|
||||
ff_battle/hint_target = Select a target · RMB / Esc — cancel
|
||||
ff_battle/hint_ally = Select an ally · RMB / Esc — cancel
|
||||
ff_battle/menu_caption = {0} · AP {1}/{2}
|
||||
ff_battle/menu_actions = Actions
|
||||
|
||||
ff_battle/log_title = Battle log
|
||||
ff_battle/log_round = round {0}
|
||||
ff_battle/log_battle_start = Battle started
|
||||
ff_battle/log_turn_player = — Player turn · round {0} —
|
||||
ff_battle/log_turn_enemy = — Enemy turn · round {0} —
|
||||
ff_battle/log_action = {0}: {1} → {2}
|
||||
ff_battle/log_action_self = {0}: {1}
|
||||
ff_battle/log_damage = {0}: {1} → {2} · -{3} HP
|
||||
ff_battle/log_shield = {0}'s shield absorbed {1} · left {2}
|
||||
ff_battle/log_defence = Defend: {0} · +{1} DEF (total {2})
|
||||
ff_battle/log_stun = Stunned: {0} · {1} turns
|
||||
ff_battle/log_stun_skip = {0} is stunned — skips the turn
|
||||
ff_battle/log_heal = {0}: {1} restored {2} HP
|
||||
ff_battle/log_death = {0} defeated
|
||||
ff_battle/log_escape_success = Escape succeeded
|
||||
ff_battle/log_escape_failed = Escape failed! {0} loses 1 AP
|
||||
ff_battle/log_no_points = The party is out of AP
|
||||
|
||||
ff_battle/detail_attack = Character action submenu. AP cost is shown next to the name.
|
||||
ff_battle/detail_items = 1 AP per item. Targeted items require an ally.
|
||||
ff_battle/detail_defend = Shortcut: the ally-target action is applied to self. Shield +4 DEF.
|
||||
ff_battle/detail_escape = 70% success chance. On failure the character loses 1 AP.
|
||||
ff_battle/detail_back = Return to the main command menu.
|
||||
ff_battle/detail_action = Special action without numeric parameters.
|
||||
ff_battle/detail_item = An item from the shared inventory.
|
||||
ff_battle/detail_damage = Damage {0} (ATK {1} × {2}) · cost {3} AP
|
||||
ff_battle/detail_stun = Stun for {0} turns · cost {1} AP
|
||||
ff_battle/detail_defence = Target shield +{0} · cost {1} AP
|
||||
|
||||
ff_battle/status_stun = Stunned {0}
|
||||
ff_battle/badge_turns = {0} turns
|
||||
ff_battle/badge_self = Self
|
||||
@@ -12,3 +12,49 @@ ff_battle/select_target=Выберите цель
|
||||
ff_battle/escape_fail=Побег не удался!
|
||||
ff_battle/escape_success=Вы сбежали!
|
||||
ff_battle/no_actions=Нет действий
|
||||
|
||||
ff_battle/hud_title=Бой
|
||||
ff_battle/round=Раунд
|
||||
ff_battle/enemies=Врагов
|
||||
ff_battle/allies=Отряд
|
||||
ff_battle/phase_player=Ход игрока
|
||||
ff_battle/phase_enemy=Ход врага
|
||||
ff_battle/banner_player=Ход игрока
|
||||
ff_battle/banner_enemy=Ход врага
|
||||
ff_battle/hint_target=Выберите цель · ПКМ / Esc — отмена
|
||||
ff_battle/hint_ally=Выберите союзника · ПКМ / Esc — отмена
|
||||
ff_battle/menu_caption={0} · ОД {1}/{2}
|
||||
ff_battle/menu_actions=Действия
|
||||
|
||||
ff_battle/log_title=Журнал боя
|
||||
ff_battle/log_round=раунд {0}
|
||||
ff_battle/log_battle_start=Бой начался
|
||||
ff_battle/log_turn_player=— Ход игрока · раунд {0} —
|
||||
ff_battle/log_turn_enemy=— Ход врага · раунд {0} —
|
||||
ff_battle/log_action={0}: {1} → {2}
|
||||
ff_battle/log_action_self={0}: {1}
|
||||
ff_battle/log_damage={0}: {1} → {2} · −{3} HP
|
||||
ff_battle/log_shield=Щит {0} поглотил {1} · осталось {2}
|
||||
ff_battle/log_defence=Защита: {0} · +{1} ЗАЩ (всего {2})
|
||||
ff_battle/log_stun=Ошеломление: {0} · {1} хода
|
||||
ff_battle/log_stun_skip={0} ошеломлён — пропускает ход
|
||||
ff_battle/log_heal={0}: {1} восстановил {2} HP
|
||||
ff_battle/log_death={0} повержен
|
||||
ff_battle/log_escape_success=Побег удался
|
||||
ff_battle/log_escape_failed=Побег не удался! {0} теряет 1 ОД
|
||||
ff_battle/log_no_points=У отряда закончились ОД
|
||||
|
||||
ff_battle/detail_attack=Подменю действий персонажа. Стоимость в ОД указана справа от названия.
|
||||
ff_battle/detail_items=Расход 1 ОД за предмет. Предметы «по цели» требуют выбора союзника.
|
||||
ff_battle/detail_defend=Шорткат: действие «по союзнику» применяется на себя. Щит +4 ЗАЩ.
|
||||
ff_battle/detail_escape=Шанс успеха 70%. При провале персонаж теряет 1 ОД.
|
||||
ff_battle/detail_back=Вернуться в главное меню команд.
|
||||
ff_battle/detail_action=Особое действие без числовых параметров.
|
||||
ff_battle/detail_item=Предмет из общего инвентаря.
|
||||
ff_battle/detail_damage=Урон {0} (АТК {1} × {2}) · расход {3} ОД
|
||||
ff_battle/detail_stun=Ошеломление на {0} хода · расход {1} ОД
|
||||
ff_battle/detail_defence=Щит цели +{0} · расход {1} ОД
|
||||
|
||||
ff_battle/status_stun=Ошеломлён {0}
|
||||
ff_battle/badge_turns={0} хода
|
||||
ff_battle/badge_self=На себя
|
||||
@@ -0,0 +1,94 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFActionDetailPanel.cs" id="1_script"]
|
||||
|
||||
[node name="FFActionDetailPanel" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(300, 160)
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Box" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Card" type="PanelContainer" parent="Box"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="CardRow" type="HBoxContainer" parent="Box/Card"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="Stripe" type="ColorRect" parent="Box/Card/CardRow"]
|
||||
custom_minimum_size = Vector2(4, 0)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="IconMargin" type="MarginContainer" parent="Box/Card/CardRow"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 8
|
||||
theme_override_constants/margin_top = 8
|
||||
theme_override_constants/margin_right = 12
|
||||
theme_override_constants/margin_bottom = 8
|
||||
|
||||
[node name="CardIcon" type="TextureRect" parent="Box/Card/CardRow/IconMargin"]
|
||||
custom_minimum_size = Vector2(52, 52)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="Body" type="VBoxContainer" parent="Box/Card/CardRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="NameRow" type="HBoxContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="NameLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="IdLabel" type="Label" parent="Box/Card/CardRow/Body/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 9
|
||||
|
||||
[node name="Badges" type="HFlowContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 5
|
||||
theme_override_constants/v_separation = 5
|
||||
|
||||
[node name="Props" type="HFlowContainer" parent="Box/Card/CardRow/Body"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/h_separation = 5
|
||||
theme_override_constants/v_separation = 3
|
||||
|
||||
[node name="Note" type="PanelContainer" parent="Box"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="NoteLabel" type="Label" parent="Box/Note"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 12
|
||||
autowrap_mode = 3
|
||||
@@ -0,0 +1,26 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleBanner.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleBanner" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="Center"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Label" type="Label" parent="Center/Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
horizontal_alignment = 1
|
||||
uppercase = true
|
||||
@@ -0,0 +1,88 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleHud.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleHud" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(380, 92)
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Panel" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Column" type="VBoxContainer" parent="Panel"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 9
|
||||
|
||||
[node name="TopRow" type="HBoxContainer" parent="Panel/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Panel/Column/TopRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/hud_title"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="PhaseBadge" type="PanelContainer" parent="Panel/Column/TopRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="PhaseLabel" type="Label" parent="Panel/Column/TopRow/PhaseBadge"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
uppercase = true
|
||||
|
||||
[node name="MetaRow" type="HBoxContainer" parent="Panel/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="RoundCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/round"
|
||||
auto_translate = true
|
||||
|
||||
[node name="RoundValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "1"
|
||||
|
||||
[node name="EnemiesCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/enemies"
|
||||
auto_translate = true
|
||||
|
||||
[node name="EnemiesValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "0"
|
||||
|
||||
[node name="AlliesCaption" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/allies"
|
||||
auto_translate = true
|
||||
|
||||
[node name="AlliesValue" type="Label" parent="Panel/Column/MetaRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "0"
|
||||
@@ -0,0 +1,65 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleLogPanel.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleLogPanel" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Window" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Column" type="VBoxContainer" parent="Window"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 0
|
||||
|
||||
[node name="Header" type="MarginContainer" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 13
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 10
|
||||
|
||||
[node name="HeaderRow" type="HBoxContainer" parent="Window/Column/Header"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
text = "ff_battle/log_title"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="Spacer" type="Control" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="RoundLabel" type="Label" parent="Window/Column/Header/HeaderRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 12
|
||||
|
||||
[node name="Rule" type="HSeparator" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Scroll" type="ScrollContainer" parent="Window/Column"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
horizontal_scroll_mode = 0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Rows" type="VBoxContainer" parent="Window/Column/Scroll"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 0
|
||||
mouse_filter = 2
|
||||
@@ -0,0 +1,35 @@
|
||||
[gd_scene format=3]
|
||||
|
||||
[ext_resource type="Script" path="src/UI/FFBattleTargetHint.cs" id="1_script"]
|
||||
|
||||
[node name="FFBattleTargetHint" type="Control" script=ExtResource("1_script")]
|
||||
layout_mode = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Center" type="CenterContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Toast" type="PanelContainer" parent="Center"]
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Row" type="HBoxContainer" parent="Center/Toast"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="IconLabel" type="Label" parent="Center/Toast/Row"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "◎"
|
||||
|
||||
[node name="TextLabel" type="Label" parent="Center/Toast/Row"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 13
|
||||
+132
-28
@@ -5,51 +5,155 @@
|
||||
[node name="FFCharacterWidget" type="Control" script=ExtResource("1_script")]
|
||||
custom_minimum_size = Vector2(180, 260)
|
||||
layout_mode = 3
|
||||
mouse_filter = 0
|
||||
|
||||
[node name="Sprite" type="TextureRect" parent="."]
|
||||
[node name="TeamLine" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchor_right = 1.0
|
||||
offset_bottom = 2.0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="Art" type="TextureRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 3.0
|
||||
offset_top = 3.0
|
||||
offset_right = -3.0
|
||||
offset_bottom = -92.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
offset_left = 8.0
|
||||
offset_top = 8.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = -44.0
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="HpBg" type="ColorRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 218.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 236.0
|
||||
[node name="Info" type="PanelContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 12
|
||||
anchor_top = 1.0
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_top = -92.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
mouse_filter = 2
|
||||
color = Color(0.137255, 0.145098, 0.184314, 1)
|
||||
|
||||
[node name="HpFill" type="ColorRect" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 218.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 236.0
|
||||
[node name="Column" type="VBoxContainer" parent="Info"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="NameRow" type="HBoxContainer" parent="Info/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="TeamDot" type="Panel" parent="Info/Column/NameRow"]
|
||||
custom_minimum_size = Vector2(7, 7)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
color = Color(0.560784, 0.682353, 0.419608, 1)
|
||||
|
||||
[node name="NameLabel" type="Label" parent="."]
|
||||
layout_mode = 0
|
||||
offset_left = 4.0
|
||||
offset_top = 238.0
|
||||
offset_right = 176.0
|
||||
offset_bottom = 256.0
|
||||
[node name="NameLabel" type="Label" parent="Info/Column/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 14
|
||||
clip_text = true
|
||||
text_overrun_behavior = 3
|
||||
|
||||
[node name="HpNumLabel" type="Label" parent="Info/Column/NameRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 11
|
||||
|
||||
[node name="HpBar" type="ProgressBar" parent="Info/Column"]
|
||||
custom_minimum_size = Vector2(0, 8)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 13
|
||||
show_percentage = false
|
||||
|
||||
[node name="Selection" type="TextureRect" parent="."]
|
||||
[node name="Foot" type="HBoxContainer" parent="Info/Column"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 6
|
||||
|
||||
[node name="PipsBox" type="HBoxContainer" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="AtkLabel" type="Label" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
|
||||
[node name="Spacer" type="Control" parent="Info/Column/Foot"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ShieldChip" type="PanelContainer" parent="Info/Column/Foot"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="ShieldRow" type="HBoxContainer" parent="Info/Column/Foot/ShieldChip"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="ShieldKey" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
text = "ui/character_panel/stat_def"
|
||||
auto_translate = true
|
||||
uppercase = true
|
||||
|
||||
[node name="ShieldValue" type="Label" parent="Info/Column/Foot/ShieldChip/ShieldRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 11
|
||||
text = "0"
|
||||
|
||||
[node name="StatusBox" type="VBoxContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 1
|
||||
anchor_left = 1.0
|
||||
anchor_right = 1.0
|
||||
offset_left = -140.0
|
||||
offset_top = 8.0
|
||||
offset_right = -8.0
|
||||
offset_bottom = 40.0
|
||||
grow_horizontal = 0
|
||||
mouse_filter = 2
|
||||
theme_override_constants/separation = 4
|
||||
|
||||
[node name="StunBadge" type="PanelContainer" parent="StatusBox"]
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="StunRow" type="HBoxContainer" parent="StatusBox/StunBadge"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 5
|
||||
|
||||
[node name="StunIcon" type="TextureRect" parent="StatusBox/StunBadge/StunRow"]
|
||||
custom_minimum_size = Vector2(14, 14)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
mouse_filter = 2
|
||||
expand_mode = 1
|
||||
stretch_mode = 5
|
||||
|
||||
[node name="StunLabel" type="Label" parent="StatusBox/StunBadge/StunRow"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
theme_override_font_sizes/font_size = 10
|
||||
uppercase = true
|
||||
|
||||
[node name="Selection" type="Panel" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
|
||||
@@ -34,3 +34,13 @@ mouse_filter = 2
|
||||
visible = false
|
||||
theme_override_font_sizes/font_size = 18
|
||||
theme_override_colors/font_color = Color(0.941176, 0.756863, 0.411765, 1)
|
||||
|
||||
[node name="Caption" type="Label" parent="."]
|
||||
offset_left = 2.0
|
||||
offset_top = -25.0
|
||||
offset_right = 218.0
|
||||
offset_bottom = -7.0
|
||||
mouse_filter = 2
|
||||
visible = false
|
||||
theme_override_font_sizes/font_size = 11
|
||||
uppercase = true
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
@@ -23,8 +24,8 @@ namespace Cthangover.FFBattle.Actions
|
||||
return new ChangedAttributes { Result = false };
|
||||
|
||||
var damageDelta = Mathf.RoundToInt(action.GetFloat("Attack", 1f) * user.Attributes.Attack.Value);
|
||||
target.StatusEffectQueue.OnTakeDamage(user, ref damageDelta);
|
||||
user.StatusEffectQueue.OnDealDamage(target, ref damageDelta);
|
||||
StatusEffectService.Of(target).OnTakeDamage(user, ref damageDelta);
|
||||
StatusEffectService.Of(user).OnDealDamage(target, ref damageDelta);
|
||||
|
||||
var defenceDelta = 0;
|
||||
var ignoreArmor = user.HasPassiveEffect("IgnoreArmor");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Utils;
|
||||
|
||||
@@ -23,8 +24,7 @@ namespace Cthangover.FFBattle.Actions
|
||||
return new ChangedAttributes { Result = true };
|
||||
|
||||
var turns = action.GetInt("Turn", 3);
|
||||
target.StatusEffectQueue.Add("effect/physics/stun", turns);
|
||||
|
||||
StatusEffectService.Of(target).Add("effect/physics/stun", turns);
|
||||
GameLogger.Log("FF_BATTLE",
|
||||
$"{user.Name} stuns {target.Name} for {turns} turns",
|
||||
LogLevel.Debug);
|
||||
|
||||
+287
-10
@@ -4,10 +4,13 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Cthangover.Core.Battle;
|
||||
using Cthangover.Core.Battle.Actions;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.CharacterPanel;
|
||||
using Cthangover.Core.Utils;
|
||||
using Cthangover.FFBattle.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
@@ -38,6 +41,11 @@ namespace Cthangover.FFBattle
|
||||
private FFPlayerPanel _playerPanel;
|
||||
private FFEnemyPanel _enemyPanel;
|
||||
private FFMenuPanel _menuPanel;
|
||||
private FFActionDetailPanel _detailPanel;
|
||||
private FFBattleHud _hud;
|
||||
private FFBattleLogPanel _logPanel;
|
||||
private FFBattleBanner _banner;
|
||||
private FFBattleTargetHint _targetHint;
|
||||
private FFBattleController _controller;
|
||||
private Control _toolPanel;
|
||||
|
||||
@@ -49,6 +57,12 @@ namespace Cthangover.FFBattle
|
||||
private bool _targetSelectMode;
|
||||
private bool _targetAlly;
|
||||
private System.Action<FFCharacterWidget> _onTargetSelected;
|
||||
private int _round;
|
||||
private int _detailKind;
|
||||
|
||||
private const int DetailMain = 0;
|
||||
private const int DetailActions = 1;
|
||||
private const int DetailItems = 2;
|
||||
|
||||
private const float PLAYER_SCALE = 0.85f;
|
||||
|
||||
@@ -91,6 +105,33 @@ namespace Cthangover.FFBattle
|
||||
_menuPanel.HideMenu();
|
||||
panel.AddChild(_menuPanel);
|
||||
|
||||
_detailPanel = (FFActionDetailPanel)TscnScenes.LoadAndBuild("scenes/ff_action_detail_panel.tscn");
|
||||
_detailPanel.Name = "FFActionDetailPanel";
|
||||
_detailPanel.EnsureConstructed();
|
||||
_detailPanel.HidePassport();
|
||||
panel.AddChild(_detailPanel);
|
||||
|
||||
_hud = (FFBattleHud)TscnScenes.LoadAndBuild("scenes/ff_battle_hud.tscn");
|
||||
_hud.Name = "FFBattleHud";
|
||||
_hud.EnsureConstructed();
|
||||
panel.AddChild(_hud);
|
||||
|
||||
_logPanel = (FFBattleLogPanel)TscnScenes.LoadAndBuild("scenes/ff_battle_log_panel.tscn");
|
||||
_logPanel.Name = "FFBattleLogPanel";
|
||||
_logPanel.EnsureConstructed();
|
||||
panel.AddChild(_logPanel);
|
||||
|
||||
_banner = (FFBattleBanner)TscnScenes.LoadAndBuild("scenes/ff_battle_banner.tscn");
|
||||
_banner.Name = "FFBattleBanner";
|
||||
_banner.EnsureConstructed();
|
||||
panel.AddChild(_banner);
|
||||
|
||||
_targetHint = (FFBattleTargetHint)TscnScenes.LoadAndBuild("scenes/ff_battle_target_hint.tscn");
|
||||
_targetHint.Name = "FFBattleTargetHint";
|
||||
_targetHint.EnsureConstructed();
|
||||
_targetHint.HideHint();
|
||||
panel.AddChild(_targetHint);
|
||||
|
||||
_endTurnButton = new Button();
|
||||
_endTurnButton.Text = TranslationServer.Translate("ff_battle/end_turn");
|
||||
_endTurnButton.Pressed += OnEndTurnPressed;
|
||||
@@ -112,6 +153,7 @@ namespace Cthangover.FFBattle
|
||||
_enemyPanel.OnEnemyClicked += OnEnemyClicked;
|
||||
_enemyPanel.OnEnemyDead += OnEnemyDied;
|
||||
_menuPanel.OnItemSelected += OnMenuItemSelected;
|
||||
_menuPanel.OnSelectionChanged += OnMenuSelectionChanged;
|
||||
_menuPanel.OnCancelled += OnMenuCancel;
|
||||
}
|
||||
|
||||
@@ -138,6 +180,20 @@ namespace Cthangover.FFBattle
|
||||
_menuPanel.Position = new Vector2(viewportSize.X * 0.66f, viewportSize.Y * 0.58f);
|
||||
_menuPanel.Size = new Vector2(viewportSize.X * 0.3f, 160f);
|
||||
|
||||
_detailPanel.Position = new Vector2(viewportSize.X * 0.66f - 320f, viewportSize.Y * 0.58f);
|
||||
_detailPanel.Size = new Vector2(300f, 190f);
|
||||
|
||||
_hud.Position = new Vector2(26f, 20f);
|
||||
|
||||
_logPanel.Position = new Vector2(viewportSize.X - 466f, 64f);
|
||||
_logPanel.Size = new Vector2(440f, 440f);
|
||||
|
||||
_banner.Position = new Vector2(0f, viewportSize.Y * 0.43f);
|
||||
_banner.Size = new Vector2(viewportSize.X, 70f);
|
||||
|
||||
_targetHint.Position = new Vector2(0f, viewportSize.Y * 0.48f);
|
||||
_targetHint.Size = new Vector2(viewportSize.X, 40f);
|
||||
|
||||
if (_endTurnButton != null)
|
||||
_endTurnButton.Position = new Vector2(viewportSize.X - 180f, viewportSize.Y * 0.92f);
|
||||
|
||||
@@ -147,6 +203,9 @@ namespace Cthangover.FFBattle
|
||||
BattleSceneContext.Instance.OnBattleCleared += OnBattleCleared;
|
||||
|
||||
_isPlayerTurn = true;
|
||||
_round = 0;
|
||||
UpdateHud();
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_battle_start"), FFBattleLogKind.Turn);
|
||||
StartPlayerTurn();
|
||||
}
|
||||
|
||||
@@ -160,13 +219,15 @@ namespace Cthangover.FFBattle
|
||||
_currentCharacter = null;
|
||||
_targetSelectMode = false;
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
BattleSceneContext.Instance.ActiveCharacter = null;
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
{
|
||||
if (!widget.IsDead)
|
||||
{
|
||||
widget.Card.StatusEffectQueue.OnTurnStart();
|
||||
StatusEffectService.Of(widget.Card).OnTurnStart();
|
||||
widget.Card.Attributes.Point.Value = widget.Card.Attributes.Point.BaseValue;
|
||||
}
|
||||
widget.UpdateInfo();
|
||||
@@ -180,6 +241,13 @@ namespace Cthangover.FFBattle
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
|
||||
_round += 1;
|
||||
UpdateHud();
|
||||
_hud.SetPhase(true);
|
||||
_banner.Show(Translate("ff_battle/banner_player"));
|
||||
_logPanel.SetRound(_round);
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_player"), _round), FFBattleLogKind.Turn);
|
||||
}
|
||||
|
||||
private void OnWidgetClicked(FFCharacterWidget widget)
|
||||
@@ -201,6 +269,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
return;
|
||||
@@ -250,12 +319,14 @@ namespace Cthangover.FFBattle
|
||||
};
|
||||
|
||||
_currentMenuAction = null;
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailMain;
|
||||
_menuPanel.ShowMenu(entries, string.Format(Translate("ff_battle/menu_caption"),
|
||||
ActorName(), _currentCharacter.Card.Attributes.Point.Value, _currentCharacter.Card.Attributes.Point.BaseValue));
|
||||
_controller.State = FFBattleState.MenuOpen;
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
_currentCharacter?.Highlight(UiTokens.Alpha(UiTokens.Info, 0.6f));
|
||||
_currentCharacter?.Highlight(UiTokens.Accent);
|
||||
}
|
||||
|
||||
private void OnMenuItemSelected(int index)
|
||||
@@ -367,7 +438,8 @@ namespace Cthangover.FFBattle
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailActions;
|
||||
_menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_actions"));
|
||||
_controller.State = FFBattleState.SubMenuOpen;
|
||||
}
|
||||
|
||||
@@ -408,7 +480,8 @@ namespace Cthangover.FFBattle
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
_menuPanel.ShowMenu(entries);
|
||||
_detailKind = DetailItems;
|
||||
_menuPanel.ShowMenu(entries, ActorName() + " · " + Translate("ff_battle/menu_items"));
|
||||
_controller.State = FFBattleState.SubMenuOpen;
|
||||
}
|
||||
|
||||
@@ -418,6 +491,7 @@ namespace Cthangover.FFBattle
|
||||
_targetAlly = targetAlly;
|
||||
_menuPanel.HideMenu();
|
||||
_controller.State = FFBattleState.TargetSelect;
|
||||
_targetHint.ShowHint(Translate(targetAlly ? "ff_battle/hint_ally" : "ff_battle/hint_target"));
|
||||
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
@@ -428,13 +502,13 @@ namespace Cthangover.FFBattle
|
||||
{
|
||||
foreach (var widget in _playerPanel.Widgets)
|
||||
if (!widget.IsDead)
|
||||
widget.Highlight(UiTokens.Alpha(UiTokens.Success, 0.5f));
|
||||
widget.Highlight(UiTokens.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var widget in _enemyPanel.Widgets)
|
||||
if (!widget.IsDead)
|
||||
widget.Highlight(UiTokens.Alpha(UiTokens.Danger, 0.5f));
|
||||
widget.Highlight(UiTokens.Danger);
|
||||
}
|
||||
|
||||
_onTargetSelected = null;
|
||||
@@ -451,6 +525,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
}
|
||||
@@ -469,6 +544,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
|
||||
ExecuteMenuAction(widget);
|
||||
}
|
||||
@@ -480,12 +556,22 @@ namespace Cthangover.FFBattle
|
||||
|
||||
_controller.State = FFBattleState.AnimationPlaying;
|
||||
_endTurnButton.Visible = false;
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
|
||||
var source = _currentCharacter;
|
||||
var actionName = string.Empty;
|
||||
var hpBefore = target.Card.Attributes.Health.Value;
|
||||
var defBefore = target.Card.Attributes.Defence.Value;
|
||||
var stunBefore = CountStunTurns(target.Card);
|
||||
|
||||
switch (_currentMenuAction.Key)
|
||||
{
|
||||
case "action":
|
||||
{
|
||||
var action = _currentMenuAction.Data as ActionCharacter;
|
||||
actionName = TranslationServer.Translate(action.Name).ToString();
|
||||
LogActionDeclared(source, actionName, target);
|
||||
IBattleAction anim = null;
|
||||
|
||||
if (action.Type == ActionCharacterType.ToEnemy)
|
||||
@@ -504,6 +590,8 @@ namespace Cthangover.FFBattle
|
||||
case "item":
|
||||
{
|
||||
var item = _currentMenuAction.Data as IItem;
|
||||
actionName = TranslationServer.Translate(item.Name).ToString();
|
||||
LogActionDeclared(source, actionName, target);
|
||||
var dummyAction = new ActionCharacter { ID = "ff/item" };
|
||||
var anim = new Actions.FFItemAnimation(_currentCharacter, target, dummyAction, item);
|
||||
await RunAnimation(anim);
|
||||
@@ -511,6 +599,8 @@ namespace Cthangover.FFBattle
|
||||
}
|
||||
}
|
||||
|
||||
LogOutcome(source, target, actionName, hpBefore, defBefore, stunBefore);
|
||||
|
||||
_currentMenuAction = null;
|
||||
_endTurnButton.Visible = true;
|
||||
|
||||
@@ -537,12 +627,15 @@ namespace Cthangover.FFBattle
|
||||
private void DoEscapeAction()
|
||||
{
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_controller.State = FFBattleState.Idle;
|
||||
|
||||
var escapeRoll = (float)GD.RandRange(0, 100);
|
||||
if (escapeRoll < 70f)
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", "Escape succeeded", LogLevel.Debug);
|
||||
_banner.Show(Translate("ff_battle/escape_success"));
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_escape_success"), FFBattleLogKind.Turn);
|
||||
_ctx.EndBattle(BattleSide.Player);
|
||||
}
|
||||
else
|
||||
@@ -550,6 +643,8 @@ namespace Cthangover.FFBattle
|
||||
GameLogger.Log("FF_BATTLE", "Escape failed", LogLevel.Debug);
|
||||
_currentCharacter.Card.Attributes.Point.Value -= 1;
|
||||
_currentCharacter.UpdateInfo();
|
||||
_banner.Show(Translate("ff_battle/escape_fail"));
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_escape_failed"), NameOf(_currentCharacter)), FFBattleLogKind.Status);
|
||||
_currentCharacter = null;
|
||||
BattleSceneContext.Instance.ActiveCharacter = null;
|
||||
TryNextCharacterOrEndTurn();
|
||||
@@ -588,6 +683,7 @@ namespace Cthangover.FFBattle
|
||||
_targetSelectMode = false;
|
||||
foreach (var w in _enemyPanel.Widgets) w.ClearHighlight();
|
||||
foreach (var w in _playerPanel.Widgets) w.ClearHighlight();
|
||||
_targetHint.HideHint();
|
||||
ShowMainMenu();
|
||||
return;
|
||||
}
|
||||
@@ -599,6 +695,7 @@ namespace Cthangover.FFBattle
|
||||
}
|
||||
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_controller.State = FFBattleState.Idle;
|
||||
_currentCharacter = null;
|
||||
_currentMenuAction = null;
|
||||
@@ -635,11 +732,12 @@ namespace Cthangover.FFBattle
|
||||
if (widget.IsDead || widget.Card?.Attributes?.Point?.Value <= 0)
|
||||
continue;
|
||||
|
||||
if (widget.Card.StatusEffectQueue.HasStun())
|
||||
if (StatusEffectService.Of(widget.Card).HasStun())
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", $"{widget.Card.Name} is stunned, skipping", LogLevel.Debug);
|
||||
widget.Card.Attributes.Point.Value = 0;
|
||||
widget.UpdateInfo();
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(widget)), FFBattleLogKind.Status);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -649,6 +747,7 @@ namespace Cthangover.FFBattle
|
||||
return;
|
||||
}
|
||||
|
||||
_logPanel.AddEntry(Translate("ff_battle/log_no_points"), FFBattleLogKind.Info);
|
||||
OnPlayerTurnEnd();
|
||||
}
|
||||
|
||||
@@ -668,6 +767,8 @@ namespace Cthangover.FFBattle
|
||||
_isPlayerTurn = false;
|
||||
_endTurnButton.Visible = false;
|
||||
_menuPanel.HideMenu();
|
||||
_detailPanel.HidePassport();
|
||||
_targetHint.HideHint();
|
||||
_controller.State = FFBattleState.AnimationPlaying;
|
||||
BattleSceneContext.Instance.IsWait = true;
|
||||
|
||||
@@ -676,6 +777,10 @@ namespace Cthangover.FFBattle
|
||||
foreach (var widget in _enemyPanel.Widgets)
|
||||
widget.ClearHighlight();
|
||||
|
||||
_hud.SetPhase(false);
|
||||
_banner.Show(Translate("ff_battle/banner_enemy"));
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_turn_enemy"), _round), FFBattleLogKind.Turn);
|
||||
|
||||
var root = (Node)_ctx.RootNode;
|
||||
await root.ToSignal(root.GetTree(), "process_frame");
|
||||
await root.ToSignal(root.GetTree().CreateTimer(0.5f), "timeout");
|
||||
@@ -694,11 +799,13 @@ namespace Cthangover.FFBattle
|
||||
if (enemyWidget.IsDead || enemyWidget.Card?.Actions == null || enemyWidget.Card.Actions.Count == 0)
|
||||
continue;
|
||||
|
||||
enemyWidget.Card.StatusEffectQueue.OnTurnStart();
|
||||
StatusEffectService.Of(enemyWidget.Card).OnTurnStart();
|
||||
enemyWidget.UpdateInfo();
|
||||
|
||||
if (enemyWidget.Card.StatusEffectQueue.HasStun())
|
||||
if (StatusEffectService.Of(enemyWidget.Card).HasStun())
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", $"{enemyWidget.Card.Name} is stunned, skipping", LogLevel.Debug);
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun_skip"), NameOf(enemyWidget)), FFBattleLogKind.Status);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -716,6 +823,12 @@ namespace Cthangover.FFBattle
|
||||
$"Enemy: {enemyWidget.Card.Name} → {action.Name} → {target.Card.Name}",
|
||||
LogLevel.Debug);
|
||||
|
||||
var actionName = TranslationServer.Translate(action.Name).ToString();
|
||||
LogActionDeclared(enemyWidget, actionName, target);
|
||||
var hpBefore = target.Card.Attributes.Health.Value;
|
||||
var defBefore = target.Card.Attributes.Defence.Value;
|
||||
var stunBefore = CountStunTurns(target.Card);
|
||||
|
||||
var anim = CreateEnemyAnimation(enemyWidget, target, action);
|
||||
if (anim != null)
|
||||
{
|
||||
@@ -725,6 +838,8 @@ namespace Cthangover.FFBattle
|
||||
anim.DoEnd();
|
||||
}
|
||||
|
||||
LogOutcome(enemyWidget, target, actionName, hpBefore, defBefore, stunBefore);
|
||||
|
||||
await root.ToSignal(root.GetTree().CreateTimer(0.2f), "timeout");
|
||||
|
||||
if (!_playerPanel.HasAlive())
|
||||
@@ -803,6 +918,8 @@ namespace Cthangover.FFBattle
|
||||
ctx.NotifyCharacterDied(widget.Card);
|
||||
}
|
||||
|
||||
UpdateHudCounts();
|
||||
|
||||
if (!_enemyPanel.HasAlive())
|
||||
{
|
||||
GameLogger.Log("FF_BATTLE", "All enemies dead — player wins", LogLevel.Debug);
|
||||
@@ -852,10 +969,170 @@ namespace Cthangover.FFBattle
|
||||
_playerPanel?.ClearAll();
|
||||
_enemyPanel?.ClearAll();
|
||||
_menuPanel?.HideMenu();
|
||||
_detailPanel?.HidePassport();
|
||||
_targetHint?.HideHint();
|
||||
_logPanel?.Clear();
|
||||
if (_endTurnButton != null)
|
||||
_endTurnButton.Visible = false;
|
||||
}
|
||||
|
||||
private void OnMenuSelectionChanged(int index)
|
||||
{
|
||||
if (_menuPanel.Entries == null || index < 0 || index >= _menuPanel.Entries.Count)
|
||||
{
|
||||
_detailPanel.HidePassport();
|
||||
return;
|
||||
}
|
||||
|
||||
var entry = _menuPanel.Entries[index];
|
||||
_detailPanel.ShowPassport();
|
||||
|
||||
if (_detailKind == DetailActions)
|
||||
{
|
||||
if (entry.Data is ActionCharacter action)
|
||||
_detailPanel.ShowAction(action, BuildActionNote(action));
|
||||
else
|
||||
_detailPanel.ShowHint(Translate("ff_battle/detail_back"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_detailKind == DetailItems)
|
||||
{
|
||||
if (entry.Data is IItem item)
|
||||
_detailPanel.ShowItem(item, item.ItemType.HasFlag(ItemType.TargetUsed), BuildItemNote(item));
|
||||
else
|
||||
_detailPanel.ShowHint(Translate("ff_battle/detail_back"));
|
||||
return;
|
||||
}
|
||||
|
||||
_detailPanel.ShowHint(MainHintFor(entry.Key));
|
||||
}
|
||||
|
||||
private string BuildActionNote(ActionCharacter action)
|
||||
{
|
||||
if (action.Properties == null)
|
||||
return TranslationServer.Translate("ff_battle/detail_action").ToString();
|
||||
|
||||
var cost = action.GetInt("RequiredPoint", 1);
|
||||
var attack = action.GetFloat("Attack", 0f);
|
||||
if (attack > 0f)
|
||||
{
|
||||
var attacker = _currentCharacter?.Card?.Attributes?.Attack?.Value ?? 0;
|
||||
return string.Format(Translate("ff_battle/detail_damage"),
|
||||
Mathf.RoundToInt(attack * attacker), attacker, attack.ToString("0.0"), cost);
|
||||
}
|
||||
|
||||
var turns = action.GetInt("Turn", 0);
|
||||
if (turns > 0)
|
||||
return string.Format(Translate("ff_battle/detail_stun"), turns, cost);
|
||||
|
||||
var defence = action.GetInt("Defence", 0);
|
||||
if (defence > 0)
|
||||
return string.Format(Translate("ff_battle/detail_defence"), defence, cost);
|
||||
|
||||
var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(action.Description));
|
||||
return !string.IsNullOrEmpty(description) ? description : Translate("ff_battle/detail_action");
|
||||
}
|
||||
|
||||
private static string BuildItemNote(IItem item)
|
||||
{
|
||||
var description = ActionCharacterUi.StripMarkup(ActionCharacterUi.Translate(item.Description));
|
||||
return !string.IsNullOrEmpty(description)
|
||||
? description
|
||||
: TranslationServer.Translate("ff_battle/detail_item").ToString();
|
||||
}
|
||||
|
||||
private static string MainHintFor(string key)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "attack": return TranslationServer.Translate("ff_battle/detail_attack").ToString();
|
||||
case "items": return TranslationServer.Translate("ff_battle/detail_items").ToString();
|
||||
case "defend": return TranslationServer.Translate("ff_battle/detail_defend").ToString();
|
||||
case "escape": return TranslationServer.Translate("ff_battle/detail_escape").ToString();
|
||||
default: return TranslationServer.Translate("ff_battle/detail_back").ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private void LogActionDeclared(FFCharacterWidget source, string actionName, FFCharacterWidget target)
|
||||
{
|
||||
var entry = source == target
|
||||
? string.Format(Translate("ff_battle/log_action_self"), NameOf(source), actionName)
|
||||
: string.Format(Translate("ff_battle/log_action"), NameOf(source), actionName, NameOf(target));
|
||||
_logPanel.AddEntry(entry, FFBattleLogKind.Info);
|
||||
}
|
||||
|
||||
private void LogOutcome(FFCharacterWidget source, FFCharacterWidget target, string actionName,
|
||||
int hpBefore, int defBefore, int stunBefore)
|
||||
{
|
||||
var hpAfter = target.Card.Attributes.Health.Value;
|
||||
var defAfter = target.Card.Attributes.Defence.Value;
|
||||
var stunAfter = CountStunTurns(target.Card);
|
||||
|
||||
if (defAfter > defBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_defence"),
|
||||
NameOf(target), defAfter - defBefore, defAfter), FFBattleLogKind.Buff);
|
||||
|
||||
var shieldLost = defBefore - defAfter;
|
||||
if (shieldLost > 0)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_shield"),
|
||||
NameOf(target), shieldLost, defAfter), FFBattleLogKind.Shield);
|
||||
|
||||
if (stunAfter > stunBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_stun"),
|
||||
NameOf(target), stunAfter), FFBattleLogKind.Status);
|
||||
|
||||
if (hpAfter > hpBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_heal"),
|
||||
actionName, NameOf(target), hpAfter - hpBefore), FFBattleLogKind.Buff);
|
||||
|
||||
if (hpAfter < hpBefore)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_damage"),
|
||||
actionName, NameOf(source), NameOf(target), hpBefore - hpAfter), FFBattleLogKind.Damage);
|
||||
|
||||
if (hpAfter <= 0 && hpBefore > 0)
|
||||
_logPanel.AddEntry(string.Format(Translate("ff_battle/log_death"), NameOf(target)), FFBattleLogKind.Dead);
|
||||
|
||||
UpdateHudCounts();
|
||||
}
|
||||
|
||||
private static int CountStunTurns(Character character)
|
||||
{
|
||||
var stun = StatusEffectService.Of(character)?.ActiveStatusEffects?
|
||||
.FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun);
|
||||
return stun?.RemainingTurns ?? 0;
|
||||
}
|
||||
|
||||
private string ActorName()
|
||||
=> _currentCharacter?.Card == null
|
||||
? string.Empty
|
||||
: TranslationServer.Translate(_currentCharacter.Card.Name).ToString();
|
||||
|
||||
private static string NameOf(FFCharacterWidget widget)
|
||||
=> widget?.Card == null
|
||||
? string.Empty
|
||||
: TranslationServer.Translate(widget.Card.Name).ToString();
|
||||
|
||||
private void UpdateHud()
|
||||
{
|
||||
if (_hud == null)
|
||||
return;
|
||||
_hud.SetRound(_round);
|
||||
UpdateHudCounts();
|
||||
}
|
||||
|
||||
private void UpdateHudCounts()
|
||||
{
|
||||
if (_hud == null)
|
||||
return;
|
||||
_hud.SetCounts(
|
||||
_enemyPanel.Widgets.Count(w => !w.IsDead),
|
||||
_playerPanel.Widgets.Count(w => !w.IsDead));
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
|
||||
/// <summary>
|
||||
/// <c>true</c> while the core accepts a player turn advance; flips to
|
||||
/// <c>false</c> synchronously at the start of <see cref="OnPlayerTurnEnd"/>.
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.CharacterPanel;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Action/item passport shown to the left of the command menu. It mirrors the
|
||||
/// action-card component of the unified style guide: a type accent stripe, the
|
||||
/// action icon, name and ID, type/cost/cooldown badges, property chips and a
|
||||
/// note line with the computed effect preview. The panel exists so the small
|
||||
/// FF-style menu can stay a compact list while the player still sees what the
|
||||
/// highlighted command actually does. Node structure lives in
|
||||
/// <c>scenes/ff_action_detail_panel.tscn</c>; the class fills it and reuses
|
||||
/// <see cref="ActionCharacterUi"/> for badges and properties so the battle and
|
||||
/// the character panel can never disagree about an action's presentation.
|
||||
/// </summary>
|
||||
public partial class FFActionDetailPanel : ModWidget
|
||||
{
|
||||
private PanelContainer _card;
|
||||
private ColorRect _stripe;
|
||||
private TextureRect _icon;
|
||||
private Label _nameLabel;
|
||||
private Label _idLabel;
|
||||
private HFlowContainer _badges;
|
||||
private HFlowContainer _props;
|
||||
private PanelContainer _note;
|
||||
private Label _noteLabel;
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_card = GetNode<PanelContainer>("Box/Card");
|
||||
_stripe = GetNode<ColorRect>("Box/Card/CardRow/Stripe");
|
||||
_icon = GetNode<TextureRect>("Box/Card/CardRow/IconMargin/CardIcon");
|
||||
_nameLabel = GetNode<Label>("Box/Card/CardRow/Body/NameRow/NameLabel");
|
||||
_idLabel = GetNode<Label>("Box/Card/CardRow/Body/NameRow/IdLabel");
|
||||
_badges = GetNode<HFlowContainer>("Box/Card/CardRow/Body/Badges");
|
||||
_props = GetNode<HFlowContainer>("Box/Card/CardRow/Body/Props");
|
||||
_note = GetNode<PanelContainer>("Box/Note");
|
||||
_noteLabel = GetNode<Label>("Box/Note/NoteLabel");
|
||||
|
||||
var cardStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface1, 0.92f), UiTokens.LineCard);
|
||||
cardStyle.ContentMarginLeft = 0;
|
||||
cardStyle.ContentMarginRight = 0;
|
||||
cardStyle.ContentMarginTop = 0;
|
||||
cardStyle.ContentMarginBottom = 0;
|
||||
_card.AddThemeStyleboxOverride("panel", cardStyle);
|
||||
|
||||
var noteStyle = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Alpha(UiTokens.Surface2, 0.9f), UiTokens.Line);
|
||||
noteStyle.ContentMarginLeft = 12;
|
||||
noteStyle.ContentMarginRight = 12;
|
||||
noteStyle.ContentMarginTop = 9;
|
||||
noteStyle.ContentMarginBottom = 9;
|
||||
_note.AddThemeStyleboxOverride("panel", noteStyle);
|
||||
|
||||
_nameLabel.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
_idLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
_noteLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills the card with an action passport. <paramref name="note"/> is a
|
||||
/// ready localised effect preview built by the core, because only the core
|
||||
/// knows the acting character's attack value for the damage estimate.
|
||||
/// </summary>
|
||||
public void ShowAction(ActionCharacter action, string note)
|
||||
{
|
||||
if (action == null)
|
||||
{
|
||||
ShowHint(note);
|
||||
return;
|
||||
}
|
||||
|
||||
_stripe.Color = ActionCharacterUi.AccentColor(action.Type);
|
||||
_icon.Texture = action.Image;
|
||||
_nameLabel.Text = ActionCharacterUi.Translate(action.Name);
|
||||
_idLabel.Text = action.ID ?? string.Empty;
|
||||
_card.Visible = true;
|
||||
|
||||
ResetBody();
|
||||
|
||||
AddBadge(ActionCharacterUi.Translate(ActionCharacterUi.TypeLabelKey(action.Type)), ActionCharacterUi.AccentColor(action.Type));
|
||||
|
||||
var costText = ActionCharacterUi.CostText(action);
|
||||
if (!string.IsNullOrEmpty(costText))
|
||||
AddBadge(costText, UiTokens.TypeResource);
|
||||
|
||||
var turns = action.GetInt("Turn", 0);
|
||||
if (turns > 0)
|
||||
AddBadge(string.Format(Translate("ff_battle/badge_turns"), turns), UiTokens.Cooldown);
|
||||
|
||||
foreach (var pair in ActionCharacterUi.SortedProperties(action))
|
||||
{
|
||||
if (pair.Key == "RequiredPoint")
|
||||
continue;
|
||||
_props.AddChild(ActionCharacterUi.MakePropertyChip(pair.Key, pair.Value));
|
||||
}
|
||||
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>Fills the card with an inventory item passport for the item sub-menu.</summary>
|
||||
public void ShowItem(IItem item, bool targetAlly, string note)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
ShowHint(note);
|
||||
return;
|
||||
}
|
||||
|
||||
_stripe.Color = UiTokens.TypeUsed;
|
||||
_icon.Texture = item.Sprite;
|
||||
_nameLabel.Text = ActionCharacterUi.Translate(item.Name);
|
||||
_idLabel.Text = item.ID ?? string.Empty;
|
||||
_card.Visible = true;
|
||||
|
||||
ResetBody();
|
||||
|
||||
if (item.ItemType.HasFlag(ItemType.Used))
|
||||
AddBadge(ActionCharacterUi.Translate("ui/inventory/type_used"), UiTokens.TypeUsed);
|
||||
AddBadge(ActionCharacterUi.Translate(targetAlly ? "ui/inventory/type_target" : "ff_battle/badge_self"),
|
||||
targetAlly ? UiTokens.TypeTarget : UiTokens.TypeSelf);
|
||||
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>Shows a plain directive note under the card, used for highlighted menu hints.</summary>
|
||||
public void ShowHint(string note)
|
||||
{
|
||||
ResetBody();
|
||||
_card.Visible = false;
|
||||
SetNote(note);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the whole passport; the core calls it when the menu closes. The
|
||||
/// inherited <see cref="CanvasItem.Hide"/> semantics are enough, so the
|
||||
/// method is only a named alias for readability at call sites.
|
||||
/// </summary>
|
||||
public void HidePassport()
|
||||
=> Visible = false;
|
||||
|
||||
/// <summary>Shows the panel; the core calls it before filling any content.</summary>
|
||||
public void ShowPassport()
|
||||
=> Visible = true;
|
||||
|
||||
private void ResetBody()
|
||||
{
|
||||
foreach (var child in _badges.GetChildren())
|
||||
{
|
||||
_badges.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
foreach (var child in _props.GetChildren())
|
||||
{
|
||||
_props.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddBadge(string text, Color color)
|
||||
=> _badges.AddChild(ActionCharacterUi.MakeBadge(text, color));
|
||||
|
||||
private void SetNote(string note)
|
||||
{
|
||||
var hasNote = !string.IsNullOrEmpty(note);
|
||||
_note.Visible = hasNote;
|
||||
_noteLabel.Text = note ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Centered announcement strip ("Player turn", "Enemy turn", escape results).
|
||||
/// The node tree lives in <c>scenes/ff_battle_banner.tscn</c>; this class
|
||||
/// dresses it with tokens and owns the fade in/hold/fade out tween. The banner
|
||||
/// is purely informational, so it ignores the mouse and the core places it on
|
||||
/// its own anchored strip between the enemy grid and the party row.
|
||||
/// </summary>
|
||||
public partial class FFBattleBanner : ModWidget
|
||||
{
|
||||
private Label _label;
|
||||
private Tween _tween;
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_label = GetNode<Label>("Center/Panel/Label");
|
||||
_label.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
|
||||
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;
|
||||
GetNode<PanelContainer>("Center/Panel").AddThemeStyleboxOverride("panel", style);
|
||||
|
||||
Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
}
|
||||
|
||||
/// <summary>Shows the given caption and restarts the 1.6s fade cycle.</summary>
|
||||
public void Show(string text)
|
||||
{
|
||||
_label.Text = text;
|
||||
_tween?.Kill();
|
||||
Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
|
||||
_tween = CreateTween();
|
||||
_tween.TweenProperty(this, "modulate:a", 1f, 0.2f);
|
||||
_tween.TweenInterval(1.1f);
|
||||
_tween.TweenProperty(this, "modulate:a", 0f, 0.3f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Top-left battle HUD: title, phase badge and the round / roster counters.
|
||||
/// The node tree comes from <c>scenes/ff_battle_hud.tscn</c>; this class only
|
||||
/// fetches nodes, dresses them with <see cref="UiTokens"/> styles and exposes
|
||||
/// narrow setters so <see cref="FFBattleCore"/> never assembles controls by
|
||||
/// hand. The phase badge reuses the type-accent palette: blue for the player
|
||||
/// turn, red for the enemy turn.
|
||||
/// </summary>
|
||||
public partial class FFBattleHud : ModWidget
|
||||
{
|
||||
private Label _title;
|
||||
private PanelContainer _phaseBadge;
|
||||
private Label _phaseLabel;
|
||||
private Label _roundValue;
|
||||
private Label _enemiesValue;
|
||||
private Label _alliesValue;
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_title = GetNode<Label>("Panel/Column/TopRow/TitleLabel");
|
||||
_phaseBadge = GetNode<PanelContainer>("Panel/Column/TopRow/PhaseBadge");
|
||||
_phaseLabel = GetNode<Label>("Panel/Column/TopRow/PhaseBadge/PhaseLabel");
|
||||
_roundValue = GetNode<Label>("Panel/Column/MetaRow/RoundValue");
|
||||
_enemiesValue = GetNode<Label>("Panel/Column/MetaRow/EnemiesValue");
|
||||
_alliesValue = GetNode<Label>("Panel/Column/MetaRow/AlliesValue");
|
||||
|
||||
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);
|
||||
|
||||
_title.AddThemeColorOverride("font_color", UiTokens.InkBright);
|
||||
|
||||
foreach (var name in new[] { "RoundCaption", "EnemiesCaption", "AlliesCaption" })
|
||||
GetNode<Label>("Panel/Column/MetaRow/" + name).AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
|
||||
foreach (var name in new[] { "RoundValue", "EnemiesValue", "AlliesValue" })
|
||||
GetNode<Label>("Panel/Column/MetaRow/" + name).AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
|
||||
ApplyPhaseStyle(true);
|
||||
}
|
||||
|
||||
/// <summary>Switches the phase badge between the player and enemy turns.</summary>
|
||||
public void SetPhase(bool playerTurn)
|
||||
{
|
||||
_phaseLabel.Text = Translate(playerTurn ? "ff_battle/phase_player" : "ff_battle/phase_enemy");
|
||||
ApplyPhaseStyle(playerTurn);
|
||||
}
|
||||
|
||||
/// <summary>Updates the round counter.</summary>
|
||||
public void SetRound(int round)
|
||||
=> _roundValue.Text = round.ToString();
|
||||
|
||||
/// <summary>Updates living combatant counters.</summary>
|
||||
public void SetCounts(int enemies, int allies)
|
||||
{
|
||||
_enemiesValue.Text = enemies.ToString();
|
||||
_alliesValue.Text = allies.ToString();
|
||||
}
|
||||
|
||||
private void ApplyPhaseStyle(bool playerTurn)
|
||||
{
|
||||
var accent = playerTurn ? UiTokens.TypeSelf : 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);
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Semantic categories of <see cref="FFBattleLogPanel"/> entries. The kind
|
||||
/// selects the row colour and the left border accent: turn separators are
|
||||
/// gold, damage red, absorbed shield blue, buffs green, statuses amber and
|
||||
/// deaths grey. <see cref="Info"/> is the neutral narration fallback.
|
||||
/// </summary>
|
||||
public enum FFBattleLogKind
|
||||
{
|
||||
Info,
|
||||
Turn,
|
||||
Damage,
|
||||
Shield,
|
||||
Buff,
|
||||
Status,
|
||||
Dead
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Battle journal docked to the upper right: a window with a header carrying
|
||||
/// the round counter and a scrollable list of colour-coded rows. The panel is
|
||||
/// a proposal on top of the bare battle screen — it gives damage, shield
|
||||
/// absorption, status and death events a readable history that the original
|
||||
/// FF-style UI lacked. Node structure lives in
|
||||
/// <c>scenes/ff_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.
|
||||
/// </summary>
|
||||
public partial class FFBattleLogPanel : 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.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_roundLabel.AddThemeColorOverride("font_color", UiTokens.Faint);
|
||||
}
|
||||
|
||||
/// <summary>Updates the round caption in the header.</summary>
|
||||
public void SetRound(int round)
|
||||
=> _roundLabel.Text = string.Format(Translate("ff_battle/log_round"), round);
|
||||
|
||||
/// <summary>Drops every row; used 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 coloured row and trims the oldest entries past the cap.
|
||||
/// The scroll is pushed to the bottom so the newest event is always visible.
|
||||
/// </summary>
|
||||
public void AddEntry(string text, FFBattleLogKind kind)
|
||||
{
|
||||
var style = UiKit.Frame(0, BackgroundOf(kind), new Color(0f, 0f, 0f, 0f));
|
||||
style.BorderWidthLeft = 2;
|
||||
style.BorderColor = BorderOf(kind);
|
||||
style.ContentMarginLeft = 16;
|
||||
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(FFBattleLogKind kind)
|
||||
=> kind == FFBattleLogKind.Turn ? UiTokens.AccentSoft : new Color(0f, 0f, 0f, 0f);
|
||||
|
||||
private static Color BorderOf(FFBattleLogKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case FFBattleLogKind.Turn: return UiTokens.Accent;
|
||||
case FFBattleLogKind.Damage: return UiTokens.Danger;
|
||||
case FFBattleLogKind.Shield: return UiTokens.TypeResource;
|
||||
case FFBattleLogKind.Buff: return UiTokens.TypePassive;
|
||||
case FFBattleLogKind.Status: return UiTokens.Cooldown;
|
||||
case FFBattleLogKind.Dead: return UiTokens.LineFaint;
|
||||
default: return UiTokens.Line;
|
||||
}
|
||||
}
|
||||
|
||||
private static Color TextOf(FFBattleLogKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case FFBattleLogKind.Turn: return UiTokens.AccentBright;
|
||||
case FFBattleLogKind.Damage: return UiTokens.DangerText;
|
||||
case FFBattleLogKind.Shield: return UiTokens.Info;
|
||||
case FFBattleLogKind.Buff: return UiTokens.SuccessText;
|
||||
case FFBattleLogKind.Status: return UiTokens.Cooldown;
|
||||
case FFBattleLogKind.Dead: return UiTokens.Faint;
|
||||
default: return UiTokens.Muted;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Toast-style directive shown while the player picks a battle target
|
||||
/// ("Select a target · RMB / Esc — cancel"). It is a deliberate addition over
|
||||
/// the bare FF UI: the original mod highlighted widgets without ever telling
|
||||
/// the player what the highlight meant, which is easy to miss on a large enemy
|
||||
/// grid. The node tree lives in <c>scenes/ff_battle_target_hint.tscn</c>; the
|
||||
/// class dresses it with tokens and toggles visibility.
|
||||
/// </summary>
|
||||
public partial class FFBattleTargetHint : ModWidget
|
||||
{
|
||||
private Label _icon;
|
||||
private Label _text;
|
||||
|
||||
protected override void Construct()
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_icon = GetNode<Label>("Center/Toast/Row/IconLabel");
|
||||
_text = GetNode<Label>("Center/Toast/Row/TextLabel");
|
||||
_icon.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
_text.AddThemeColorOverride("font_color", UiTokens.Ink);
|
||||
|
||||
var style = UiKit.Frame(UiTokens.RadiusMd, UiTokens.ToastBg, UiTokens.Line);
|
||||
style.ContentMarginLeft = 14;
|
||||
style.ContentMarginRight = 14;
|
||||
style.ContentMarginTop = 10;
|
||||
style.ContentMarginBottom = 10;
|
||||
style.ShadowColor = new Color(0f, 0f, 0f, 0.5f);
|
||||
style.ShadowSize = 10;
|
||||
style.ShadowOffset = new Vector2(0f, 4f);
|
||||
GetNode<PanelContainer>("Center/Toast").AddThemeStyleboxOverride("panel", style);
|
||||
}
|
||||
|
||||
/// <summary>Shows the hint with the given caption.</summary>
|
||||
public void ShowHint(string text)
|
||||
{
|
||||
_text.Text = text;
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
/// <summary>Hides the hint; called when a target is confirmed or selection is cancelled.</summary>
|
||||
public void HideHint()
|
||||
=> Visible = false;
|
||||
}
|
||||
}
|
||||
+230
-75
@@ -1,7 +1,8 @@
|
||||
using System;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Cards.StatusEffect;
|
||||
using Cthangover.Core.Characters;
|
||||
using Cthangover.Core.Mods.Resolvers;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Theming;
|
||||
using Godot;
|
||||
@@ -9,23 +10,44 @@ using Godot;
|
||||
namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Visual representation of a single character (player or enemy) in the FF battle UI.
|
||||
/// Composite widget containing a sprite, HP bar, name label, and selection overlay.
|
||||
/// Supports highlight, flash, shake, and dissolve-death animations via Godot tweens.
|
||||
/// Created per-character by <see cref="FFPlayerPanel.Init"/> and
|
||||
/// <see cref="FFEnemyPanel.Init"/>. Clickable via <c>GuiInput</c> events;
|
||||
/// embedding panels wire these to <see cref="FFBattleCore"/> for action flow.
|
||||
/// Battle card of a single combatant, shared by the party and enemy panels.
|
||||
/// The node tree lives in <c>scenes/ff_character_widget.tscn</c>; this class
|
||||
/// only fetches the nodes, dresses them with <see cref="UiTokens"/> styles and
|
||||
/// refreshes their content. Visually the card follows the unified style guide:
|
||||
/// a team accent line on top, a sprite well, status badges in the upper right
|
||||
/// and an info block holding name, HP numbers, HP bar, action point pips
|
||||
/// (party) or attack caption (enemies) and the defence shield chip.
|
||||
/// <para/>
|
||||
/// The public surface is animation-facing, not layout-facing:
|
||||
/// <see cref="Highlight"/> paints the selection frame, <see cref="Flash"/> and
|
||||
/// <see cref="Shake"/> give impact feedback, <see cref="PlayDeathAnimation"/>
|
||||
/// dissolves the card. Battle code never touches labels directly — it mutates
|
||||
/// the <see cref="Character"/> and calls <see cref="UpdateInfo"/>, which
|
||||
/// re-reads health, defence, points and stun in one place. Panels must assign
|
||||
/// <see cref="IsPlayer"/> before <see cref="Init"/>, because the team colour is
|
||||
/// applied during initialisation.
|
||||
/// </summary>
|
||||
public partial class FFCharacterWidget : ModWidget
|
||||
{
|
||||
private TextureRect _sprite;
|
||||
private TextureRect _selection;
|
||||
/// <summary>Diameter of one action point pip.</summary>
|
||||
private const int PipSize = 9;
|
||||
|
||||
private TextureRect _art;
|
||||
private ColorRect _teamLine;
|
||||
private Panel _selection;
|
||||
private Panel _teamDot;
|
||||
private Label _nameLabel;
|
||||
private ColorRect _hpBg;
|
||||
private ColorRect _hpFill;
|
||||
private Label _hpNumLabel;
|
||||
private ProgressBar _hpBar;
|
||||
private HBoxContainer _pipsBox;
|
||||
private Label _atkLabel;
|
||||
private PanelContainer _shieldChip;
|
||||
private Label _shieldValue;
|
||||
private VBoxContainer _statusBox;
|
||||
private PanelContainer _stunBadge;
|
||||
private TextureRect _stunIcon;
|
||||
private Label _stunLabel;
|
||||
private Tween _animTween;
|
||||
private static Texture2D _cachedSelectTex;
|
||||
private static bool _selectTexLoaded;
|
||||
private static Shader _dissolveShader;
|
||||
|
||||
/// <summary>Whether this widget belongs to the player party (affects highlight/targeting logic).</summary>
|
||||
@@ -34,7 +56,7 @@ namespace Cthangover.FFBattle.UI
|
||||
public bool IsDead { get; set; }
|
||||
/// <summary>The character data model backing this widget: stats, actions, status effects.</summary>
|
||||
public Character Card { get; set; }
|
||||
/// <summary>Original scale captured at construction; used to restore after death animation.</summary>
|
||||
/// <summary>Original scale captured at initialisation; used to restore after death animation.</summary>
|
||||
public Vector2 BaseScale { get; private set; }
|
||||
/// <summary>Exposes this widget as a <see cref="Godot.Control"/> for layout calculations.</summary>
|
||||
public Control ControlNode => this;
|
||||
@@ -43,33 +65,29 @@ namespace Cthangover.FFBattle.UI
|
||||
{
|
||||
CthangoverTheme.Apply(this);
|
||||
|
||||
_sprite = GetNode<TextureRect>("Sprite");
|
||||
_hpBg = GetNode<ColorRect>("HpBg");
|
||||
_hpFill = GetNode<ColorRect>("HpFill");
|
||||
_nameLabel = GetNode<Label>("NameLabel");
|
||||
_selection = GetNode<TextureRect>("Selection");
|
||||
_teamLine = GetNode<ColorRect>("TeamLine");
|
||||
_art = GetNode<TextureRect>("Art");
|
||||
_nameLabel = GetNode<Label>("Info/Column/NameRow/NameLabel");
|
||||
_teamDot = GetNode<Panel>("Info/Column/NameRow/TeamDot");
|
||||
_hpNumLabel = GetNode<Label>("Info/Column/NameRow/HpNumLabel");
|
||||
_hpBar = GetNode<ProgressBar>("Info/Column/HpBar");
|
||||
_pipsBox = GetNode<HBoxContainer>("Info/Column/Foot/PipsBox");
|
||||
_atkLabel = GetNode<Label>("Info/Column/Foot/AtkLabel");
|
||||
_shieldChip = GetNode<PanelContainer>("Info/Column/Foot/ShieldChip");
|
||||
_shieldValue = GetNode<Label>("Info/Column/Foot/ShieldChip/ShieldRow/ShieldValue");
|
||||
_statusBox = GetNode<VBoxContainer>("StatusBox");
|
||||
_stunBadge = GetNode<PanelContainer>("StatusBox/StunBadge");
|
||||
_stunIcon = GetNode<TextureRect>("StatusBox/StunBadge/StunRow/StunIcon");
|
||||
_stunLabel = GetNode<Label>("StatusBox/StunBadge/StunRow/StunLabel");
|
||||
_selection = GetNode<Panel>("Selection");
|
||||
|
||||
Size = CustomMinimumSize;
|
||||
|
||||
if (!_selectTexLoaded)
|
||||
{
|
||||
_cachedSelectTex = Textures.Resolve("select");
|
||||
_selectTexLoaded = true;
|
||||
}
|
||||
if (_cachedSelectTex != null)
|
||||
{
|
||||
_selection.Texture = _cachedSelectTex;
|
||||
_selection.ExpandMode = TextureRect.ExpandModeEnum.IgnoreSize;
|
||||
_selection.StretchMode = TextureRect.StretchModeEnum.Scale;
|
||||
_selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
}
|
||||
ApplyStyles();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds this widget to a <see cref="Character"/> model. Sets the sprite from
|
||||
/// <see cref="Character.Image"/>, localises the name via
|
||||
/// <see cref="TranslationServer"/>, and calls <see cref="UpdateInfo"/>
|
||||
/// to refresh the HP bar.
|
||||
/// Binds the widget to a <see cref="Character"/>: sprite, localised name,
|
||||
/// team colour and per-side info layout. The team colour is derived from
|
||||
/// <see cref="IsPlayer"/>, which is why callers set it before calling here.
|
||||
/// </summary>
|
||||
public void Init(Character character)
|
||||
{
|
||||
@@ -77,33 +95,72 @@ namespace Cthangover.FFBattle.UI
|
||||
BaseScale = Scale;
|
||||
IsDead = false;
|
||||
|
||||
if (_sprite != null && character.Image != null)
|
||||
_sprite.Texture = character.Image;
|
||||
if (_art != null && character?.Image != null)
|
||||
_art.Texture = character.Image;
|
||||
|
||||
if (_nameLabel != null)
|
||||
_nameLabel.Text = TranslationServer.Translate(character.Name);
|
||||
_nameLabel.Text = TranslationServer.Translate(character?.Name ?? string.Empty).ToString();
|
||||
|
||||
var teamColor = IsPlayer ? UiTokens.Info : UiTokens.Danger;
|
||||
if (_teamLine != null)
|
||||
_teamLine.Color = UiTokens.Alpha(teamColor, 0.55f);
|
||||
if (_teamDot != null)
|
||||
_teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Alpha(teamColor, 0.9f)));
|
||||
if (_pipsBox != null)
|
||||
_pipsBox.Visible = IsPlayer;
|
||||
if (_atkLabel != null)
|
||||
_atkLabel.Visible = !IsPlayer;
|
||||
|
||||
UpdateInfo();
|
||||
}
|
||||
|
||||
/// <summary>Refreshes the HP bar fill width and colour gradient based on current health percentage.</summary>
|
||||
/// <summary>
|
||||
/// Single refresh entry point for all mutable character state: HP bar and
|
||||
/// numbers with threshold colours, action point pips, shield chip, attack
|
||||
/// caption and the stun badge with its remaining turns. Called by battle
|
||||
/// code and by animations after each resolved action.
|
||||
/// </summary>
|
||||
public void UpdateInfo()
|
||||
{
|
||||
if (_hpFill == null || Card == null)
|
||||
if (Card?.Attributes == null)
|
||||
return;
|
||||
|
||||
var hpPercent = Card.Attributes.Health.Percent;
|
||||
_hpFill.Size = new Vector2((Size.X - 8) * hpPercent, 18);
|
||||
var health = Card.Attributes.Health;
|
||||
var percent = Mathf.Clamp(health.Percent, 0f, 1f);
|
||||
var fillColor = percent > 0.5f ? UiTokens.HpOk : percent > 0.25f ? UiTokens.Warning : UiTokens.HpLow;
|
||||
var textColor = percent > 0.5f ? UiTokens.HpText : percent > 0.25f ? UiTokens.Warning : UiTokens.DangerText;
|
||||
|
||||
if (hpPercent > 0.5f)
|
||||
_hpFill.Color = UiTokens.HpOk;
|
||||
else if (hpPercent > 0.25f)
|
||||
_hpFill.Color = UiTokens.Warning;
|
||||
else
|
||||
_hpFill.Color = UiTokens.HpLow;
|
||||
if (_hpBar != null)
|
||||
{
|
||||
_hpBar.Value = percent * 100f;
|
||||
_hpBar.AddThemeStyleboxOverride("background", UiKit.Frame(UiTokens.RadiusSm, UiTokens.LineDisabled));
|
||||
_hpBar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, fillColor));
|
||||
}
|
||||
|
||||
/// <summary>Shows a coloured selection overlay with a 0.15s tween fade-in. Used for target highlighting and selection indication.</summary>
|
||||
if (_hpNumLabel != null)
|
||||
{
|
||||
_hpNumLabel.Text = $"{health.Value}/{health.BaseValue}";
|
||||
_hpNumLabel.AddThemeColorOverride("font_color", textColor);
|
||||
}
|
||||
|
||||
if (_pipsBox != null && IsPlayer)
|
||||
RefreshPips();
|
||||
|
||||
if (_atkLabel != null && !IsPlayer)
|
||||
_atkLabel.Text = string.Format("{0} {1}", Translate("ui/character_panel/stat_atk"), Card.Attributes.Attack.Value);
|
||||
|
||||
if (_shieldChip != null)
|
||||
{
|
||||
var defence = Card.Attributes.Defence?.Value ?? 0;
|
||||
_shieldChip.Visible = defence > 0;
|
||||
if (_shieldValue != null)
|
||||
_shieldValue.Text = defence.ToString();
|
||||
}
|
||||
|
||||
RefreshStun();
|
||||
}
|
||||
|
||||
/// <summary>Shows a coloured selection frame over the card with a 0.15s fade.</summary>
|
||||
public void Highlight(Color color)
|
||||
{
|
||||
if (_selection == null)
|
||||
@@ -115,7 +172,7 @@ namespace Cthangover.FFBattle.UI
|
||||
_animTween.TweenProperty(_selection, "modulate", color, 0.15f);
|
||||
}
|
||||
|
||||
/// <summary>Fades out the selection overlay to transparent via a 0.15s tween.</summary>
|
||||
/// <summary>Fades the selection frame out via a 0.15s tween.</summary>
|
||||
public void ClearHighlight()
|
||||
{
|
||||
if (_selection == null)
|
||||
@@ -127,7 +184,7 @@ namespace Cthangover.FFBattle.UI
|
||||
_animTween.TweenProperty(_selection, "modulate", UiTokens.Alpha(UiTokens.White, 0f), 0.15f);
|
||||
}
|
||||
|
||||
/// <summary>Rapidly oscillates the widget's position with random offsets (6 iterations) to simulate impact feedback.</summary>
|
||||
/// <summary>Rapidly oscillates the widget with random offsets to simulate impact feedback.</summary>
|
||||
public void Shake(float intensity, float duration)
|
||||
{
|
||||
var originalPos = Position;
|
||||
@@ -145,7 +202,7 @@ namespace Cthangover.FFBattle.UI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Briefly tints the entire widget with a colour pulse (30% rise, 70% fall) for damage/heal feedback.</summary>
|
||||
/// <summary>Briefly tints the whole card with a colour pulse (30% rise, 70% fall).</summary>
|
||||
public void Flash(Color color, float duration)
|
||||
{
|
||||
var originalModulate = Modulate;
|
||||
@@ -156,11 +213,11 @@ namespace Cthangover.FFBattle.UI
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a dissolve-and-shrink death sequence. If a <c>"scene_transition"</c>
|
||||
/// shader is available, applies a dissolve material to the sprite. Fades the
|
||||
/// widget alpha, scales it to zero, and hides the HP bar and name label.
|
||||
/// Invokes <paramref name="onComplete"/> when finished so the parent panel
|
||||
/// can remove the widget from the grid.
|
||||
/// Plays a dissolve-and-shrink death sequence. The sprite gets the
|
||||
/// <c>scene_transition</c> shader with a growing dissolve progress while the
|
||||
/// card fades and squashes; the info block fades with it. Invokes
|
||||
/// <paramref name="onComplete"/> when finished so the parent panel can
|
||||
/// remove the widget and re-grid the survivors.
|
||||
/// </summary>
|
||||
public void PlayDeathAnimation(Action onComplete)
|
||||
{
|
||||
@@ -180,21 +237,20 @@ namespace Cthangover.FFBattle.UI
|
||||
if (_dissolveShader == null)
|
||||
_dissolveShader = Shaders.Resolve("scene_transition");
|
||||
|
||||
if (_dissolveShader != null && _sprite?.Texture != null)
|
||||
if (_dissolveShader != null && _art?.Texture != null)
|
||||
{
|
||||
var mat = new ShaderMaterial { Shader = _dissolveShader };
|
||||
mat.SetShaderParameter("noise_scale", 5f);
|
||||
mat.SetShaderParameter("smoothness", 0.25f);
|
||||
mat.SetShaderParameter("distortion", 0.092f);
|
||||
mat.SetShaderParameter("glow_intensity", 0f);
|
||||
mat.SetShaderParameter("hue_shift", 0f);
|
||||
mat.SetShaderParameter("invert_direction", false);
|
||||
mat.SetShaderParameter("progress", 0f);
|
||||
_sprite.Material = mat;
|
||||
var material = new ShaderMaterial { Shader = _dissolveShader };
|
||||
material.SetShaderParameter("noise_scale", 5f);
|
||||
material.SetShaderParameter("smoothness", 0.25f);
|
||||
material.SetShaderParameter("distortion", 0.092f);
|
||||
material.SetShaderParameter("glow_intensity", 0f);
|
||||
material.SetShaderParameter("hue_shift", 0f);
|
||||
material.SetShaderParameter("invert_direction", false);
|
||||
material.SetShaderParameter("progress", 0f);
|
||||
_art.Material = material;
|
||||
|
||||
var matTween = CreateTween();
|
||||
matTween.TweenMethod(
|
||||
Callable.From<float>(t => mat.SetShaderParameter("progress", t)),
|
||||
CreateTween().TweenMethod(
|
||||
Callable.From<float>(t => material.SetShaderParameter("progress", t)),
|
||||
0f, 1f, duration);
|
||||
}
|
||||
|
||||
@@ -202,9 +258,13 @@ namespace Cthangover.FFBattle.UI
|
||||
tween.SetParallel(true);
|
||||
tween.TweenProperty(this, "modulate:a", 0f, duration * 0.5f);
|
||||
tween.TweenProperty(this, "scale", BaseScale * new Vector2(0.1f, 1.2f), duration);
|
||||
tween.TweenProperty(_hpBg, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpFill, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpBar, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_hpNumLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_nameLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_pipsBox, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_atkLabel, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_shieldChip, "modulate:a", 0f, duration);
|
||||
tween.TweenProperty(_statusBox, "modulate:a", 0f, duration);
|
||||
|
||||
tween.Chain().TweenProperty(this, "scale", Vector2.Zero, 0.15f);
|
||||
|
||||
@@ -213,5 +273,100 @@ namespace Cthangover.FFBattle.UI
|
||||
onComplete?.Invoke();
|
||||
};
|
||||
}
|
||||
|
||||
private void ApplyStyles()
|
||||
{
|
||||
_teamLine.Color = UiTokens.Alpha(UiTokens.Danger, 0.55f);
|
||||
|
||||
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);
|
||||
_hpNumLabel.AddThemeColorOverride("font_color", UiTokens.HpText);
|
||||
_atkLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Danger));
|
||||
|
||||
var hpBackground = UiKit.Frame(UiTokens.RadiusSm, UiTokens.LineDisabled);
|
||||
var hpFill = UiKit.Frame(UiTokens.RadiusSm, UiTokens.HpOk);
|
||||
_hpBar.AddThemeStyleboxOverride("background", hpBackground);
|
||||
_hpBar.AddThemeStyleboxOverride("fill", hpFill);
|
||||
|
||||
var shieldStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.StatBg, UiTokens.StatLine);
|
||||
shieldStyle.ContentMarginLeft = 7;
|
||||
shieldStyle.ContentMarginRight = 7;
|
||||
shieldStyle.ContentMarginTop = 2;
|
||||
shieldStyle.ContentMarginBottom = 2;
|
||||
_shieldChip.AddThemeStyleboxOverride("panel", shieldStyle);
|
||||
GetNode<Label>("Info/Column/Foot/ShieldChip/ShieldRow/ShieldKey").AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
_shieldValue.AddThemeColorOverride("font_color", UiTokens.Info);
|
||||
|
||||
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);
|
||||
|
||||
var selectionStyle = UiKit.Frame(UiTokens.RadiusMd, new Color(1f, 1f, 1f, 0.07f), Colors.White, 2);
|
||||
_selection.AddThemeStyleboxOverride("panel", selectionStyle);
|
||||
_selection.Modulate = UiTokens.Alpha(UiTokens.White, 0f);
|
||||
}
|
||||
|
||||
private void RefreshPips()
|
||||
{
|
||||
foreach (var child in _pipsBox.GetChildren())
|
||||
{
|
||||
_pipsBox.RemoveChild(child);
|
||||
child.QueueFree();
|
||||
}
|
||||
|
||||
var basePoints = Mathf.Max(0, Card.Attributes.Point.BaseValue);
|
||||
var points = Card.Attributes.Point.Value;
|
||||
for (var i = 0; i < basePoints; i++)
|
||||
{
|
||||
var on = i < points;
|
||||
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));
|
||||
_pipsBox.AddChild(pip);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshStun()
|
||||
{
|
||||
if (_stunBadge == null)
|
||||
return;
|
||||
|
||||
var stun = StatusEffectService.Of(Card)?.ActiveStatusEffects?
|
||||
.FirstOrDefault(effect => effect.EffectType == StatusEffectType.Stun);
|
||||
|
||||
if (stun == null)
|
||||
{
|
||||
_stunBadge.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_stunBadge.Visible = true;
|
||||
if (_stunIcon != null)
|
||||
_stunIcon.Texture = stun.Icon;
|
||||
if (_stunLabel != null)
|
||||
_stunLabel.Text = string.Format(Translate("ff_battle/status_stun"), stun.RemainingTurns);
|
||||
}
|
||||
|
||||
private static string Translate(string key)
|
||||
=> TranslationServer.Translate(key).ToString();
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ namespace Cthangover.FFBattle.UI
|
||||
var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn");
|
||||
widget.EnsureConstructed();
|
||||
widget.Scale = new Vector2(scale, scale);
|
||||
widget.Init(enemies[i]);
|
||||
widget.IsPlayer = false;
|
||||
widget.Init(enemies[i]);
|
||||
widget.MouseFilter = MouseFilterEnum.Stop;
|
||||
widget.GuiInput += (evt) =>
|
||||
{
|
||||
|
||||
+27
-1
@@ -39,6 +39,9 @@ namespace Cthangover.FFBattle.UI
|
||||
private ColorRect _background;
|
||||
private Panel _border;
|
||||
private Label _cursorLabel;
|
||||
private Label _caption;
|
||||
private StyleBoxFlat _rowActiveStyle;
|
||||
private StyleBoxFlat _rowIdleStyle;
|
||||
private List<Label> _itemLabels = new();
|
||||
private Tween _cursorTween;
|
||||
private int _selectedIndex;
|
||||
@@ -70,6 +73,8 @@ namespace Cthangover.FFBattle.UI
|
||||
|
||||
/// <summary>Raised when an entry is confirmed (Enter/Space or left click).</summary>
|
||||
public event System.Action<int> OnItemSelected;
|
||||
/// <summary>Raised when the highlighted entry changes (keyboard or mouse hover); carries the new index.</summary>
|
||||
public event System.Action<int> OnSelectionChanged;
|
||||
/// <summary>Raised when the menu is cancelled (right click, or Escape handled externally).</summary>
|
||||
public event System.Action OnCancelled;
|
||||
|
||||
@@ -87,6 +92,12 @@ namespace Cthangover.FFBattle.UI
|
||||
_cursorLabel = GetNode<Label>("Cursor");
|
||||
_cursorLabel.AddThemeColorOverride("font_color", UiTokens.AccentBright);
|
||||
|
||||
_caption = GetNode<Label>("Caption");
|
||||
_caption.AddThemeColorOverride("font_color", UiTokens.Muted);
|
||||
|
||||
_rowActiveStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.AccentTrace, UiTokens.Alpha(UiTokens.White, 0f));
|
||||
_rowIdleStyle = UiKit.Frame(UiTokens.RadiusSm, UiTokens.Alpha(UiTokens.White, 0f), UiTokens.Alpha(UiTokens.White, 0f));
|
||||
|
||||
GuiInput += OnPanelGuiInput;
|
||||
}
|
||||
|
||||
@@ -112,6 +123,7 @@ namespace Cthangover.FFBattle.UI
|
||||
label.AddThemeColorOverride("font_color", entries[i].Enabled
|
||||
? UiTokens.Ink
|
||||
: UiTokens.Faint);
|
||||
label.AddThemeStyleboxOverride("normal", _rowIdleStyle);
|
||||
label.MouseFilter = MouseFilterEnum.Stop;
|
||||
label.Position = new Vector2(PADDING + 24, PADDING + i * ITEM_HEIGHT + CURSOR_OFFSET_Y);
|
||||
label.Size = new Vector2(width - PADDING * 2 - 24, ITEM_HEIGHT);
|
||||
@@ -145,16 +157,19 @@ namespace Cthangover.FFBattle.UI
|
||||
_selectedIndex = 0;
|
||||
MoveCursor(0);
|
||||
_cursorLabel.Visible = true;
|
||||
_caption.Text = title ?? string.Empty;
|
||||
_caption.Visible = !string.IsNullOrEmpty(title);
|
||||
_visible = true;
|
||||
Visible = true;
|
||||
}
|
||||
|
||||
/// <summary>Hides the menu and cursor. Does not clear entries — call <see cref="ShowMenu"/> to repopulate.</summary>
|
||||
/// <summary>Hides the menu, caption and cursor. Does not clear entries — call <see cref="ShowMenu"/> to repopulate.</summary>
|
||||
public void HideMenu()
|
||||
{
|
||||
_visible = false;
|
||||
Visible = false;
|
||||
_cursorLabel.Visible = false;
|
||||
_caption.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>Moves selection down, wrapping around and skipping disabled entries.</summary>
|
||||
@@ -208,6 +223,17 @@ namespace Cthangover.FFBattle.UI
|
||||
_cursorTween.SetLoops();
|
||||
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X + 3f, 0.3f);
|
||||
_cursorTween.TweenProperty(_cursorLabel, "position:x", CURSOR_OFFSET_X, 0.3f);
|
||||
|
||||
for (var i = 0; i < _itemLabels.Count; i++)
|
||||
{
|
||||
var enabled = i < Entries.Count && Entries[i].Enabled;
|
||||
var selected = i == index && enabled;
|
||||
_itemLabels[i].AddThemeStyleboxOverride("normal", selected ? _rowActiveStyle : _rowIdleStyle);
|
||||
_itemLabels[i].AddThemeColorOverride("font_color",
|
||||
selected ? UiTokens.AccentBright : enabled ? UiTokens.Ink : UiTokens.Faint);
|
||||
}
|
||||
|
||||
OnSelectionChanged?.Invoke(index);
|
||||
}
|
||||
|
||||
private void OnPanelGuiInput(InputEvent evt)
|
||||
|
||||
@@ -47,8 +47,8 @@ namespace Cthangover.FFBattle.UI
|
||||
var widget = (FFCharacterWidget)TscnScenes.LoadAndBuild("scenes/ff_character_widget.tscn");
|
||||
widget.EnsureConstructed();
|
||||
widget.Scale = new Vector2(scale, scale);
|
||||
widget.Init(players[i]);
|
||||
widget.IsPlayer = true;
|
||||
widget.Init(players[i]);
|
||||
widget.MouseFilter = MouseFilterEnum.Stop;
|
||||
widget.GuiInput += (evt) =>
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user