2021-01-25 05:53:36 +00:00
|
|
|
using System;
|
|
|
|
|
using System.Collections;
|
|
|
|
|
using System.Collections.Generic;
|
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 InstantWeaponData : WeaponData
|
|
|
|
|
{
|
|
|
|
|
[Inspectable, JsonProperty("count"), Key(17)]
|
|
|
|
|
public PerformanceStat Count = new PerformanceStat();
|
|
|
|
|
|
|
|
|
|
[Inspectable, JsonProperty("burstTime"), Key(18)]
|
|
|
|
|
public PerformanceStat BurstTime = new PerformanceStat();
|
|
|
|
|
|
|
|
|
|
[Inspectable, JsonProperty("cooldown"), Key(19), RuntimeInspectable]
|
|
|
|
|
public PerformanceStat Cooldown = new PerformanceStat();
|
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
[Inspectable, JsonProperty("ammoInterval"), Key(20)]
|
2026-08-10 14:57:50 +01:00
|
|
|
public bool SingleAmmoBurst;
|
|
|
|
|
|
|
|
|
|
public override Behavior CreateInstance(EquippedItem item)
|
|
|
|
|
{
|
|
|
|
|
return new InstantWeapon(this, item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override Behavior CreateInstance(ConsumableItemEffect item)
|
|
|
|
|
{
|
|
|
|
|
return new InstantWeapon(this, item);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-01-25 05:53:36 +00:00
|
|
|
|
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 class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
|
2021-01-25 05:53:36 +00:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
private InstantWeaponData _data;
|
2021-01-25 05:53:36 +00:00
|
|
|
|
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): replaces the direct Entity.TrySpendCapacitorCharge spend at
|
|
|
|
|
// both Trigger() and Execute() below -- the named, temporary exception Cut 3 left standing.
|
|
|
|
|
protected readonly InputCapacitor _capacitor = new InputCapacitor();
|
|
|
|
|
|
2021-01-25 05:53:36 +00:00
|
|
|
protected int _burstRemaining;
|
Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:31:59 +02:00
|
|
|
private bool _burstPaid;
|
2026-09-29 21:16:52 +02:00
|
|
|
private bool _burstStarted;
|
2021-01-25 05:53:36 +00:00
|
|
|
private float _burstTimer;
|
|
|
|
|
private float _burstInterval;
|
|
|
|
|
protected float _cooldown; // Normalized
|
2021-02-01 08:13:14 +00:00
|
|
|
private int _ammo = 0;
|
2021-01-30 01:02:11 +00:00
|
|
|
protected bool _coolingDown;
|
2021-02-26 19:01:47 +00:00
|
|
|
|
2021-01-31 03:52:32 +00:00
|
|
|
public float BurstCount { get; protected set; }
|
|
|
|
|
public float BurstTime { get; protected set; }
|
|
|
|
|
public float Cooldown { get; protected set; }
|
2021-02-26 19:01:47 +00:00
|
|
|
public virtual bool CanFire
|
|
|
|
|
{
|
2026-09-29 21:16:52 +02:00
|
|
|
// A triggered burst that has not yet fired a round is not ready either: its cooldown has not started.
|
|
|
|
|
get => !_coolingDown && (_burstStarted || _burstRemaining == 0);
|
2021-02-26 19:01:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override float DamagePerSecond => Damage / Cooldown;
|
|
|
|
|
public override float RangeDamagePerSecond(float range)
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range))) / Cooldown;
|
2021-02-26 19:01:47 +00:00
|
|
|
}
|
2021-01-25 05:53:36 +00:00
|
|
|
|
|
|
|
|
public override int Ammo
|
|
|
|
|
{
|
|
|
|
|
get => _ammo;
|
|
|
|
|
}
|
|
|
|
|
public virtual float Progress => saturate(_cooldown);
|
|
|
|
|
|
|
|
|
|
public event Action OnReloadBegin;
|
|
|
|
|
public event Action OnReloadComplete;
|
|
|
|
|
public event Action OnCooldownComplete;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
// Cut 3 (docs/fire-control-cut.md): carries the ShotId FireControl.Fire assigned, so a Unity effect
|
|
|
|
|
// manager can bind its presentation to the one shot it belongs to instead of applying damage itself.
|
|
|
|
|
public event Action<int> OnFire;
|
2021-01-25 05:53:36 +00:00
|
|
|
|
2021-03-21 01:55:25 +00:00
|
|
|
public virtual void ResetEvents()
|
2021-01-31 03:52:32 +00:00
|
|
|
{
|
|
|
|
|
OnReloadBegin = null;
|
|
|
|
|
OnReloadComplete = null;
|
|
|
|
|
OnCooldownComplete = null;
|
|
|
|
|
OnFire = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
public InstantWeapon(InstantWeaponData data, EquippedItem item) : base(data, item)
|
2026-06-15 20:12:34 +01:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_data = data;
|
|
|
|
|
_ammo = data.MagazineSize;
|
2026-06-15 20:12:34 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
public InstantWeapon(InstantWeaponData data, ConsumableItemEffect item) : base(data, item)
|
2021-01-25 05:53:36 +00:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_data = data;
|
|
|
|
|
_ammo = data.MagazineSize;
|
2021-04-18 05:13:20 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-25 05:53:36 +00:00
|
|
|
protected void Trigger()
|
|
|
|
|
{
|
Gate weapon fire on the shooter's own stance, shared by player and AI
Weapon.StanceAllowsFire is the one shared gate: true with no target, else the
shooter's own Entity.IsHostileTo(Target) -- not the target's stance toward the
shooter. It lives on the Weapon base class so every weapon subtype and every
caller (player input, TurretController, AI) goes through the same rule.
Wired at the two places weapons actually start dealing damage:
- InstantWeapon.Trigger (the base for InstantWeapon, AutoWeapon, ChargedWeapon
and LockWeapon/Launcher, all of which route real firing through Trigger).
- ConstantWeapon.Execute's continuous-beam loop, which stops firing (and
raises OnStopFiring) the moment the shooter's stance disallows it.
Also fixes LockWeapon's lock-building condition, which read the wrong
direction (target.IsHostileTo(shooter) instead of shooter.IsHostileTo(target))
-- the same backwards check as the HUD lock indicator, fixed alongside this in
ActionGameManager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:00:59 +02:00
|
|
|
// Safed: shooter has a target and hasn't declared hostility toward it.
|
|
|
|
|
if (!StanceAllowsFire) return;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
// Cut 3 (docs/fire-control-cut.md, Q2): arc-gated the same way AI and turret fire are (Weapon.
|
|
|
|
|
// ArcAllowsFire), and applied here because this is the one place every shooter's trigger passes
|
|
|
|
|
// through -- a player's action-bar Activate() reaches this exactly the same way Combat.cs's and
|
|
|
|
|
// TurretController.cs's Activate() calls do.
|
|
|
|
|
if (!ArcAllowsFire) return;
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
// A refused round is free (operator ruling 2026-09-30): no ammo, energy or cooldown is spent on a burst
|
|
|
|
|
// whose first round FireControl would refuse.
|
|
|
|
|
if (FireControl.Refuses(this, Entity)) return;
|
Gate weapon fire on the shooter's own stance, shared by player and AI
Weapon.StanceAllowsFire is the one shared gate: true with no target, else the
shooter's own Entity.IsHostileTo(Target) -- not the target's stance toward the
shooter. It lives on the Weapon base class so every weapon subtype and every
caller (player input, TurretController, AI) goes through the same rule.
Wired at the two places weapons actually start dealing damage:
- InstantWeapon.Trigger (the base for InstantWeapon, AutoWeapon, ChargedWeapon
and LockWeapon/Launcher, all of which route real firing through Trigger).
- ConstantWeapon.Execute's continuous-beam loop, which stops firing (and
raises OnStopFiring) the moment the shooter's stance disallows it.
Also fixes LockWeapon's lock-building condition, which read the wrong
direction (target.IsHostileTo(shooter) instead of shooter.IsHostileTo(target))
-- the same backwards check as the HUD lock indicator, fixed alongside this in
ActionGameManager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 14:00:59 +02:00
|
|
|
|
Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:31:59 +02:00
|
|
|
// A burst that pays once (SingleAmmoBurst) pays at its first round that is not refused, in Execute, judged
|
|
|
|
|
// with the Range that round flies with: paying here would judge with the Range of the previous tick, and a
|
|
|
|
|
// Range that fell in between would spend the ammo on rounds Fire then refuses.
|
|
|
|
|
_burstPaid = false;
|
2021-01-31 03:52:32 +00:00
|
|
|
_burstRemaining = (int) BurstCount;
|
|
|
|
|
_burstInterval = BurstTime / _burstRemaining;
|
2021-01-25 05:53:36 +00:00
|
|
|
_burstTimer = 0;
|
2026-09-29 21:16:52 +02:00
|
|
|
// The cooldown starts with the burst's first round that is not refused (Execute), not here: a burst whose
|
|
|
|
|
// every round is refused fires nothing and leaves no cooldown behind.
|
|
|
|
|
_burstStarted = false;
|
2021-01-25 05:53:36 +00:00
|
|
|
}
|
|
|
|
|
|
2021-01-31 03:52:32 +00:00
|
|
|
protected override void UpdateStats()
|
|
|
|
|
{
|
|
|
|
|
base.UpdateStats();
|
2026-08-10 14:57:50 +01:00
|
|
|
BurstCount = Evaluate(_data.Count);
|
|
|
|
|
BurstTime = Evaluate(_data.BurstTime);
|
|
|
|
|
Cooldown = Evaluate(_data.Cooldown);
|
2021-02-01 08:13:14 +00:00
|
|
|
|
|
|
|
|
Damage /= (int) BurstCount;
|
|
|
|
|
Heat /= (int) BurstCount;
|
|
|
|
|
Energy /= (int) BurstCount;
|
|
|
|
|
}
|
|
|
|
|
|
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 §7 Q4): capacity and rate, resolved fresh rather than off the cached
|
|
|
|
|
// Energy/Cooldown properties above -- Capacitor.ResolveCapacity's precedent (PowerBus.Step, which calls
|
|
|
|
|
// PowerRequest below, runs before this behaviour's own Execute this tick, so those cached properties would
|
|
|
|
|
// still hold last tick's value). Capacity is one shot's own energy cost, exactly what the two deleted
|
|
|
|
|
// direct-spend call sites measured. Default rate keeps sustained fire exactly at Cooldown's pace when the
|
|
|
|
|
// bus grants the full request; RateOverride lets an owner replace that (ChargedWeapon does).
|
|
|
|
|
protected virtual float RateOverride => 0f;
|
|
|
|
|
|
2026-09-19 06:58:57 +02:00
|
|
|
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Count, Energy and
|
|
|
|
|
// Cooldown are all registered request fields (StatValidation.PowerRequestFields) -- read nominally here so
|
|
|
|
|
// the capacitor's own Capacity/Rate this feeds into PowerRequest below never depends on this tick's own
|
|
|
|
|
// grant. Nothing here needs the real, curved value: PowerRequest only ever asks "how much would a full shot
|
|
|
|
|
// cost," and the actual charge added back in Execute is separately scaled by Item.PowerSupply.
|
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
|
|
|
protected void RefreshInputCapacitor()
|
|
|
|
|
{
|
2026-09-19 06:58:57 +02:00
|
|
|
var burstCount = max(1, (int) EvaluateNominalPower(_data.Count));
|
|
|
|
|
var perShotEnergy = EvaluateNominalPower(_data.Energy) / burstCount;
|
|
|
|
|
var cooldown = EvaluateNominalPower(_data.Cooldown);
|
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
|
|
|
_capacitor.UpdateStats(perShotEnergy, cooldown, rateOverride: RateOverride);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cut 4: the request PowerBus needs before Execute runs, mirroring EnergyDraw's IPowerConsumer pattern.
|
|
|
|
|
public float PowerRequest(float dt)
|
|
|
|
|
{
|
|
|
|
|
RefreshInputCapacitor();
|
|
|
|
|
return _capacitor.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): Low -- offense. The ruling's own example of what
|
|
|
|
|
// a reactor throttle should sacrifice first.
|
|
|
|
|
public int DefaultPowerTier => PowerTiers.Low;
|
|
|
|
|
|
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: whole-or-nothing against this behaviour's own input capacitor -- the operator's ruling ("no item
|
|
|
|
|
// ever receives a fraction of a shot") means a fire attempt only ever succeeds at full charge, so the cost
|
|
|
|
|
// spent is always exactly Capacity, never the possibly-stale cached Energy. A consumable-hosted instance
|
|
|
|
|
// (Item == null) has no PowerBus entry (Behaviors.cs: "no PowerBus entry ... always succeeds"), so nothing
|
|
|
|
|
// would ever fill this capacitor -- bypass it entirely rather than starving such an instance forever.
|
|
|
|
|
protected bool TrySpendActivationEnergy() => Item == null || _capacitor.TrySpend(_capacitor.Capacity);
|
|
|
|
|
|
2021-02-01 08:13:14 +00:00
|
|
|
private bool UseAmmo()
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
if (_data.MagazineSize <= 1) return true;
|
|
|
|
|
|
2021-03-22 06:10:00 +00:00
|
|
|
if (_ammo > 0)
|
2021-02-01 08:13:14 +00:00
|
|
|
{
|
2021-03-22 06:10:00 +00:00
|
|
|
_ammo--;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
|
2021-03-22 06:10:00 +00:00
|
|
|
var hasAmmo = true;
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
if (_data.AmmoType.IsSet())
|
2021-03-22 06:10:00 +00:00
|
|
|
{
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
var cargo = Entity.FindItemInCargo(_data.AmmoType.Key);
|
2021-03-22 06:10:00 +00:00
|
|
|
if (cargo != null)
|
2021-03-22 05:22:31 +00:00
|
|
|
{
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
var item = cargo.ItemsOfType[_data.AmmoType.Key][0];
|
2021-03-22 06:10:00 +00:00
|
|
|
if (item is SimpleCommodity simpleCommodity)
|
|
|
|
|
cargo.Remove(simpleCommodity, 1);
|
2021-02-01 08:13:14 +00:00
|
|
|
}
|
2021-03-22 06:10:00 +00:00
|
|
|
else hasAmmo = false;
|
|
|
|
|
}
|
|
|
|
|
if(hasAmmo)
|
|
|
|
|
{
|
|
|
|
|
OnReloadBegin?.Invoke();
|
|
|
|
|
_cooldown = 1;
|
|
|
|
|
_coolingDown = true;
|
|
|
|
|
_firing = false;
|
2021-02-01 08:13:14 +00:00
|
|
|
}
|
2021-03-22 06:10:00 +00:00
|
|
|
_burstRemaining = 0;
|
|
|
|
|
return false;
|
2021-02-01 08:13:14 +00:00
|
|
|
|
2021-01-31 03:52:32 +00:00
|
|
|
}
|
|
|
|
|
|
2021-04-14 03:29:14 +01:00
|
|
|
public override bool Execute(float dt)
|
2021-01-25 05:53:36 +00:00
|
|
|
{
|
2021-04-14 03:29:14 +01:00
|
|
|
base.Execute(dt);
|
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: this tick's grant, read back via Item.PowerSupply -- Item is non-null here because a null-Item
|
|
|
|
|
// instance never reaches PowerBus.Step (see TrySpendActivationEnergy) and so never accrues charge.
|
|
|
|
|
if (Item != null)
|
|
|
|
|
_capacitor.AddCharge(_capacitor.RequestedFill(dt) * Item.PowerSupply);
|
2021-01-25 05:53:36 +00:00
|
|
|
if (_coolingDown)
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_cooldown -= dt / (_data.MagazineSize > 0 && _ammo == 0 ? _data.ReloadTime : Cooldown);
|
2021-01-25 05:53:36 +00:00
|
|
|
if (_cooldown < 0)
|
|
|
|
|
{
|
|
|
|
|
_coolingDown = false;
|
2026-08-10 14:57:50 +01:00
|
|
|
if (_data.MagazineSize > 0 && _ammo == 0)
|
2021-01-25 05:53:36 +00:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_ammo = _data.MagazineSize;
|
2021-01-25 05:53:36 +00:00
|
|
|
OnReloadComplete?.Invoke();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
OnCooldownComplete?.Invoke();
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-04-30 06:39:57 +01:00
|
|
|
|
|
|
|
|
var firedThisFrame = false;
|
2021-04-14 03:29:14 +01:00
|
|
|
_burstTimer += dt;
|
2021-01-25 05:53:36 +00:00
|
|
|
while (_burstRemaining > 0 && _burstTimer > 0)
|
|
|
|
|
{
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
// A fused round whose arming distance Range cannot reach is refused, per round, before anything is
|
|
|
|
|
// spent (operator ruling 2026-09-30): it costs no ammo, energy, sound, heat, wear or visibility.
|
|
|
|
|
// The decision is FireControl's; Fire makes the same one.
|
|
|
|
|
var refused = FireControl.Refuses(this, Entity);
|
Fuse fixes: arming holds for every burst, no-lock rounds stop on the first hull, settle before user code
Operator ruling 2026-09-30: a no-lock round of any fuse stops at the first hull
on its aim line and detonates there (bomb fishing still bursts at max range on
a clear line). CommitBurst no longer special-cases Contact.
Soul's findings on 14f66634..7e620ff9:
- Arming (HIGH): a contact or delayed blast on a hull nearer the muzzle than the
blast radius detonated at the hull. Armed is now the one owner of the arming
rule; Solve plans its burst point through it and ApplyBlastHit pushes the hull
point through it.
- A refused beam never starts (ConstantWeapon.Activate asks Refuses), so
OnStartFiring no longer flashes visuals and audio for a beam that fires nothing.
- Step removes a shot from the queue before Apply runs damage observers, so a
throwing observer cannot make the blast repeat every tick.
- AgentFires consults Solve for a fused weapon: a refused weapon is never fired
and Combat's group pick no longer counts its damage.
- SingleAmmoBurst pays at its first unrefused round in Execute, judged with the
Range that round flies with, instead of at the trigger on the previous tick's.
- Inspect reads Designated from Solve (FireSolution.Designated) instead of
recomputing visible, in-range and locked; FireControlDiagnostic drops those
three fields and the debug HUD shows the one. Combat's group range test is
FireControl.InReach, the range half of Designated (a LockWeapon must be
activated to acquire its lock, so Combat cannot ask Designated itself).
Tests pin each rule, plus the two surviving gaps: a pushed-out targeted round's
arrival at the arming distance, and the AgentMinHitProbability boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 20:31:59 +02:00
|
|
|
// Ammo and energy are consumed here, per round, or once per burst at its first unrefused round
|
|
|
|
|
// (SingleAmmoBurst). UseAmmo returns false when triggering reload; cancel firing if that is the case
|
|
|
|
|
if (!refused && !_burstPaid && (!TrySpendActivationEnergy() || !UseAmmo()))
|
2021-03-02 23:02:19 +00:00
|
|
|
{
|
|
|
|
|
_burstRemaining = 0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-09-29 21:16:52 +02:00
|
|
|
if (!refused)
|
|
|
|
|
{
|
|
|
|
|
_burstPaid = _data.SingleAmmoBurst;
|
|
|
|
|
if (!_burstStarted)
|
|
|
|
|
{
|
|
|
|
|
_burstStarted = true;
|
2026-09-29 22:15:51 +02:00
|
|
|
_cooldown = 1 - dt / Cooldown;
|
|
|
|
|
// The round starts the cooldown after this tick's decrement above: charge the tick to it, so the cadence stays Cooldown
|
2026-09-29 21:16:52 +02:00
|
|
|
_coolingDown = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
|
2021-01-25 05:53:36 +00:00
|
|
|
_burstRemaining--;
|
|
|
|
|
_burstTimer -= _burstInterval;
|
Fuse batch 5: agents fire per weapon, Solve owns the burst, refusal is free, arming is the blast radius
Operator rulings 2026-09-30.
* Combat and TurretController ask FireControl.AgentFires for each weapon. A
fused weapon fires whenever its target is designated (Designated, the one
visible / in-range / locked predicate PFire already used), whatever the arc;
an agent never fires one at nothing. A direct-hit weapon keeps the
AgentMinHitProbability threshold. The group's first weapon no longer speaks
for the group, and the turret's own range test is gone.
* FireControl.Solve is the one decision about what a round does, extracted
from Fire: Direct, Burst at a reach, or Refused. Fire, Inspect and the
weapons' refusal check all read it. Inspect gains Outcome and BurstReach and
the debug HUD prints them for fused weapons.
* A refused round costs nothing. InstantWeapon asks before its ammo and energy
spend (at the trigger and at each round), ConstantWeapon folds the refusal
into the gate its power request and Execute already share, so a refused beam
draws no power, wear, heat or visibility and stops.
* The arming distance is BlastRadius, measured from the shooter's position and
blind to hull size. HullAhead's exit, ArmingDistance and
PendingShot.ArmingDistance are deleted; HullAhead survives as HullEntry for
the contact fuse. A round fired at a valid target inside the radius is pushed
out along its line but keeps its target, so a departed target cancels it.
* Step settles a shot's queue state before publishing about it, so a throwing
presentation observer costs its own notification and cannot re-commit or
re-resolve the shot on every later tick.
Tests: FireControlPerWeaponTests (new; includes a fused-beam fixture), the
arming tests in FireControlCut124Tests rewritten to the literal distance,
the hull-inclusive probe premises deleted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 18:36:59 +02:00
|
|
|
if (refused) continue;
|
Fire control Cut 3: hit authority moves to FireControl
Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the
raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and
Lightning) and replaces it with a single owner in the same cut: FireControl
now decides whether a shot hits, Zone owns the pending-shot lifecycle, and
Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the
result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies
nothing.
FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor
* pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the
weapon's payload and the shooter's resolved targeting stats at the instant the
trigger is pulled (Q6), computes the predicted intercept through the one
shared PredictedIntercept function (CombatState now calls the same function
instead of duplicating first_order_intercept), and queues a PendingShot.
FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon --
deviation from the predicted intercept, measured once, live, at that instant
-- and applies it at ArrivalTime. A shot whose flight is shorter than the
horizon commits and resolves in the same tick it fires. Rolls draw from the
shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc
or otherwise gated shot draws nothing.
Player fire is arc-gated through the same predicate AI and turret fire use
(Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every
shooter's trigger passes through), closing Q2.
Adds GameplaySettings.CommitHorizon, SchematicCellSize and
AgentMinHitProbability (first guesses; the headless suite is the tuning
harness, operator play smoke is the arbiter).
FORK reported, not worked around silently: HullCollider.SendHit/SendSplash
cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs,
ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them
from their own live Physics queries -- that's Cut 4's migration. They fire
into the void today: nothing subscribes to Hit/Splash any more for the four
weapon kinds this cut owns.
tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the
gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its
evasion window, short-flight degradation, the frozen-payload snapshot, aimed
hits, the item-then-hull damage order, the planar penetration march and the
shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are
updated for the new AI gate and OnFire(int) signature.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
|
|
|
// Cut 3: this is fire authority's one entry point. FireControl.Fire freezes the payload snapshot
|
|
|
|
|
// (Q6 -- the gun's own stats at this exact instant, base.Execute(dt) above already refreshed them
|
|
|
|
|
// this tick) and queues a PendingShot; nothing downstream re-evaluates a stat.
|
2026-09-19 15:16:57 +02:00
|
|
|
// Cut 4 fix: FireControl.Fire must run whether or not anything is listening -- inlined into
|
|
|
|
|
// `OnFire?.Invoke(...)` it looked equivalent but was not. C#'s null-conditional short-circuits the
|
|
|
|
|
// whole expression, argument included, whenever OnFire has no subscriber (a headless run with no
|
|
|
|
|
// Unity EntityInstance wiring, docs/headless-playground-cut.md), so a shot silently never fired.
|
|
|
|
|
// Evaluate it into a local first.
|
|
|
|
|
var shotId = FireControl.Fire(this, Item, Entity);
|
|
|
|
|
OnFire?.Invoke(shotId);
|
2021-04-30 06:39:57 +01:00
|
|
|
if(!firedThisFrame)
|
|
|
|
|
{
|
|
|
|
|
Item.FireAudioEvent(WeaponAudioEvent.Fire);
|
|
|
|
|
firedThisFrame = true;
|
|
|
|
|
}
|
2021-04-18 05:13:20 +01:00
|
|
|
CauseWearDamage(1);
|
|
|
|
|
AddHeat(Heat);
|
|
|
|
|
Entity.VisibilitySources[this] = Visibility;
|
2021-01-25 05:53:36 +00:00
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override void Activate()
|
|
|
|
|
{
|
2021-04-07 01:27:35 +01:00
|
|
|
if(CanFire)
|
|
|
|
|
Trigger();
|
|
|
|
|
base.Activate();
|
2021-01-25 05:53:36 +00:00
|
|
|
}
|
2026-08-10 14:57:50 +01:00
|
|
|
}
|