Aetheria/Assets/Scripts/ServerShared/Behaviors/ConstantWeapon.cs

214 lines
8.8 KiB
C#
Raw Normal View History

2021-01-05 02:56:17 +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/. */
using System;
using MessagePack;
using Newtonsoft.Json;
using CultMath;
using static CultMath.math;
[Inspectable, MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class ConstantWeaponData : WeaponData
{
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(17)]
public float AmmoInterval = 1;
public override Behavior CreateInstance(EquippedItem item)
{
return new ConstantWeapon(this, item);
}
public override Behavior CreateInstance(ConsumableItemEffect item)
{
return new ConstantWeapon(this, item);
}
}
2021-01-05 02:56:17 +00:00
public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerConsumer
2021-01-05 02:56:17 +00:00
{
private ConstantWeaponData _data;
private int _ammo = 1;
private float _ammoInterval;
private float _reload;
private bool _reloading;
Fire control Cut 4, part 1: FireControl gains Splash and a beam roll FireControl.Fire takes an optional damageOverride so a continuous weapon can queue a shot for less than its full Damage. FireControl.Splash is the one new rule this cut adds: an unconditional area effect (mines, airburst) applying the shield-absorb-or-hull-takes-it branch per target, then DamageSchematic over the directional half of each target's own hull that faces the blast -- the rule moved verbatim from the Splash subscription EntityInstance.cs carried before Cut 3 deleted it (git show b7743789^). ConstantWeapon.Execute rolls once per GameplaySettings.BeamResolveInterval (new setting) for Damage*interval, through the same FireControl.Fire/Step pair a discrete shot uses -- a beam's authored Velocity is 0, so the flight commits and resolves in the same tick it fires (R4's short-flight degradation). A beam is a sequence of rolls, not a continuous truth. Fixes a real defect found while testing the above: both InstantWeapon.Execute (Cut 3) and the new ConstantWeapon.Execute wrote `OnFire?.Invoke(FireControl. Fire(...))` inline. C#'s null-conditional short-circuits the whole expression, argument included, whenever the event has no subscriber -- true of any headless run with no Unity EntityInstance wired up (docs/headless-playground-cut.md) -- so a shot silently never fired. Both now evaluate FireControl.Fire into a local before the null-conditional invoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:16:57 +02:00
// Cut 4 (docs/fire-control-cut.md): accumulates while firing; rolled off every time it reaches
// GameplaySettings.BeamResolveInterval, through FireControl.Fire/Step exactly like a discrete shot.
private float _beamTimer;
public override int Ammo
{
get => _ammo;
}
public float Progress
{
get { return saturate(_reload); }
}
public override float DamagePerSecond => Damage;
public override float RangeDamagePerSecond(float range)
{
return Damage * _data.DamageCurve.Evaluate(saturate(unlerp(MinRange, Range, range)));
}
2021-01-05 02:56:17 +00:00
public event Action OnReloadBegin;
public event Action OnReloadComplete;
2021-01-05 02:56:17 +00:00
public event Action OnStartFiring;
public event Action OnStopFiring;
Fire control Cut 4, part 1: FireControl gains Splash and a beam roll FireControl.Fire takes an optional damageOverride so a continuous weapon can queue a shot for less than its full Damage. FireControl.Splash is the one new rule this cut adds: an unconditional area effect (mines, airburst) applying the shield-absorb-or-hull-takes-it branch per target, then DamageSchematic over the directional half of each target's own hull that faces the blast -- the rule moved verbatim from the Splash subscription EntityInstance.cs carried before Cut 3 deleted it (git show b7743789^). ConstantWeapon.Execute rolls once per GameplaySettings.BeamResolveInterval (new setting) for Damage*interval, through the same FireControl.Fire/Step pair a discrete shot uses -- a beam's authored Velocity is 0, so the flight commits and resolves in the same tick it fires (R4's short-flight degradation). A beam is a sequence of rolls, not a continuous truth. Fixes a real defect found while testing the above: both InstantWeapon.Execute (Cut 3) and the new ConstantWeapon.Execute wrote `OnFire?.Invoke(FireControl. Fire(...))` inline. C#'s null-conditional short-circuits the whole expression, argument included, whenever the event has no subscriber -- true of any headless run with no Unity EntityInstance wired up (docs/headless-playground-cut.md) -- so a shot silently never fired. Both now evaluate FireControl.Fire into a local before the null-conditional invoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:16:57 +02:00
// Cut 4 (docs/fire-control-cut.md): carries the ShotId each beam roll's FireControl.Fire assigned, the
// same handle InstantWeapon.OnFire gives discrete shots. No presentation binds to it yet -- kept for a
// future pass, same as Laser.ShotId.
public event Action<int> OnBeamShot;
public void ResetEvents()
{
OnReloadBegin = null;
OnReloadComplete = null;
OnStartFiring = null;
OnStopFiring = null;
Fire control Cut 4, part 1: FireControl gains Splash and a beam roll FireControl.Fire takes an optional damageOverride so a continuous weapon can queue a shot for less than its full Damage. FireControl.Splash is the one new rule this cut adds: an unconditional area effect (mines, airburst) applying the shield-absorb-or-hull-takes-it branch per target, then DamageSchematic over the directional half of each target's own hull that faces the blast -- the rule moved verbatim from the Splash subscription EntityInstance.cs carried before Cut 3 deleted it (git show b7743789^). ConstantWeapon.Execute rolls once per GameplaySettings.BeamResolveInterval (new setting) for Damage*interval, through the same FireControl.Fire/Step pair a discrete shot uses -- a beam's authored Velocity is 0, so the flight commits and resolves in the same tick it fires (R4's short-flight degradation). A beam is a sequence of rolls, not a continuous truth. Fixes a real defect found while testing the above: both InstantWeapon.Execute (Cut 3) and the new ConstantWeapon.Execute wrote `OnFire?.Invoke(FireControl. Fire(...))` inline. C#'s null-conditional short-circuits the whole expression, argument included, whenever the event has no subscriber -- true of any headless run with no Unity EntityInstance wired up (docs/headless-playground-cut.md) -- so a shot silently never fired. Both now evaluate FireControl.Fire into a local before the null-conditional invoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:16:57 +02:00
OnBeamShot = null;
}
2021-01-05 02:56:17 +00:00
public ConstantWeapon(ConstantWeaponData data, EquippedItem item) : base(data, item)
{
_data = data;
}
2021-01-05 02:56:17 +00:00
public ConstantWeapon(ConstantWeaponData data, ConsumableItemEffect item) : base(data, item)
{
_data = data;
}
// Cut 3 (docs/stats-and-power-cut.md): the request the bus needs before Execute runs -- what continuing to
// fire this tick would cost, or nothing when not firing or safed. _firing is set externally (Activate/
// Deactivate) before Entity.Update calls PowerBus.Step, so it is already current when this runs.
Resolve request/brownout conflict: evaluate power requests nominally A power request may not depend on power supply (Cut 6/F6), and continuous consumers brown out through a power term on their own performance stats (Cut 7). For Radiator (PumpedHeat) and AetherDrive (Torque) those were the same stat, making six shipped catalog records illegal and leaving six BrownoutTests cases skipped. Resolved per the operator's ruling: a power request is evaluated nominally -- what the item wants at full supply, not what it is currently managing. EquippedItem.EvaluateNominalPower (mirroring ConditionRatio's own NominalContext) pins only PowerSupplyFactor to 1; Heat/Durability stay live, since they are not the term a request would be circular through. Every PowerRequest/RefreshReserve/RefreshInputCapacitor implementation now reads a StatValidation.PowerRequestFields stat through it, so the same stat can carry a brownout curve and be a request field without the request depending on its own answer. StatValidation.ValidateNoPowerSupplyOnRequest (F6's static half of the rule, forbidding the term outright) is deleted -- nominal evaluation removes the circularity at the root instead. Only the dynamic modifier-chain half (StatModifier.ValidateNoPowerSupplyChain) still refuses anything, since EvaluateNominalPower forwards ScaleModifier/ ConstantModifier unchanged (real, not nominal). Un-skips BrownoutTests' six Radiator/AetherDrive cases and adds two new regression tests (a direct EvaluateNominalPower probe, and a two-tick Radiator demand check) backed by a new mutation harness. Updates the four PowerCurveTests/AetheriaStoresTests cases that pinned the now-superseded static refusal to instead assert acceptance. GameData/Aetheria.cc is unchanged and still validates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 06:58:57 +02:00
//
// Nominal-request ruling (docs/stats-and-power-cut.md, operator ruling 2026-09-19): Energy is a registered
// request field (StatValidation.PowerRequestFields) -- read nominally, same reasoning as every other
// IPowerConsumer in this cut. Damage (the field Cut 7 curves) is a separate stat base.Execute reads with the
// real Evaluate.
Fire control Cut 5: close the rules that landed on one path and not its twin Soul's pass over the whole campaign confirmed eleven findings. Seven were code defects of one shape -- a rule implemented on one path and left off the path beside it -- and this is all seven. 5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of zero, and Commit drops the zero branch that turned a Tracking-less shooter into a hard wall. Unaided fire was meant to be really, really bad; it was 334 shots and no hits against a target drifting at 5 u/s. 5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that recharges mid-flight cannot retroactively survive. Splash already did this; the discrete roll never did, which made the whole shield-break mechanic unreachable for every weapon in the catalog. 5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A side-mounted beam was firing forward. 5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into InArc, which used to normalize the zero vector into NaN. The trigger said fire and the probability said impossible. 5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops concentrating hits onto a dead subsystem's footprint. 5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal lived only in Unity's loot-drop subscription, so headless nothing ever left the zone, shots landed on corpses and no fight could end. 5.7 PendingShot drops PredictedIntercept and FlightTime, written and read nowhere. 208 tests pass (197 plus eleven new). Known defect, recorded rather than hidden: the committed mutation harness is unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs, leaving live mutations in the working tree -- including its own 5.1 mutation, which is how a green suite briefly reported two failures. Its verdicts are therefore evidence only where re-run from a verified-clean tree. Two anchors (Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and 5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's word, and the suite result above was taken by hand from a hash-verified tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:56:40 +02:00
// Cut 5 (docs/fire-control-cut.md, 5.3, Soul finding 9): StanceAllowsFire alone let a side-mounted beam
// fire forward -- ArcAllowsFire joins it here, matching InstantWeapon.cs's player arc gate (Q2: manual and
// programmatic are one truth, and a continuous weapon is no exception).
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
// Operator ruling 2026-09-30: a refused round is free, and a beam is a stream of rounds, so a fused beam whose
// arming distance exceeds its Range fires nothing and draws nothing -- no power, ammo, wear, heat or
// visibility. FireControl.Refuses is the one arming test; it reads the Range Execute last refreshed, the same
// one-tick lag every PowerRequest reads its stats with.
private bool MayFire => StanceAllowsFire && ArcAllowsFire && !FireControl.Refuses(this, Entity);
public float PowerRequest(float dt) => _firing && MayFire ? EvaluateNominalPower(_data.Energy) * dt : 0f;
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, same as InstantWeapon.
public int DefaultPowerTier => PowerTiers.Low;
public override bool Execute(float dt)
2021-01-05 02:56:17 +00:00
{
base.Execute(dt);
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 (_firing && !MayFire)
{
Fire control Cut 5: close the rules that landed on one path and not its twin Soul's pass over the whole campaign confirmed eleven findings. Seven were code defects of one shape -- a rule implemented on one path and left off the path beside it -- and this is all seven. 5.1 Tracking falls back to GameplaySettings.UnaidedTracking (new, 10) instead of zero, and Commit drops the zero branch that turned a Tracking-less shooter into a hard wall. Unaided fire was meant to be really, really bad; it was 334 shots and no hits against a target drifting at 5 u/s. 5.2 A shot the shield cannot absorb now breaks it. The decision is frozen into ShotOutcome.ShieldBroken at commit and performed in Apply, so a shield that recharges mid-flight cannot retroactively survive. Splash already did this; the discrete roll never did, which made the whole shield-break mechanic unreachable for every weapon in the catalog. 5.3 ConstantWeapon gates on ArcAllowsFire alongside StanceAllowsFire. A side-mounted beam was firing forward. 5.4 The point-blank bearing case moves out of Weapon.ArcAllowsFire and into InArc, which used to normalize the zero vector into NaN. The trigger said fire and the probability said impossible. 5.5 A destroyed item drops out of IsRevealed's ranking, so Precision stops concentrating hits onto a dead subsystem's footprint. 5.6 Zone subscribes Entity.Death and removes the corpse itself. That removal lived only in Unity's loot-drop subscription, so headless nothing ever left the zone, shots landed on corpses and no fight could end. 5.7 PendingShot drops PredictedIntercept and FlightTime, written and read nowhere. 208 tests pass (197 plus eleven new). Known defect, recorded rather than hidden: the committed mutation harness is unreliable. Across two runs it failed to restore FireControl.cs and Weapon.cs, leaving live mutations in the working tree -- including its own 5.1 mutation, which is how a green suite briefly reported two failures. Its verdicts are therefore evidence only where re-run from a verified-clean tree. Two anchors (Zone.cs's ObserveAdd line, Weapon.cs's InArc return) fail to match at all, and 5.3's ConstantWeaponObeysArc survives its own mutation, so that rule is written but not pinned. Cut 5b owns the repair; nothing here is trusted on the harness's word, and the suite result above was taken by hand from a hash-verified tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 16:56:40 +02:00
// Safed: shooter has a target and hasn't declared hostility toward it, or the target no longer
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
// bears (5.3: a beam obeys its arc exactly as InstantWeapon's trigger does), or a fused round is
// refused.
_firing = false;
OnStopFiring?.Invoke();
return false;
}
if (_firing)
{
// Cut 7 (docs/stats-and-power-target.md): base.Execute(dt) above already re-read Damage through
// Evaluate(_data.Damage), so a Damage stat carrying a PowerSupply term already fires this tick for
// less under a partial grant -- the weapon no longer safes itself off (the "flicking off gear" bug
// the ruling names) just because the grant fell short of 1. Only true zero supply still stops it.
if (Item != null && Item.PowerSupply <= 1e-4f)
{
_firing = false;
OnStopFiring?.Invoke();
return false;
}
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())
{
if (_reloading)
{
_reload -= dt / _data.ReloadTime;
if (_reload < 0)
{
_reloading = false;
OnReloadComplete?.Invoke();
}
return false;
}
_ammoInterval -= dt / _data.AmmoInterval;
if (_ammoInterval < 0)
{
_ammoInterval = 1;
if (_data.MagazineSize > 1 && _ammo > 0) _ammo--;
else
{
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);
if (cargo != null)
{
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];
if (item is SimpleCommodity simpleCommodity)
cargo.Remove(simpleCommodity, 1);
if(_data.MagazineSize > 1)
{
_reloading = true;
_reload = 1;
OnReloadBegin?.Invoke();
_firing = false;
OnStopFiring?.Invoke();
}
}
return false;
}
}
}
CauseWearDamage(dt);
AddHeat(Evaluate(_data.Heat) * dt);
Entity.SetSignature(this, SignatureChannel.LaunchFlash, Evaluate(_data.Visibility), Item);
Fire control Cut 4, part 1: FireControl gains Splash and a beam roll FireControl.Fire takes an optional damageOverride so a continuous weapon can queue a shot for less than its full Damage. FireControl.Splash is the one new rule this cut adds: an unconditional area effect (mines, airburst) applying the shield-absorb-or-hull-takes-it branch per target, then DamageSchematic over the directional half of each target's own hull that faces the blast -- the rule moved verbatim from the Splash subscription EntityInstance.cs carried before Cut 3 deleted it (git show b7743789^). ConstantWeapon.Execute rolls once per GameplaySettings.BeamResolveInterval (new setting) for Damage*interval, through the same FireControl.Fire/Step pair a discrete shot uses -- a beam's authored Velocity is 0, so the flight commits and resolves in the same tick it fires (R4's short-flight degradation). A beam is a sequence of rolls, not a continuous truth. Fixes a real defect found while testing the above: both InstantWeapon.Execute (Cut 3) and the new ConstantWeapon.Execute wrote `OnFire?.Invoke(FireControl. Fire(...))` inline. C#'s null-conditional short-circuits the whole expression, argument included, whenever the event has no subscriber -- true of any headless run with no Unity EntityInstance wired up (docs/headless-playground-cut.md) -- so a shot silently never fired. Both now evaluate FireControl.Fire into a local before the null-conditional invoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:16:57 +02:00
// Cut 4 (docs/fire-control-cut.md): a beam is a sequence of rolls, not a continuous truth -- one
// FireControl.Fire per BeamResolveInterval, for that interval's worth of Damage, through the same
// freeze-and-queue path a discrete shot uses (a beam's authored Velocity is 0, so the flight
// commits and resolves in this same tick, R4's short-flight degradation).
var interval = Entity.ItemManager.GameplaySettings.BeamResolveInterval;
_beamTimer += dt;
while (_beamTimer >= interval)
{
_beamTimer -= interval;
// FireControl.Fire must run whether or not anything is listening: `OnBeamShot?.Invoke(FireControl.Fire(...))`
// looks equivalent but is not -- C#'s null-conditional short-circuits the whole expression,
// argument included, when OnBeamShot has no subscriber (true today, R9's "no design uses it
// yet"), so the roll would silently never happen. Evaluate it into a local first.
var shotId = FireControl.Fire(this, Item, Entity, Damage * interval);
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
OnBeamShot?.Invoke(shotId);
Fire control Cut 4, part 1: FireControl gains Splash and a beam roll FireControl.Fire takes an optional damageOverride so a continuous weapon can queue a shot for less than its full Damage. FireControl.Splash is the one new rule this cut adds: an unconditional area effect (mines, airburst) applying the shield-absorb-or-hull-takes-it branch per target, then DamageSchematic over the directional half of each target's own hull that faces the blast -- the rule moved verbatim from the Splash subscription EntityInstance.cs carried before Cut 3 deleted it (git show b7743789^). ConstantWeapon.Execute rolls once per GameplaySettings.BeamResolveInterval (new setting) for Damage*interval, through the same FireControl.Fire/Step pair a discrete shot uses -- a beam's authored Velocity is 0, so the flight commits and resolves in the same tick it fires (R4's short-flight degradation). A beam is a sequence of rolls, not a continuous truth. Fixes a real defect found while testing the above: both InstantWeapon.Execute (Cut 3) and the new ConstantWeapon.Execute wrote `OnFire?.Invoke(FireControl. Fire(...))` inline. C#'s null-conditional short-circuits the whole expression, argument included, whenever the event has no subscriber -- true of any headless run with no Unity EntityInstance wired up (docs/headless-playground-cut.md) -- so a shot silently never fired. Both now evaluate FireControl.Fire into a local before the null-conditional invoke. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 15:16:57 +02:00
}
}
return true;
2021-01-05 02:56:17 +00:00
}
public override void Activate()
2021-01-05 02:56:17 +00: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 refused beam never starts (operator ruling 2026-09-30): OnStartFiring drives the beam's visuals and
// audio, so starting it only for Execute to stop it a tick later would flash a beam that fires nothing.
if(!_firing && !_reloading && !FireControl.Refuses(this, Entity))
2021-01-05 02:56:17 +00:00
{
_firing = true;
OnStartFiring?.Invoke();
}
}
public override void Deactivate()
2021-01-05 02:56:17 +00:00
{
if (_firing)
{
_firing = false;
OnStopFiring?.Invoke();
2021-01-05 02:56:17 +00:00
}
}
}