upd: sync local changes

Fix enemy turn hanging when a lethal hit frees the target widget:
UpdateInfo skips freed widgets, animations finish early and restore
the attacker only while it is alive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
iisaevandClaude Opus 5.5 committed 2026-10-16 16:18:14 +00:00
1 parent ba2c08b1dc
commit 00aa70e302
21 files changed
+122 -34

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
mod_ff_battle_name = FFBattle mod_ff_battle_name = FFBattle
mod_ff_battle_desc = Final Fantasy style battle system mod_ff_battle_desc = Final Fantasy style battle system on top of turn_battle_base: menu-driven commands (attack/defence/item/stun), enemy and party panels, per-action animations and executors, battle HUD and log.
ff_battle/menu_attack = Attack ff_battle/menu_attack = Attack
ff_battle/menu_items = Items ff_battle/menu_items = Items
+1 -1
View File
@@ -1,5 +1,5 @@
mod_ff_battle_name=FF Битва mod_ff_battle_name=FF Битва
mod_ff_battle_desc=Боевая система в стиле Final Fantasy mod_ff_battle_desc=Боевая система в стиле Final Fantasy на платформе turn_battle_base: командное меню (атака/защита/предмет/оглушение), панели партии и врагов, анимации и исполнители действий, HUD и лог боя.
ff_battle/menu_attack=Атака ff_battle/menu_attack=Атака
ff_battle/menu_items=Предметы ff_battle/menu_items=Предметы
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "FFBattle", "name": "FFBattle",
"version": "1.0.0", "version": "1.0.0",
"author": "ct", "author": "ct",
"description": "Final Fantasy style battle system", "description": "Final Fantasy style battle system on top of turn_battle_base: menu-driven commands (attack/defence/item/stun), enemy and party panels, per-action animations and executors, battle HUD and log.",
"category": "gameplay", "category": "gameplay",
"sources": [ "sources": [
"src/**/*.cs" "src/**/*.cs"
+20 -2
View File
@@ -66,10 +66,28 @@ layout_mode = 2
size_flags_vertical = 4 size_flags_vertical = 4
theme_override_font_sizes/font_size = 11 theme_override_font_sizes/font_size = 11
[node name="HpBar" type="ProgressBar" parent="Info/Column"] [node name="HpBarStack" type="Control" parent="Info/Column"]
custom_minimum_size = Vector2(0, 8) custom_minimum_size = Vector2(0, 8)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4 mouse_filter = 2
[node name="ShieldBar" type="ProgressBar" parent="Info/Column/HpBarStack"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
show_percentage = false
[node name="HpBar" type="ProgressBar" parent="Info/Column/HpBarStack"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2 mouse_filter = 2
show_percentage = false show_percentage = false
+11
View File
@@ -49,9 +49,17 @@ namespace Cthangover.FFBattle.Actions
{ {
if (Source == null || Target == null || Action == null) if (Source == null || Target == null || Action == null)
return true; return true;
// The target's death animation may free its widget mid-animation;
// finish early and let DoEnd snap the source back.
if (!IsAlive(Source) || !IsAlive(Target))
return true;
return DoInternalAction(); return DoInternalAction();
} }
/// <summary>True while the widget is still in the tree and not queued for deletion.</summary>
protected static bool IsAlive(FFCharacterWidget widget)
=> GodotObject.IsInstanceValid(widget) && !widget.IsQueuedForDeletion();
/// <summary>Captures the initial world positions of source and target, then delegates to subclass setup.</summary> /// <summary>Captures the initial world positions of source and target, then delegates to subclass setup.</summary>
public void DoStart() public void DoStart()
{ {
@@ -71,8 +79,11 @@ namespace Cthangover.FFBattle.Actions
DoInternalEnd(); DoInternalEnd();
} }
/// <summary>Frame tick of the animation; implementors advance phases and return true when finished.</summary>
protected abstract bool DoInternalAction(); protected abstract bool DoInternalAction();
/// <summary>Phase setup called once by <see cref="DoStart"/> after positions are captured.</summary>
protected abstract void DoInternalStart(); protected abstract void DoInternalStart();
/// <summary>Cleanup called once by <see cref="DoEnd"/> — restore captured positions here.</summary>
protected abstract void DoInternalEnd(); protected abstract void DoInternalEnd();
/// <summary>Quadratic ease-out: fast start, decelerating to the target. Used for return-to-start motion.</summary> /// <summary>Quadratic ease-out: fast start, decelerating to the target. Used for return-to-start motion.</summary>
+10 -6
View File
@@ -1,4 +1,4 @@
using Cthangover.Core.Battle; using Cthangover.Core.Battle;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.TurnBattle; using Cthangover.TurnBattle;
@@ -29,9 +29,10 @@ namespace Cthangover.FFBattle.Actions
public FFAttackAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f) public FFAttackAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f)
: base(source, target, action, speed) { } : base(source, target, action, speed) { }
/// <summary>Starts the attack sequence: compute the stop point short of the target and reset the phase clock.</summary>
protected override void DoInternalStart() protected override void DoInternalStart()
{ {
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
_damageApplied = false; _damageApplied = false;
_phase = Phase.MoveForward; _phase = Phase.MoveForward;
@@ -39,9 +40,10 @@ namespace Cthangover.FFBattle.Actions
_midPoint = TargetPos - dir * 30f; _midPoint = TargetPos - dir * 30f;
} }
/// <summary>Advances move-in → impact (damage via <see cref="ActionExecutorHub"/>, hit flash/shake, damage popup) → move-back.</summary>
protected override bool DoInternalAction() protected override bool DoInternalAction()
{ {
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale; var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
switch (_phase) switch (_phase)
{ {
@@ -53,7 +55,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.Impact; _phase = Phase.Impact;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -86,7 +88,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.MoveBack; _phase = Phase.MoveBack;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -108,9 +110,11 @@ namespace Cthangover.FFBattle.Actions
return false; return false;
} }
/// <summary>Returns the attacker to its original position and refreshes both widgets.</summary>
protected override void DoInternalEnd() protected override void DoInternalEnd()
{ {
Source.GlobalPosition = SourcePos; if (IsAlive(Source))
Source.GlobalPosition = SourcePos;
Source.UpdateInfo(); Source.UpdateInfo();
Target.UpdateInfo(); Target.UpdateInfo();
} }
+2
View File
@@ -16,8 +16,10 @@ namespace Cthangover.FFBattle.Actions
/// </summary> /// </summary>
public class FFDamageExecutor : ActionBase public class FFDamageExecutor : ActionBase
{ {
/// <summary>Hub registry key for this executor.</summary>
public override string ID => "FFDamageExecutor"; public override string ID => "FFDamageExecutor";
/// <summary>Applies attack damage with status hooks and defence absorption; consumes the action's required points.</summary>
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
+2
View File
@@ -12,8 +12,10 @@ namespace Cthangover.FFBattle.Actions
/// </summary> /// </summary>
public class FFDefenceExecutor : ActionBase public class FFDefenceExecutor : ActionBase
{ {
/// <summary>Hub registry key for this executor.</summary>
public override string ID => "FFDefenceExecutor"; public override string ID => "FFDefenceExecutor";
/// <summary>Adds the action's "Defence" value (default 4) to the target's defence shield.</summary>
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
+10 -6
View File
@@ -1,4 +1,4 @@
using Cthangover.Core.Battle; using Cthangover.Core.Battle;
using Cthangover.Core.Battle.Actions; using Cthangover.Core.Battle.Actions;
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.TurnBattle; using Cthangover.TurnBattle;
@@ -30,17 +30,19 @@ namespace Cthangover.FFBattle.Actions
public FFDefendAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f) public FFDefendAnimation(FFCharacterWidget source, FFCharacterWidget target, ActionCharacter action, float speed = 1f)
: base(source, target, action, speed) { } : base(source, target, action, speed) { }
/// <summary>Starts the defend sequence from the bounce phase and captures the glow state.</summary>
protected override void DoInternalStart() protected override void DoInternalStart()
{ {
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
_effectApplied = false; _effectApplied = false;
_phase = Phase.Bounce; _phase = Phase.Bounce;
_bounceOffset = new Vector2(0, -12f); _bounceOffset = new Vector2(0, -12f);
} }
/// <summary>Advances bounce → glow (defence application with a green flash) → settle.</summary>
protected override bool DoInternalAction() protected override bool DoInternalAction()
{ {
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale; var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
switch (_phase) switch (_phase)
{ {
@@ -53,7 +55,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.Glow; _phase = Phase.Glow;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -84,7 +86,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.Recover; _phase = Phase.Recover;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -106,9 +108,11 @@ namespace Cthangover.FFBattle.Actions
return false; return false;
} }
/// <summary>Returns the defender to its original position and refreshes both widgets.</summary>
protected override void DoInternalEnd() protected override void DoInternalEnd()
{ {
Source.GlobalPosition = SourcePos; if (IsAlive(Source))
Source.GlobalPosition = SourcePos;
Source.UpdateInfo(); Source.UpdateInfo();
Target.UpdateInfo(); Target.UpdateInfo();
} }
+10 -6
View File
@@ -1,4 +1,4 @@
using Cthangover.Core.Characters; using Cthangover.Core.Characters;
using Cthangover.Core.Items; using Cthangover.Core.Items;
using Cthangover.FFBattle.UI; using Cthangover.FFBattle.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
@@ -31,9 +31,10 @@ namespace Cthangover.FFBattle.Actions
_item = item; _item = item;
} }
/// <summary>Spawns the raised item icon above the user and starts the item sequence.</summary>
protected override void DoInternalStart() protected override void DoInternalStart()
{ {
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
_effectApplied = false; _effectApplied = false;
_phase = Phase.RaiseItem; _phase = Phase.RaiseItem;
@@ -49,9 +50,10 @@ namespace Cthangover.FFBattle.Actions
} }
} }
/// <summary>Advances raise-item → apply (item use with effect flash) → lower-icon.</summary>
protected override bool DoInternalAction() protected override bool DoInternalAction()
{ {
var elapsed = (float)(Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Engine.TimeScale; var elapsed = (float)(Godot.Time.GetTicksUsec() / 1_000_000.0 - Timestamp) * Speed * (float)Godot.Engine.TimeScale;
switch (_phase) switch (_phase)
{ {
@@ -71,7 +73,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.ApplyEffect; _phase = Phase.ApplyEffect;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -103,7 +105,7 @@ namespace Cthangover.FFBattle.Actions
if (progress >= 1f) if (progress >= 1f)
{ {
_phase = Phase.Recover; _phase = Phase.Recover;
Timestamp = Time.GetTicksUsec() / 1_000_000.0; Timestamp = Godot.Time.GetTicksUsec() / 1_000_000.0;
} }
break; break;
} }
@@ -125,9 +127,11 @@ namespace Cthangover.FFBattle.Actions
return false; return false;
} }
/// <summary>Frees the item icon, returns the user to its original position and refreshes both widgets.</summary>
protected override void DoInternalEnd() protected override void DoInternalEnd()
{ {
Source.GlobalPosition = SourcePos; if (IsAlive(Source))
Source.GlobalPosition = SourcePos;
if (_itemIcon != null) if (_itemIcon != null)
{ {
+2
View File
@@ -13,8 +13,10 @@ namespace Cthangover.FFBattle.Actions
/// </summary> /// </summary>
public class FFStunExecutor : ActionBase public class FFStunExecutor : ActionBase
{ {
/// <summary>Hub registry key for this executor.</summary>
public override string ID => "FFStunExecutor"; public override string ID => "FFStunExecutor";
/// <summary>Adds the stun effect for the action's "Turn" count (default 3); stun-immune targets consume the points but stay un-stunned.</summary>
public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target) public override ChangedAttributes Execute(ActionCharacter action, Character user, Character target)
{ {
if (target == null || user == null || !CheckRequiredAndUsePoint(action, user)) if (target == null || user == null || !CheckRequiredAndUsePoint(action, user))
+3
View File
@@ -5,6 +5,8 @@ using Cthangover.Core.UI.CharacterPanel;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -30,6 +32,7 @@ namespace Cthangover.FFBattle.UI
private PanelContainer _note; private PanelContainer _note;
private Label _noteLabel; private Label _noteLabel;
/// <summary>Builds the action detail passport panel (description plate shown when an action is inspected).</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+3
View File
@@ -2,6 +2,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -16,6 +18,7 @@ namespace Cthangover.FFBattle.UI
private Label _label; private Label _label;
private Tween _tween; private Tween _tween;
/// <summary>Builds the centre-screen turn/outcome banner label.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+2
View File
@@ -1,6 +1,7 @@
using Cthangover.Core.UI; using Cthangover.Core.UI;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -77,6 +78,7 @@ namespace Cthangover.FFBattle.UI
ProcessMode = ProcessModeEnum.Always; ProcessMode = ProcessModeEnum.Always;
} }
/// <summary>Routes menu/confirm input into the FF battle state machine (target select, turn advance).</summary>
protected override void OnInput(InputEvent @event) protected override void OnInput(InputEvent @event)
{ {
if (State == FFBattleState.AnimationPlaying || State == FFBattleState.Ended) if (State == FFBattleState.AnimationPlaying || State == FFBattleState.Ended)
+3
View File
@@ -2,6 +2,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -21,6 +23,7 @@ namespace Cthangover.FFBattle.UI
private Label _enemiesValue; private Label _enemiesValue;
private Label _alliesValue; private Label _alliesValue;
/// <summary>Builds the FF battle HUD frame: command menu host, speed control and log toggle.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+3
View File
@@ -2,6 +2,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -23,6 +25,7 @@ namespace Cthangover.FFBattle.UI
private ScrollContainer _scroll; private ScrollContainer _scroll;
private VBoxContainer _rows; private VBoxContainer _rows;
/// <summary>Builds the scrollable FF battle log with coloured entries per <see cref="FFBattleLogKind"/>.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+3
View File
@@ -2,6 +2,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -17,6 +19,7 @@ namespace Cthangover.FFBattle.UI
private Label _icon; private Label _icon;
private Label _text; private Label _text;
/// <summary>Builds the floating hint shown above the hovered target.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+27 -11
View File
@@ -7,6 +7,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -15,7 +17,10 @@ namespace Cthangover.FFBattle.UI
/// only fetches the nodes, dresses them with <see cref="UiTokens"/> styles and /// only fetches the nodes, dresses them with <see cref="UiTokens"/> styles and
/// refreshes their content. Visually the card follows the unified style guide: /// 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 /// 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 /// and an info block holding name, HP numbers, the layered health bar
/// (<see cref="UiKit.StyleShieldedHealthBar"/>: track → health fill → blue
/// defence segment drawn past the health edge while
/// <see cref="CharacterAttributes.Defence"/> absorbs damage), action point pips
/// (party) or attack caption (enemies) and the defence shield chip. /// (party) or attack caption (enemies) and the defence shield chip.
/// <para/> /// <para/>
/// The public surface is animation-facing, not layout-facing: /// The public surface is animation-facing, not layout-facing:
@@ -38,6 +43,7 @@ namespace Cthangover.FFBattle.UI
private Panel _teamDot; private Panel _teamDot;
private Label _nameLabel; private Label _nameLabel;
private Label _hpNumLabel; private Label _hpNumLabel;
private ProgressBar _shieldBar;
private ProgressBar _hpBar; private ProgressBar _hpBar;
private HBoxContainer _pipsBox; private HBoxContainer _pipsBox;
private Label _atkLabel; private Label _atkLabel;
@@ -61,6 +67,7 @@ namespace Cthangover.FFBattle.UI
/// <summary>Exposes this widget as a <see cref="Godot.Control"/> for layout calculations.</summary> /// <summary>Exposes this widget as a <see cref="Godot.Control"/> for layout calculations.</summary>
public Control ControlNode => this; public Control ControlNode => this;
/// <summary>Builds the character widget: portrait frame, HP bar and status chips.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
@@ -70,7 +77,8 @@ namespace Cthangover.FFBattle.UI
_nameLabel = GetNode<Label>("Info/Column/NameRow/NameLabel"); _nameLabel = GetNode<Label>("Info/Column/NameRow/NameLabel");
_teamDot = GetNode<Panel>("Info/Column/NameRow/TeamDot"); _teamDot = GetNode<Panel>("Info/Column/NameRow/TeamDot");
_hpNumLabel = GetNode<Label>("Info/Column/NameRow/HpNumLabel"); _hpNumLabel = GetNode<Label>("Info/Column/NameRow/HpNumLabel");
_hpBar = GetNode<ProgressBar>("Info/Column/HpBar"); _shieldBar = GetNode<ProgressBar>("Info/Column/HpBarStack/ShieldBar");
_hpBar = GetNode<ProgressBar>("Info/Column/HpBarStack/HpBar");
_pipsBox = GetNode<HBoxContainer>("Info/Column/Foot/PipsBox"); _pipsBox = GetNode<HBoxContainer>("Info/Column/Foot/PipsBox");
_atkLabel = GetNode<Label>("Info/Column/Foot/AtkLabel"); _atkLabel = GetNode<Label>("Info/Column/Foot/AtkLabel");
_shieldChip = GetNode<PanelContainer>("Info/Column/Foot/ShieldChip"); _shieldChip = GetNode<PanelContainer>("Info/Column/Foot/ShieldChip");
@@ -115,28 +123,35 @@ namespace Cthangover.FFBattle.UI
} }
/// <summary> /// <summary>
/// Single refresh entry point for all mutable character state: HP bar and /// Single refresh entry point for all mutable character state: layered
/// numbers with threshold colours, action point pips, shield chip, attack /// HP/defence bar and numbers with threshold colours, action point pips,
/// caption and the stun badge with its remaining turns. Called by battle /// shield chip, attack caption and the stun badge with its remaining
/// code and by animations after each resolved action. /// turns. Called by battle code and by animations after each resolved
/// action.
/// </summary> /// </summary>
public void UpdateInfo() public void UpdateInfo()
{ {
if (Card?.Attributes == null) // A lethal hit frees the widget through the death animation while
// the attack animation and Health.OnChange subscribers still hold it.
if (!IsInstanceValid(this) || IsQueuedForDeletion() || Card?.Attributes == null)
return; return;
var health = Card.Attributes.Health; var health = Card.Attributes.Health;
var percent = Mathf.Clamp(health.Percent, 0f, 1f); var percent = Mathf.Clamp(health.Percent, 0f, 1f);
var defence = Card.Attributes.Defence?.Value ?? 0;
var fillColor = percent > 0.5f ? UiTokens.HpOk : percent > 0.25f ? UiTokens.Warning : UiTokens.HpLow; 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; var textColor = percent > 0.5f ? UiTokens.HpText : percent > 0.25f ? UiTokens.Warning : UiTokens.DangerText;
if (_hpBar != null) if (_hpBar != null)
{ {
_hpBar.Value = percent * 100f; _hpBar.Value = percent * 100f;
_hpBar.AddThemeStyleboxOverride("background", UiKit.Frame(UiTokens.RadiusSm, UiTokens.LineDisabled));
_hpBar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, fillColor)); _hpBar.AddThemeStyleboxOverride("fill", UiKit.Frame(UiTokens.RadiusSm, fillColor));
} }
// Blue defence segment on top of the health fill; collapses onto
// the health edge when the shield is gone.
UiKit.UpdateShieldedHealthBar(_shieldBar, percent, defence, health.BaseValue);
if (_hpNumLabel != null) if (_hpNumLabel != null)
{ {
_hpNumLabel.Text = $"{health.Value}/{health.BaseValue}"; _hpNumLabel.Text = $"{health.Value}/{health.BaseValue}";
@@ -151,7 +166,6 @@ namespace Cthangover.FFBattle.UI
if (_shieldChip != null) if (_shieldChip != null)
{ {
var defence = Card.Attributes.Defence?.Value ?? 0;
_shieldChip.Visible = defence > 0; _shieldChip.Visible = defence > 0;
if (_shieldValue != null) if (_shieldValue != null)
_shieldValue.Text = defence.ToString(); _shieldValue.Text = defence.ToString();
@@ -258,6 +272,7 @@ namespace Cthangover.FFBattle.UI
tween.SetParallel(true); tween.SetParallel(true);
tween.TweenProperty(this, "modulate:a", 0f, duration * 0.5f); tween.TweenProperty(this, "modulate:a", 0f, duration * 0.5f);
tween.TweenProperty(this, "scale", BaseScale * new Vector2(0.1f, 1.2f), duration); tween.TweenProperty(this, "scale", BaseScale * new Vector2(0.1f, 1.2f), duration);
tween.TweenProperty(_shieldBar, "modulate:a", 0f, duration);
tween.TweenProperty(_hpBar, "modulate:a", 0f, duration); tween.TweenProperty(_hpBar, "modulate:a", 0f, duration);
tween.TweenProperty(_hpNumLabel, "modulate:a", 0f, duration); tween.TweenProperty(_hpNumLabel, "modulate:a", 0f, duration);
tween.TweenProperty(_nameLabel, "modulate:a", 0f, duration); tween.TweenProperty(_nameLabel, "modulate:a", 0f, duration);
@@ -292,10 +307,11 @@ namespace Cthangover.FFBattle.UI
_atkLabel.AddThemeColorOverride("font_color", UiTokens.Muted); _atkLabel.AddThemeColorOverride("font_color", UiTokens.Muted);
_teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Danger)); _teamDot.AddThemeStyleboxOverride("panel", UiKit.Frame(4, UiTokens.Danger));
var hpBackground = UiKit.Frame(UiTokens.RadiusSm, UiTokens.LineDisabled);
var hpFill = UiKit.Frame(UiTokens.RadiusSm, UiTokens.HpOk); var hpFill = UiKit.Frame(UiTokens.RadiusSm, UiTokens.HpOk);
_hpBar.AddThemeStyleboxOverride("background", hpBackground);
_hpBar.AddThemeStyleboxOverride("fill", hpFill); _hpBar.AddThemeStyleboxOverride("fill", hpFill);
// Layered pie: the shield bar owns the track and the blue defence
// fill; the health bar paints only its fill on top of it.
UiKit.StyleShieldedHealthBar(_shieldBar, _hpBar);
var shieldStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.StatBg, UiTokens.StatLine); var shieldStyle = UiKit.Frame(UiTokens.RadiusXs, UiTokens.StatBg, UiTokens.StatLine);
shieldStyle.ContentMarginLeft = 7; shieldStyle.ContentMarginLeft = 7;
+2
View File
@@ -7,6 +7,7 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -33,6 +34,7 @@ namespace Cthangover.FFBattle.UI
/// <summary>Raised when an enemy widget is clicked (used for target selection).</summary> /// <summary>Raised when an enemy widget is clicked (used for target selection).</summary>
public event System.Action<FFCharacterWidget> OnEnemyClicked; public event System.Action<FFCharacterWidget> OnEnemyClicked;
/// <summary>Builds the enemy grid panel and lays out enemy widgets.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
+4
View File
@@ -3,6 +3,8 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -78,6 +80,7 @@ namespace Cthangover.FFBattle.UI
/// <summary>Raised when the menu is cancelled (right click, or Escape handled externally).</summary> /// <summary>Raised when the menu is cancelled (right click, or Escape handled externally).</summary>
public event System.Action OnCancelled; public event System.Action OnCancelled;
/// <summary>Builds the command menu list (Attack/Defence/Item/...) used during the player turn.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
@@ -251,6 +254,7 @@ namespace Cthangover.FFBattle.UI
_itemLabels.Clear(); _itemLabels.Clear();
} }
/// <summary>Frees the menu entries and disconnects selection handlers when the panel closes.</summary>
protected override void Destruct() protected override void Destruct()
{ {
ClearEntries(); ClearEntries();
+2
View File
@@ -6,6 +6,7 @@ using Cthangover.Core.UI;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Godot; using Godot;
using Cthangover.Mods.Interface.Base.Lists;
namespace Cthangover.FFBattle.UI namespace Cthangover.FFBattle.UI
{ {
/// <summary> /// <summary>
@@ -26,6 +27,7 @@ namespace Cthangover.FFBattle.UI
/// <summary>Raised when a player widget is clicked — routed to <see cref="FFBattleCore"/> for character selection.</summary> /// <summary>Raised when a player widget is clicked — routed to <see cref="FFBattleCore"/> for character selection.</summary>
public event System.Action<FFCharacterWidget> OnWidgetClicked; public event System.Action<FFCharacterWidget> OnWidgetClicked;
/// <summary>Builds the bottom player-party panel hosting one widget per party member.</summary>
protected override void Construct() protected override void Construct()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);