Consumable host: initialise at activation, dispose at expiry, instantiate consumables
ActivateConsumable runs Initialize on the effect's behaviours; expiry disposes them before Resolver.Forget, so a consumable's modifiers exist exactly while its effect is active. ItemManager.CreateInstance returns a ConsumableItem for a consumable design. StatResolver.DetachModifier drops an entry left with nothing attached, so ModifierEntryCount returns to baseline. LoadoutTests' hand-built ConsumableItem goes through CreateInstance. Builds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
739eee4a8e
commit
aaab561f98
5 changed files with 20 additions and 7 deletions
|
|
@ -860,7 +860,13 @@ public abstract class Entity
|
||||||
|
|
||||||
public void ActivateConsumable(ConsumableItem item)
|
public void ActivateConsumable(ConsumableItem item)
|
||||||
{
|
{
|
||||||
_activeConsumables.Add(new ConsumableItemEffect(item, this));
|
var effect = new ConsumableItemEffect(item, this);
|
||||||
|
// The same hook Activate runs over Equipment: a behaviour that computes its targets there (StatModifier)
|
||||||
|
// would otherwise reach its second tick uninitialised.
|
||||||
|
foreach (var behavior in effect.Behaviors)
|
||||||
|
if (behavior is IInitializableBehavior initializable)
|
||||||
|
initializable.Initialize();
|
||||||
|
_activeConsumables.Add(effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data)
|
public ConsumableItemEffect FindActiveConsumable(ConsumableItemData data)
|
||||||
|
|
@ -1453,6 +1459,11 @@ public abstract class Entity
|
||||||
// Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this
|
// Cut 2 Gate 1 fix (docs/stats-and-power-cut.md): an expired consumable dropped out of this
|
||||||
// list without ever telling the resolver, leaving its generation/cache/modifier entries
|
// list without ever telling the resolver, leaving its generation/cache/modifier entries
|
||||||
// reachable (keyed by this ConsumableItemEffect instance) for the rest of the process.
|
// reachable (keyed by this ConsumableItemEffect instance) for the rest of the process.
|
||||||
|
// A StatModifier's attachments are keyed by its target items, not by the effect, so Forget alone
|
||||||
|
// leaves a boost attached to its targets: dispose each behaviour first.
|
||||||
|
foreach (var behavior in _activeConsumables[i].Behaviors)
|
||||||
|
if (behavior is IDisposable disposable)
|
||||||
|
disposable.Dispose();
|
||||||
Resolver.Forget(_activeConsumables[i]);
|
Resolver.Forget(_activeConsumables[i]);
|
||||||
_activeConsumables.RemoveAt(i--);
|
_activeConsumables.RemoveAt(i--);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,9 @@ public class ItemManager
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (design is ConsumableItemData)
|
||||||
|
return new ConsumableItem { Data = l.Design, Lot = lot };
|
||||||
|
|
||||||
return new CompoundCommodity
|
return new CompoundCommodity
|
||||||
{
|
{
|
||||||
Data = l.Design, Lot = lot
|
Data = l.Design, Lot = lot
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,9 @@ public class StatResolver
|
||||||
{
|
{
|
||||||
set.Scale.Remove(modifierKey);
|
set.Scale.Remove(modifierKey);
|
||||||
set.Constant.Remove(modifierKey);
|
set.Constant.Remove(modifierKey);
|
||||||
|
// An entry with nothing attached is no entry: a modifier that is gone leaves no trace in the resolver.
|
||||||
|
if (set.Scale.Count == 0 && set.Constant.Count == 0)
|
||||||
|
_modifiers.Remove(entry);
|
||||||
}
|
}
|
||||||
_cache.Remove(entry);
|
_cache.Remove(entry);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ public sealed class ConsumableTests : IDisposable
|
||||||
Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3);
|
Assert.Equal(20f, rig.Engine.Evaluate(rig.Thrust), 3);
|
||||||
Assert.True(resolver.ModifierEntryCount > baseline);
|
Assert.True(resolver.ModifierEntryCount > baseline);
|
||||||
|
|
||||||
Tick(rig, 3, .3f); // 0.9s elapsed in all: expired on the third of these ticks
|
Tick(rig, 3, .3f); // 1.5s elapsed in all: the effect ran out on the fourth tick
|
||||||
Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3);
|
Assert.Equal(10f, rig.Engine.Evaluate(rig.Thrust), 3);
|
||||||
Assert.Equal(baseline, resolver.ModifierEntryCount);
|
Assert.Equal(baseline, resolver.ModifierEntryCount);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -760,11 +760,7 @@ public sealed class LoadoutTests : IDisposable
|
||||||
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
var items = new ItemManager(cache, new ProvenanceLedger(), RunSaveTests.TestSettings(), _ => { });
|
||||||
var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f));
|
var hullItem = (EquippableItem) items.CreateInstance(items.CreateLot(hullData, maker, .5f));
|
||||||
var ship = new Ship(items, null, hullItem, new EntitySettings());
|
var ship = new Ship(items, null, hullItem, new EntitySettings());
|
||||||
// ItemManager.CreateInstance has no ConsumableItemData branch (it only special-cases EquippableItemData,
|
var consumableItem = (ConsumableItem) items.CreateInstance(items.CreateLot(consumableData, maker, .5f));
|
||||||
// falling back to CompoundCommodity otherwise); build the instance directly, as that method does for
|
|
||||||
// EquippableItem.
|
|
||||||
var consumableLot = items.CreateLot(consumableData, maker, .5f);
|
|
||||||
var consumableItem = new ConsumableItem { Data = cache.RefOf<ItemData>(consumableData), Lot = consumableLot };
|
|
||||||
var effect = new ConsumableItemEffect(consumableItem, ship);
|
var effect = new ConsumableItemEffect(consumableItem, ship);
|
||||||
effect.Update(5f); // half the duration elapsed -> progress .5 -> effectiveness .5 on a linear ramp
|
effect.Update(5f); // half the duration elapsed -> progress .5 -> effectiveness .5 on a linear ramp
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue