feat: coins and skills move to the new domain (commit B of the swap)
atomic pair - purchasing couples them, so neither moves alone. SkillData.Level is gone. It was an [Export] on a cached Resource: one instance per process, so the level never persisted, leaked between save slots, and made "unowned" indistinguishable from "level 1". Level now comes from the Playthrough and is passed to ISkill.Initialize. SkillManager reconciles from SkillSetChanged instead of being told by each caller. The old shape was "record it, then tell SkillManager" as two calls at four sites; AddSkill's early return when no player is registered left the skill owned but inert. Reconciliation also tracks the level each component was built at, so an upgrade rebuilds it rather than being patched in from outside. SkillUnlockerComponent collapses to one TryBuy - unlock and upgrade were two methods each doing affordability, debit, level mutation and manager notification in order. The guard three components copy-pasted around Upgrades[Level - 1] - and which BrickArmor omitted - now lives once in SkillComponentBase. Hud reads coins from CoinsChanged instead of rebuilding translated strings every frame. Lives still polls; it moves with the death path.
This commit is contained in:
@@ -10,9 +10,9 @@ public partial class BrickArmorSkillComponent : SkillComponentBase
|
||||
private HealthComponent _healthComponent;
|
||||
private float _armorBonus = 0;
|
||||
|
||||
public override void Initialize(Node owner, SkillData data)
|
||||
public override void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
base.Initialize(owner, data);
|
||||
base.Initialize(owner, data, level);
|
||||
if (Player != null)
|
||||
{
|
||||
_healthComponent = Player.GetNode<HealthComponent>("HealthComponent");
|
||||
@@ -21,8 +21,8 @@ public partial class BrickArmorSkillComponent : SkillComponentBase
|
||||
|
||||
public override void Activate()
|
||||
{
|
||||
if (_healthComponent == null || Data == null) return;
|
||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
||||
if (_healthComponent == null) return;
|
||||
ApplyCurrentUpgrade();
|
||||
}
|
||||
|
||||
public override void Deactivate()
|
||||
|
||||
@@ -55,8 +55,8 @@ public partial class BrickShieldSkillComponent : SkillComponentBase
|
||||
{
|
||||
if (Data != null && _skillManager != null)
|
||||
{
|
||||
GameStateStore.Instance?.RemoveUnlockedSkill(Data.Name);
|
||||
_skillManager.RemoveSkill(Data.Name);
|
||||
// Revoking is enough: SkillManager reconciles the live component away from SkillSetChanged.
|
||||
ProgressionRoot.Instance?.RevokeSkill(Progression.SkillId.Of(Data.Name));
|
||||
}
|
||||
_shieldInstance = null;
|
||||
}
|
||||
|
||||
@@ -81,16 +81,13 @@ public partial class BrickThrowComponent : SkillComponentBase
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
public override void Initialize(Node owner, SkillData data)
|
||||
public override void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
base.Initialize(owner, data);
|
||||
base.Initialize(owner, data, level);
|
||||
|
||||
ThrowInputBehavior = (ThrowInputResource)ThrowInputBehavior?.Duplicate();
|
||||
|
||||
if (Data.Level > 0 && Data.Upgrades.Count >= Data.Level)
|
||||
{
|
||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
||||
}
|
||||
ApplyCurrentUpgrade();
|
||||
}
|
||||
|
||||
public override void Activate()
|
||||
|
||||
@@ -13,9 +13,9 @@ public partial class GroundPoundSkillComponent : SkillComponentBase
|
||||
private PlayerInputHandler _input;
|
||||
private bool _isPounding = false;
|
||||
|
||||
public override void Initialize(Node owner, SkillData data)
|
||||
public override void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
base.Initialize(owner, data);
|
||||
base.Initialize(owner, data, level);
|
||||
if (Player != null)
|
||||
{
|
||||
_input = Player.GetNode<PlayerInputHandler>("PlayerInputHandler");
|
||||
|
||||
@@ -71,9 +71,9 @@ public partial class MagneticSkillComponent : SkillComponentBase
|
||||
collectable.GlobalPosition += direction.Normalized() * speed;
|
||||
}
|
||||
|
||||
public override void Initialize(Node owner, SkillData data)
|
||||
public override void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
base.Initialize(owner, data);
|
||||
base.Initialize(owner, data, level);
|
||||
|
||||
if (Player == null)
|
||||
{
|
||||
@@ -94,10 +94,7 @@ public partial class MagneticSkillComponent : SkillComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
if (Data.Level > 0 && Data.Upgrades.Count >= Data.Level)
|
||||
{
|
||||
ApplyUpgrade(Data.Upgrades[Data.Level - 1]);
|
||||
}
|
||||
ApplyCurrentUpgrade();
|
||||
}
|
||||
|
||||
public override void Activate()
|
||||
|
||||
@@ -5,17 +5,24 @@ using Mr.BrickAdventures.scripts.Resources;
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all skill components to reduce boilerplate.
|
||||
/// Base class for all skill components, to reduce boilerplate.
|
||||
/// </summary>
|
||||
public abstract partial class SkillComponentBase : Node, ISkill
|
||||
{
|
||||
protected PlayerController Player { get; private set; }
|
||||
protected SkillData Data { get; private set; }
|
||||
|
||||
public virtual void Initialize(Node owner, SkillData data)
|
||||
/// <summary>
|
||||
/// How far this skill is upgraded, as owned by the Playthrough. Never read from
|
||||
/// <see cref="Data"/> — see <see cref="ISkill.Initialize"/>.
|
||||
/// </summary>
|
||||
protected int Level { get; private set; } = 1;
|
||||
|
||||
public virtual void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
Player = owner as PlayerController;
|
||||
Data = data;
|
||||
Level = level;
|
||||
|
||||
if (Player == null)
|
||||
{
|
||||
@@ -23,6 +30,21 @@ public abstract partial class SkillComponentBase : Node, ISkill
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the upgrade matching the level this skill was initialised at, if the authored table
|
||||
/// goes that far.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives here because three components had this guard copy-pasted and one of them — the brick
|
||||
/// armor — was missing it, indexing <c>Upgrades[Level - 1]</c> unchecked.
|
||||
/// </remarks>
|
||||
protected void ApplyCurrentUpgrade()
|
||||
{
|
||||
if (Data == null || Level < 1 || Data.Upgrades.Count < Level) return;
|
||||
|
||||
ApplyUpgrade(Data.Upgrades[Level - 1]);
|
||||
}
|
||||
|
||||
public abstract void Activate();
|
||||
public abstract void Deactivate();
|
||||
public virtual void ApplyUpgrade(SkillUpgrade upgrade) { }
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using Mr.BrickAdventures.Autoloads;
|
||||
using Mr.BrickAdventures.scripts.interfaces;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
namespace Mr.BrickAdventures.scripts.components;
|
||||
|
||||
/// <summary>
|
||||
/// Buys skills on the player's behalf.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Now a thin adapter. Unlocking and upgrading used to be two methods that each checked affordability,
|
||||
/// debited coins, mutated <c>SkillData.Level</c> and told <c>SkillManager</c> separately — four steps a
|
||||
/// caller had to get right, in order. <c>PurchaseSkill</c> is one operation covering both; it charges
|
||||
/// the Attempt's ledger, so dying undoes the spend and the skill together, and SkillManager reconciles
|
||||
/// itself rather than being told.
|
||||
/// </remarks>
|
||||
[GlobalClass]
|
||||
public partial class SkillUnlockerComponent : Node
|
||||
{
|
||||
@@ -14,59 +23,36 @@ public partial class SkillUnlockerComponent : Node
|
||||
[Signal]
|
||||
public delegate void SkillUnlockedEventHandler(SkillData skill);
|
||||
|
||||
private GameStateStore Store => GameStateStore.Instance;
|
||||
private static ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
SkillManager = SkillManager.Instance;
|
||||
}
|
||||
|
||||
private bool HasEnoughCoins(int amount) => (Store?.GetTotalCoins() ?? 0) >= amount;
|
||||
|
||||
public bool TryUnlockSkill(SkillData skill)
|
||||
/// <summary>Buys the next level of a skill, whether that is the first or an upgrade.</summary>
|
||||
public PurchaseOutcome TryBuy(SkillData skill)
|
||||
{
|
||||
if (Store == null) return false;
|
||||
if (Store.IsSkillUnlocked(skill)) return false;
|
||||
if (!HasEnoughCoins(skill.Upgrades[0].Cost)) return false;
|
||||
if (skill == null || Progression == null) return PurchaseOutcome.NotPlaying;
|
||||
|
||||
skill.Level = 1;
|
||||
Store.RemoveCoins(skill.Upgrades[0].Cost);
|
||||
Store.UnlockSkillInSession(skill);
|
||||
SkillManager.AddSkill(skill);
|
||||
EmitSignalSkillUnlocked(skill);
|
||||
var outcome = Progression.PurchaseSkill(SkillId.Of(skill.Name));
|
||||
if (outcome == PurchaseOutcome.Purchased)
|
||||
EmitSignalSkillUnlocked(skill);
|
||||
|
||||
return true;
|
||||
return outcome;
|
||||
}
|
||||
|
||||
/// <summary>Grants every purchasable skill, ignoring cost. Console only.</summary>
|
||||
public void UnlockAllSkills()
|
||||
{
|
||||
var availableSkills = SkillManager.AvailableSkills;
|
||||
if (SkillManager == null || Progression == null) return;
|
||||
|
||||
foreach (var skill in availableSkills)
|
||||
foreach (var skill in SkillManager.AvailableSkills)
|
||||
{
|
||||
Store?.UnlockSkillPermanently(skill);
|
||||
if (skill == null) continue;
|
||||
|
||||
Progression.GrantSkill(SkillId.Of(skill.Name));
|
||||
EmitSignalSkillUnlocked(skill);
|
||||
}
|
||||
|
||||
SkillManager.ApplyUnlockedSkills();
|
||||
}
|
||||
|
||||
public bool TryUpgradeSkill(SkillData skill)
|
||||
{
|
||||
if (Store == null) return false;
|
||||
if (!Store.IsSkillUnlocked(skill)) return false;
|
||||
if (skill.Level >= skill.MaxLevel) return false;
|
||||
if (!HasEnoughCoins(skill.Upgrades[skill.Level].Cost)) return false;
|
||||
|
||||
Store.RemoveCoins(skill.Upgrades[skill.Level].Cost);
|
||||
skill.Level++;
|
||||
if (SkillManager.ActiveComponents.TryGetValue(skill.Name, out Variant componentVariant))
|
||||
{
|
||||
var component = componentVariant.AsGodotObject();
|
||||
if (component is ISkill skillInstance)
|
||||
skillInstance.ApplyUpgrade(skill.Upgrades[skill.Level - 1]);
|
||||
}
|
||||
EmitSignalSkillUnlocked(skill);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ public partial class XRayVisionSkillComponent : SkillComponentBase
|
||||
private uint _originalVisibilityLayer;
|
||||
private Timer _timer;
|
||||
|
||||
public override void Initialize(Node owner, SkillData data)
|
||||
public override void Initialize(Node owner, SkillData data, int level)
|
||||
{
|
||||
base.Initialize(owner, data);
|
||||
base.Initialize(owner, data, level);
|
||||
_viewport = GetViewport();
|
||||
_camera = GetViewport().GetCamera2D();
|
||||
_timer = new Timer { OneShot = true };
|
||||
|
||||
Reference in New Issue
Block a user