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:
1 parent
ba2c08b1dc
commit
00aa70e302
21 files changed
+122
-34
No files matched your search
@@ -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,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
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in new issue
Block a user