2020-12-22 03:13:43 +00:00
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
using MessagePack;
|
|
|
|
|
using Newtonsoft.Json;
|
2026-09-14 17:39:12 +02:00
|
|
|
using CultMath;
|
|
|
|
|
using static CultMath.math;
|
2026-08-10 14:57:50 +01:00
|
|
|
|
|
|
|
|
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn), RuntimeInspectable]
|
|
|
|
|
public class ShieldData : BehaviorData
|
|
|
|
|
{
|
2026-09-19 02:46:17 +02:00
|
|
|
[Inspectable, JsonProperty("efficiency"), Key(1), RuntimeInspectable]
|
2026-08-10 14:57:50 +01:00
|
|
|
public PerformanceStat Efficiency = new PerformanceStat();
|
|
|
|
|
|
2026-09-19 02:46:17 +02:00
|
|
|
[Inspectable, JsonProperty("energy"), Key(2), RuntimeInspectable]
|
2026-08-10 14:57:50 +01:00
|
|
|
public PerformanceStat EnergyUsage = new PerformanceStat();
|
2026-09-19 02:46:17 +02:00
|
|
|
|
|
|
|
|
// Operator ruling, 2026-09-19 (docs/stats-and-power-cut.md, shield reserve ruling block): supersedes Cut
|
|
|
|
|
// 4's derived sizing (reserve = EnergyUsage, refilling every second). The reserve is now an authored energy
|
|
|
|
|
// pool, sized independently of EnergyUsage (which stays the per-damage-point cost multiplier).
|
|
|
|
|
[Inspectable, JsonProperty("capacity"), Key(3), RuntimeInspectable]
|
|
|
|
|
public PerformanceStat Capacity = new PerformanceStat();
|
|
|
|
|
|
|
|
|
|
// Seconds to go from empty to full while the shield is up (holding, not broken).
|
|
|
|
|
[Inspectable, JsonProperty("refillDuration"), Key(4), RuntimeInspectable]
|
|
|
|
|
public PerformanceStat RefillDuration = new PerformanceStat();
|
|
|
|
|
|
|
|
|
|
// Seconds to go from empty to full while the shield is broken. This is the punish window: how long a hit
|
|
|
|
|
// that overwhelms the reserve leaves the hull exposed.
|
|
|
|
|
[Inspectable, JsonProperty("restoreDuration"), Key(5), RuntimeInspectable]
|
|
|
|
|
public PerformanceStat RestoreDuration = new PerformanceStat();
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
public override Behavior CreateInstance(EquippedItem item)
|
|
|
|
|
{
|
|
|
|
|
return new Shield(this, item);
|
|
|
|
|
}
|
|
|
|
|
public override Behavior CreateInstance(ConsumableItemEffect item)
|
|
|
|
|
{
|
|
|
|
|
return new Shield(this, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00
|
|
|
public class Shield : Behavior, IProgressBehavior, IPowerConsumer, IAlwaysUpdatedBehavior
|
2020-04-09 07:48:45 +01:00
|
|
|
{
|
2021-03-06 01:04:26 +00:00
|
|
|
public float Efficiency { get; private set; }
|
|
|
|
|
public float EnergyUsage { get; private set; }
|
2026-09-19 02:46:17 +02:00
|
|
|
public float Capacity { get; private set; }
|
|
|
|
|
public float RefillDuration { get; private set; }
|
|
|
|
|
public float RestoreDuration { get; private set; }
|
|
|
|
|
|
|
|
|
|
// Operator ruling, 2026-09-19 (docs/stats-and-power-cut.md, shield reserve ruling block): the state Cut 4's
|
|
|
|
|
// reserve never had. A hit the reserve cannot fully cover breaks the shield instead of being silently
|
|
|
|
|
// refused with its charge intact. Persists tick to tick (unlike Efficiency/EnergyUsage/Capacity above,
|
|
|
|
|
// which are re-evaluated fresh every Execute): it is the shield's own history, not a derived stat.
|
|
|
|
|
//
|
|
|
|
|
// Operator's second ruling, same date, on the fork the first ruling left open: the breaking hit passes
|
|
|
|
|
// through in full -- no partial absorption. The reserve does not spend the portion it could have covered;
|
F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00
|
|
|
// TakeHit is never called for this hit at all -- CanTakeHit's false return is the caller's signal to route
|
|
|
|
|
// the whole hit to the hull instead, and Break() below (F5, Soul pass 2026-09-19) is what the caller invokes
|
|
|
|
|
// at that same point to actually break the shield, since CanTakeHit itself is now a pure query (see every
|
|
|
|
|
// caller under Assets/Scripts/Gameplay/Weapons). The break is a distinct event the player feels, not a
|
|
|
|
|
// discount on an overkill hit.
|
2026-09-19 02:46:17 +02:00
|
|
|
//
|
|
|
|
|
// A second question the operator asked me to settle and state: what happens to whatever charge the reserve
|
|
|
|
|
// held at the moment it broke. Chosen: emptied by the break, not left at its pre-hit charge. A reserve that
|
|
|
|
|
// broke at 9/10 and kept that charge would refill to full in a heartbeat under RestoreDuration (which is
|
|
|
|
|
// sized for a full recharge, not a top-up) -- the punish window would only ever bite on a shield broken
|
|
|
|
|
// from near-empty. Zeroing it makes RestoreDuration mean the same thing every time a shield breaks,
|
|
|
|
|
// regardless of how close to full it was when the hit that broke it landed.
|
|
|
|
|
public bool Broken { get; private set; }
|
2020-04-09 07:48:45 +01:00
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
private ShieldData _data;
|
2020-04-09 07:48:45 +01:00
|
|
|
|
2026-09-19 02:46:17 +02:00
|
|
|
// Operator ruling, 2026-09-19: reserve capacity is now an authored number (Capacity), independent of
|
|
|
|
|
// EnergyUsage (which stays the per-damage-point cost multiplier applied in CanTakeHit/TakeHit below). Rate
|
|
|
|
|
// is derived the same way InputCapacitor already derives every other consumer's rate (Capacity/duration,
|
|
|
|
|
// UpdateStats' own cooldown-to-rate formula) -- just fed RefillDuration while up and RestoreDuration while
|
|
|
|
|
// Broken, so refilling from partial and restoring from broken are two different speeds through the one
|
|
|
|
|
// mechanism every other IPowerConsumer already uses. A partial bus grant (Item.PowerSupply < 1) stretches
|
|
|
|
|
// either duration proportionally for free: AddCharge below is scaled by PowerSupply same as every other
|
|
|
|
|
// consumer, so a half grant simply takes twice as long to fill regardless of which duration is active.
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
private readonly InputCapacitor _reserve = new InputCapacitor();
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
public Shield(ShieldData data, EquippedItem item) : base(data, item)
|
2021-04-18 05:13:20 +01:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_data = data;
|
2021-04-18 05:13:20 +01:00
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
public Shield(ShieldData data, ConsumableItemEffect item) : base(data, item)
|
2020-04-09 07:48:45 +01:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_data = data;
|
2020-04-09 07:48:45 +01:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 02:46:17 +02:00
|
|
|
// Resolved fresh (Capacitor.ResolveCapacity's precedent -- PowerBus.Step calls PowerRequest below before
|
|
|
|
|
// this behaviour's own Execute runs this tick), reading Broken as it stood at the end of the previous tick.
|
2026-09-19 06:58:57 +02:00
|
|
|
//
|
|
|
|
|
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Capacity, RefillDuration
|
|
|
|
|
// and RestoreDuration are registered request fields (StatValidation.PowerRequestFields) -- read nominally.
|
|
|
|
|
// This also feeds Update's own real accumulation math below (RefreshReserve is called from both PowerRequest
|
|
|
|
|
// and Update), which is fine: the actual charge added is separately scaled by Item.PowerSupply there, so
|
|
|
|
|
// nominalizing the reserve's physical size/rate does not change how much of a partial grant it actually
|
|
|
|
|
// receives, only removes any dependency the size/rate calculation itself could have had on this tick's grant.
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
private void RefreshReserve()
|
|
|
|
|
{
|
2026-09-19 06:58:57 +02:00
|
|
|
var capacity = EvaluateNominalPower(_data.Capacity);
|
|
|
|
|
var duration = Broken ? EvaluateNominalPower(_data.RestoreDuration) : EvaluateNominalPower(_data.RefillDuration);
|
2026-09-19 02:46:17 +02:00
|
|
|
_reserve.UpdateStats(capacity, cooldown: duration, capacityOverride: capacity);
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 02:46:17 +02:00
|
|
|
// The request PowerBus needs before Execute runs.
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
public float PowerRequest(float dt)
|
|
|
|
|
{
|
|
|
|
|
RefreshReserve();
|
|
|
|
|
return _reserve.RequestedFill(dt);
|
|
|
|
|
}
|
|
|
|
|
|
Cut 5: priority tiers replace PowerBus's single shared GrantRatio
PowerTiers.cs names five tiers (Critical/High/Medium/Low/Utility) and their
per-behaviour-kind defaults: Radiator is Critical (the closest thing this game
has to life support -- a starved radiator cascades into wear and shutdown
across the whole hull), Shield is High, Thruster/AetherDrive are Medium,
ConstantWeapon/InstantWeapon are Low, and Sensor/EnergyDraw are Utility.
IPowerConsumer.DefaultPowerTier lets each behaviour own its own default.
EquippableItem.PowerTier (Key 12) is the stored, player-choosable tier on the
unit rather than the design; EquippedItem's constructor seeds a kind default
the first time a unit is equipped, only when the field is still
PowerTiers.Unassigned, and never again -- a tier is a stored choice, not
something the bus re-derives.
PowerBus.AllocateTiers walks tiers lowest-number-first: a tier is granted in
full before the next tier sees anything left, an empty tier passes its whole
share down, and within a tier every consumer rations by the same
grant/demand fraction so unequal requests still divide proportionally. A new
TierGrantRatio[] exposes each tier's own ratio for a future UI (Cut 8) to
read starvation from directly, instead of reconstructing it from every item's
PowerSupply.
Deletes EquippedItem.SortPosition, IOrderedBehavior and Reactor.Order (its
only implementer): the bus already steps before any equipment's Execute, so
equip/behaviour order never decided who got fed even before this cut, and
nothing else needed the interface (subject to the map's O3 operator check).
docs/stats-and-power-cut.md Cut 5 / §1.3.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:58:38 +02:00
|
|
|
// Cut 5 (docs/stats-and-power-cut.md §1.3, PowerTiers.cs): High -- hull survival the player is actively
|
|
|
|
|
// trading power for, but not the life-support constant Radiator is, so it sits one tier below.
|
|
|
|
|
public int DefaultPowerTier => PowerTiers.High;
|
|
|
|
|
|
2021-04-18 05:13:20 +01:00
|
|
|
public override bool Execute(float dt)
|
2020-05-11 01:17:10 +01:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
Efficiency = Evaluate(_data.Efficiency);
|
|
|
|
|
EnergyUsage = Evaluate(_data.EnergyUsage);
|
2026-09-19 02:46:17 +02:00
|
|
|
Capacity = Evaluate(_data.Capacity);
|
|
|
|
|
RefillDuration = Evaluate(_data.RefillDuration);
|
|
|
|
|
RestoreDuration = Evaluate(_data.RestoreDuration);
|
2020-05-11 01:17:10 +01:00
|
|
|
return true;
|
|
|
|
|
}
|
2021-03-06 01:04:26 +00:00
|
|
|
|
F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00
|
|
|
// F4 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): the reserve's own recharge, and clearing Broken
|
|
|
|
|
// once it is full, used to live in Execute above -- which Entity.Update only calls while Item.Active is
|
|
|
|
|
// true. A hit that breaks the shield usually also cooks it (AddHeat in TakeHit below), which can knock the
|
|
|
|
|
// item thermally offline in the same moment it breaks -- Execute then stops running for as long as the item
|
|
|
|
|
// stays offline, so the shield could never come back no matter how long RestoreDuration allows for. Restoring
|
|
|
|
|
// must not depend on whether the item is currently Active, so it moved to IAlwaysUpdatedBehavior.Update,
|
|
|
|
|
// which Entity.Update calls every tick unconditionally (Cooldown.cs already relies on the same interface for
|
|
|
|
|
// exactly this reason -- see Entity.cs's own alwaysUpdatedBehavior loop, outside the `if (Active.Value)`
|
|
|
|
|
// block Execute lives in). PowerBus (F2) only bills and refreshes Item.PowerSupply while the item is Active,
|
|
|
|
|
// so while offline this keeps recharging at the last grant it actually received rather than freezing dead or
|
|
|
|
|
// fabricating a fresh one -- the punish window still runs down on its own clock instead of being held
|
|
|
|
|
// hostage by an unrelated shutdown.
|
|
|
|
|
public void Update(float dt)
|
|
|
|
|
{
|
|
|
|
|
// Item is non-null-checked the same way Execute's old version was: a null-Item (consumable-hosted)
|
|
|
|
|
// instance never reaches PowerBus.Step and so never accrues charge -- see CanTakeHit's own comment.
|
|
|
|
|
if (Item == null) return;
|
|
|
|
|
RefreshReserve();
|
|
|
|
|
_reserve.AddCharge(_reserve.RequestedFill(dt) * Item.PowerSupply);
|
|
|
|
|
// Restored to full on the restore duration: back up.
|
|
|
|
|
if (Broken && _reserve.Charge >= _reserve.Capacity - .01f) Broken = false;
|
|
|
|
|
}
|
|
|
|
|
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
// Cut 4 (docs/stats-and-power-cut.md, Cut 4): a hit taken, not a chosen activation, drawing from the
|
|
|
|
|
// shield's own continuous reserve instead of the entity's shared bus capacitors -- closing Cut 3's named,
|
|
|
|
|
// temporary exception. A consumable-hosted instance (Item == null) has no PowerBus entry, so nothing would
|
|
|
|
|
// ever fill this reserve; bypass it rather than starving such an instance forever.
|
2026-09-19 02:46:17 +02:00
|
|
|
//
|
F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00
|
|
|
// F5 (docs/stats-and-power-cut.md, Soul pass 2026-09-19): this used to mutate -- breaking the shield and
|
|
|
|
|
// draining the reserve -- as a side effect of the query itself, on the premise that "every caller queries
|
|
|
|
|
// CanTakeHit exactly once per hit." That premise was false: RaycastAll returns both the shield collider and
|
|
|
|
|
// the hull collider for one shot, and every caller under Assets/Scripts/Gameplay/Weapons queries this twice
|
|
|
|
|
// per hit (once deciding whether the shield collider absorbs it, once deciding whether the hull collider is
|
|
|
|
|
// exposed). A caller must be able to ask without consequence, so this is now a pure read: Broken and the
|
|
|
|
|
// reserve's own CanSpend, nothing more. See Break() below for where the mutation moved.
|
2021-03-06 01:04:26 +00:00
|
|
|
public bool CanTakeHit(DamageType type, float damage)
|
|
|
|
|
{
|
2026-09-19 02:46:17 +02:00
|
|
|
if (Item == null) return true;
|
|
|
|
|
if (Broken) return false;
|
F4/F5: shield restore survives inactivity, and CanTakeHit stops mutating
F4: a hit that breaks the shield usually also cooks it (TakeHit's own
AddHeat), which can knock the item thermally offline in the same tick. The
reserve's recharge and its Broken-clearing check lived inside Execute, which
Entity.Update only runs while Item.Active is true -- so a shield broken by a
hit that also shuts it down could never come back, no matter how long
RestoreDuration allows for. That logic now lives in Shield.Update
(IAlwaysUpdatedBehavior), which Entity.Update calls every tick unconditionally
(Cooldown.cs already relies on the same interface for the same reason).
F5: CanTakeHit broke the shield and drained the reserve as a side effect of
the query itself, on the premise that every caller queries it exactly once
per hit. That premise was false: RaycastAll/SphereCastAll/OverlapSphere hand
every caller under Assets/Scripts/Gameplay/Weapons both the shield collider
and the hull collider for one shot, so the same shield's CanTakeHit was
queried twice per hit. CanTakeHit is now a pure read (Broken + the reserve's
own CanSpend); the mutation moved to a new Shield.Break(), idempotent, called
by each weapon file exactly where it already decided to route a hit past the
shield instead of absorbing it.
Pinned in tests/Aetheria.Shared.Tests/ShieldReserveTests.cs:
RestorationProceedsWhileTheItemIsInactive (F4),
CanTakeHitNeverMutatesRegardlessOfHowManyTimesItIsCalled and
BreakIsIdempotentAcrossRepeatedCalls (F5). Updated every existing test that
relied on CanTakeHit's old mutating side effect to call Break() explicitly,
matching the new caller pattern.
Cannot compile-verify Assets/Scripts/Gameplay/Weapons/*.cs here (UnityEngine,
Unity batchmode unavailable in this environment) -- reviewed by hand for
brace/control-flow correctness; Self or a Unity batchmode pass should confirm.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 05:32:29 +02:00
|
|
|
return _reserve.CanSpend(damage * EnergyUsage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// F5: the mutation CanTakeHit used to perform when it returned false, now a caller invokes explicitly at the
|
|
|
|
|
// one point damage is actually applied -- specifically, the point a caller decides to route a hit past this
|
|
|
|
|
// shield (to the hull) instead of absorbing it, which is also the only point that decision is made at all,
|
|
|
|
|
// so this is naturally called at most once per hit despite the double CanTakeHit query above. Idempotent
|
|
|
|
|
// (a no-op once Broken is already true) as a defense-in-depth against a caller structure that cannot
|
|
|
|
|
// guarantee single-call discipline, not as licence to call it more than once on purpose.
|
|
|
|
|
public void Break()
|
|
|
|
|
{
|
|
|
|
|
if (Item == null || Broken) return;
|
2026-09-19 02:46:17 +02:00
|
|
|
Broken = true;
|
|
|
|
|
_reserve.AddCharge(-_reserve.Charge); // emptied by the break, not left at its pre-hit charge
|
2021-03-06 01:04:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void TakeHit(DamageType type, float damage)
|
|
|
|
|
{
|
Cut 4: input capacitors close the last direct capacitor spend
Deletes Entity.CanSpendCapacitorCharge/TrySpendCapacitorCharge (and the
now-dead _capacitors bookkeeping list that only fed them) -- Cut 3's
named, temporary exception for the four instant draws. InstantWeapon
(both fire sites), Sensor.Ping, and Shield.CanTakeHit/TakeHit now spend
from their own InputCapacitor, filled continuously by PowerBus like
every other IPowerConsumer instead of touching the entity's shared bus
capacitors directly.
InputCapacitor's Capacity/Rate derive from the owning item's Energy and
Cooldown, with an authored-override hook (ChargedWeapon repurposes its
previously-unused ChargeEnergy stat as that override, per the map's own
instruction, rather than adding a new field). A weapon/sensor fire
attempt only ever spends exactly Capacity, so it only ever succeeds at
full charge -- no fractional shot. Shield is the map's named exception:
its reserve is continuous, not an activation buffer, so it allows a
partial draw; its size (no Energy/Cooldown pair exists on Shield) is a
flagged judgment call, not a spec'd formula.
Adds InputCapacitorTests.cs (8 tests) and
mutation_tests_stats_power_cut4.py (7 mutations, all behave as
declared); removes the one PowerBusTests.cs test and Cut 3 mutation
case that exercised the now-deleted Entity methods.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:52:15 +02:00
|
|
|
if (Item != null) _reserve.TrySpend(damage * EnergyUsage);
|
2021-04-18 05:13:20 +01:00
|
|
|
AddHeat(damage / Efficiency);
|
2021-03-06 01:04:26 +00:00
|
|
|
}
|
2021-04-29 00:21:33 +01:00
|
|
|
|
|
|
|
|
public virtual float Progress => Item.ThermalPerformance;
|
2026-08-10 14:57:50 +01:00
|
|
|
}
|