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>
This commit is contained in:
parent
8e719d6dba
commit
482c47bffe
4 changed files with 89 additions and 4 deletions
|
|
@ -32,6 +32,9 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
|
|||
private float _ammoInterval;
|
||||
private float _reload;
|
||||
private bool _reloading;
|
||||
// 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
|
||||
{
|
||||
|
|
@ -53,13 +56,18 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
|
|||
public event Action OnReloadComplete;
|
||||
public event Action OnStartFiring;
|
||||
public event Action OnStopFiring;
|
||||
|
||||
// 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;
|
||||
OnBeamShot = null;
|
||||
}
|
||||
|
||||
public ConstantWeapon(ConstantWeaponData data, EquippedItem item) : base(data, item)
|
||||
|
|
@ -152,6 +160,23 @@ public class ConstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerC
|
|||
CauseWearDamage(dt);
|
||||
AddHeat(Evaluate(_data.Heat) * dt);
|
||||
Entity.VisibilitySources[this] = Evaluate(_data.Visibility);
|
||||
|
||||
// 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);
|
||||
OnBeamShot?.Invoke(shotId);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,7 +240,13 @@ public class InstantWeapon : Weapon, IProgressBehavior, IEventBehavior, IPowerCo
|
|||
// 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.
|
||||
OnFire?.Invoke(FireControl.Fire(this, Item, Entity));
|
||||
// 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);
|
||||
if(!firedThisFrame)
|
||||
{
|
||||
Item.FireAudioEvent(WeaponAudioEvent.Fire);
|
||||
|
|
|
|||
|
|
@ -155,7 +155,12 @@ public static class FireControl
|
|||
// flight time, freezes the payload snapshot (R10, Q6 -- the gun that fired it, not a re-read later), and
|
||||
// queues a PendingShot for Zone.Step to age and eventually resolve. Returns the ShotId so the caller's
|
||||
// OnFire event can carry it to presentation.
|
||||
public static int Fire(Weapon weapon, EquippedItem item, Entity source)
|
||||
// Cut 4 (docs/fire-control-cut.md): damageOverride lets a continuous weapon fire a shot for less than its
|
||||
// full Damage -- ConstantWeapon.Execute rolls one of these per GameplaySettings.BeamResolveInterval, for
|
||||
// Damage * interval, through this exact same freeze-and-queue path (a flight time of zero, since a beam's
|
||||
// authored Velocity is 0, commits and resolves in the tick it fires -- R4's short-flight degradation,
|
||||
// unchanged). Every other caller keeps reading the frozen weapon.Damage it always did.
|
||||
public static int Fire(Weapon weapon, EquippedItem item, Entity source, float? damageOverride = null)
|
||||
{
|
||||
var zone = source.Zone;
|
||||
var target = source.Target.Value;
|
||||
|
|
@ -186,7 +191,7 @@ public static class FireControl
|
|||
Target = target,
|
||||
Weapon = item,
|
||||
Aimed = source.ResolvedTargetItem,
|
||||
Damage = weapon.Damage,
|
||||
Damage = damageOverride ?? weapon.Damage,
|
||||
Penetration = weapon.Penetration,
|
||||
DamageSpread = weapon.DamageSpread,
|
||||
DamageType = weapon.WeaponData.DamageType,
|
||||
|
|
@ -337,6 +342,49 @@ public static class FireControl
|
|||
};
|
||||
}
|
||||
|
||||
// Cut 4 (docs/fire-control-cut.md): the one splash rule -- an unconditional area effect, not a rolled
|
||||
// shot. Nothing here draws: a mine or an airburst round always damages everything it catches, the same
|
||||
// as the Physics.OverlapSphere queries this replaces always did. Applies the shield-absorb-or-hull-takes-
|
||||
// it branch (F5, docs/stats-and-power-cut.md) per target, then -- for a target the shield didn't fully
|
||||
// absorb -- DamageSchematic over the directional half of that 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^:Assets/Scripts/Gameplay/EntityInstance.cs), made planar (R7): the blast-to-target
|
||||
// direction is measured in the zone's (x,z) plane and rotated into each target's own facing by its
|
||||
// Direction, the same rotation Entity.ApplyHit's penetration march uses, in place of a Unity
|
||||
// InverseTransformDirection.
|
||||
public static void Splash(Zone zone, float3 position, float radius, float damage, DamageType damageType)
|
||||
{
|
||||
foreach (var target in zone.Entities)
|
||||
{
|
||||
var toTarget = (target.Position - position).xz;
|
||||
if (length(toTarget) > radius) continue;
|
||||
|
||||
var shield = target.Shield;
|
||||
var shieldActive = shield != null && shield.Item.Active.Value;
|
||||
var shieldAbsorbs = shieldActive && shield.CanTakeHit(damageType, damage);
|
||||
if (shieldActive && !shieldAbsorbs) shield.Break();
|
||||
if (shieldAbsorbs)
|
||||
{
|
||||
shield.TakeHit(damageType, damage);
|
||||
continue;
|
||||
}
|
||||
|
||||
var hullData = target.ItemManager.GetData(target.Hull) as HullData;
|
||||
var forward = normalize(target.Direction);
|
||||
var right = float2(forward.y, -forward.x);
|
||||
var localDirection = lengthsq(toTarget) > 1e-6f
|
||||
? normalize(float2(dot(toTarget, right), dot(toTarget, forward)))
|
||||
: float2(0, 1);
|
||||
|
||||
var hitShape = new Shape(hullData.Shape.Width, hullData.Shape.Height);
|
||||
foreach (var v in hullData.Shape.Coordinates)
|
||||
if (dot(normalize((float2) v - hullData.Shape.CenterOfMass), localDirection) < 0)
|
||||
hitShape[v] = true;
|
||||
|
||||
target.DamageSchematic(damage, hitShape);
|
||||
}
|
||||
}
|
||||
|
||||
// The cells of the target's hull schematic actually occupied by `item` -- GearOccupancy is the one source
|
||||
// of truth for where an equipped item's footprint lands, the same table Entity.DamageSchematic reads.
|
||||
private static int2[] CellsOf(Entity target, EquippedItem item)
|
||||
|
|
|
|||
|
|
@ -240,6 +240,12 @@ public class GameplaySettings
|
|||
// a firing solution worth taking (Combat.cs, TurretController.cs). Player fire is gated on arc alone
|
||||
// (Weapon.ArcAllowsFire, Q2) -- this threshold is an AI fire-discipline heuristic, not part of the roll.
|
||||
public float AgentMinHitProbability = .2f;
|
||||
|
||||
// Cut 4 (docs/fire-control-cut.md): how often a continuous weapon (ConstantWeapon) rolls a discrete
|
||||
// outcome for Damage * this interval, through the same FireControl.Fire/Step pair a discrete shot uses --
|
||||
// a beam is a sequence of rolls, not a continuous truth. First guess; the headless fixture is the tuning
|
||||
// harness, the operator smoke is the arbiter, same as Cut 3's other first guesses.
|
||||
public float BeamResolveInterval = .25f;
|
||||
}
|
||||
|
||||
[Serializable, MessagePackObject(keyAsPropertyName: true), JsonObject]
|
||||
|
|
|
|||
Loading…
Reference in a new issue