Mining 4b: a shot at a rock goes through FireControl's one fire path and wears it; the rock is a disc
Designated/Bears/Solve/HitProbability/TravelDirection/PredictedIntercept take TargetRef; PendingShot and ShotOutcome carry Chunk; Zone.ChunkVelocity is the one rock velocity owner; Silhouette's Gaussian sum is the one integrator the rock disc also reads; Apply wears the rock or detonates at its centre; a rock broken in flight is a miss. Deletes the untargeted-round stopgap. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c47a34115c
commit
152f97ba0c
2 changed files with 185 additions and 67 deletions
|
|
@ -84,10 +84,10 @@ public static class FireControl
|
|||
// Whether the shooter's next round from this weapon is refused: the weapons ask before they spend anything, so
|
||||
// a refused round costs no ammo, energy, sound, heat, wear or visibility (operator ruling 2026-09-30). The
|
||||
// answer is Solve's own -- there is no second copy of the arming test.
|
||||
public static bool Refuses(Weapon weapon, Entity shooter) => Refuses(weapon, shooter, shooter.Target.Value.Entity);
|
||||
public static bool Refuses(Weapon weapon, Entity shooter) => Refuses(weapon, shooter, shooter.Target.Value);
|
||||
|
||||
// The same answer against a target the shooter's pilot has commanded but not yet applied.
|
||||
public static bool Refuses(Weapon weapon, Entity shooter, Entity target) =>
|
||||
public static bool Refuses(Weapon weapon, Entity shooter, TargetRef target) =>
|
||||
!IsMineLayer(weapon) && Solve(weapon, weapon.Item, shooter, target).Outcome == FireOutcome.Refused;
|
||||
|
||||
// A mine layer lays a body, not a round: it is never refused, never arc-gated, and Fire routes it to Lay.
|
||||
|
|
@ -139,27 +139,18 @@ public static class FireControl
|
|||
// The one owner of a gun's solution against its subject (the designated target; ruling controls-solution-subject).
|
||||
// Bears: the gun can engage the subject, so the player fires on it without the aim. Free: the aim is inside the
|
||||
// arc, so the gun can fire along it. Direction: where a round from this gun travels and where its barrel points --
|
||||
// the subject's intercept when it bears, else the arc-clamped aim. A chunk subject bears through its pose and
|
||||
// flies the aim, as an untargeted round (Subject is null for it). Derived on every read and never stored: turrets
|
||||
// the subject's intercept when it bears (a rock's too), else the arc-clamped aim. Subject is null for a rock. Derived on every read and never stored: turrets
|
||||
// traverse instantly in the simulation. Traverse is the weapon's Tracking (ruling mount-arc-gun-traverse), priced in
|
||||
// TrackingRate; this holds no slew state, and a rig's slew is presentation. It never prices; HitProbability is the one price.
|
||||
public static GunSolution Solution(Weapon weapon, Entity shooter, TargetRef subject)
|
||||
{
|
||||
var bears = false;
|
||||
if (subject.Entity != null) bears = Bears(weapon, shooter, subject.Entity, out _);
|
||||
else if (subject.Chunk is ChunkId chunk)
|
||||
{
|
||||
var at = shooter.Zone.ChunkPose(chunk.Field, chunk.Index).xy;
|
||||
bears = InArc(weapon.Item, float3(at.x, 0, at.y) - shooter.Position);
|
||||
}
|
||||
var bears = Bears(weapon, shooter, subject, out _);
|
||||
return new GunSolution
|
||||
{
|
||||
Subject = subject.Entity,
|
||||
Bears = bears,
|
||||
Free = InArc(weapon.Item, shooter.Aim),
|
||||
Direction = bears && subject.Entity != null
|
||||
? TravelDirection(weapon, shooter, subject.Entity)
|
||||
: AimDirection(weapon.Item, shooter)
|
||||
Direction = bears ? TravelDirection(weapon, shooter, subject) : AimDirection(weapon.Item, shooter)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -224,8 +215,8 @@ public static class FireControl
|
|||
return degrees(abs(offset.x * relativeVelocity.y - offset.y * relativeVelocity.x) / (range * range));
|
||||
}
|
||||
|
||||
public static float AngularVelocity(Entity source, Entity target) =>
|
||||
AngularVelocity((target.Position - source.Position).xz, target.Velocity - source.Velocity);
|
||||
public static float AngularVelocity(Entity source, TargetRef target) =>
|
||||
AngularVelocity((PositionOf(source, target) - source.Position).xz, VelocityOf(source, target) - source.Velocity);
|
||||
|
||||
// The gun's rate times the ship's gear over the unaided baseline (ruling tracking-per-gun, "gun times ship"): the
|
||||
// one product of the two. An unauthored gun is +infinity and so is its rate whatever the gear reads: a mount with no
|
||||
|
|
@ -241,14 +232,31 @@ public static class FireControl
|
|||
// be judged against another. This is the one function; CombatState (and FireControl.Fire below) both call
|
||||
// it instead of touching CultMath.first_order_intercept directly. Planar: source velocity is never fed in
|
||||
// (no shooter here leads its own motion into the shot), matching every existing call site this replaces.
|
||||
public static float3 PredictedIntercept(Weapon weapon, Entity source, Entity target)
|
||||
// Mining Cut 4b: a target is an entity or a rock; its position and velocity come from PositionOf and VelocityOf.
|
||||
public static float3 PredictedIntercept(Weapon weapon, Entity source, TargetRef target)
|
||||
{
|
||||
var targetVelocity = float3(target.Velocity.x, 0, target.Velocity.y);
|
||||
var position = PositionOf(source, target);
|
||||
var velocity = VelocityOf(source, target);
|
||||
return weapon.Velocity > .01f
|
||||
? first_order_intercept(source.Position, float3.zero, weapon.Velocity, target.Position, targetVelocity)
|
||||
: target.Position;
|
||||
? first_order_intercept(source.Position, float3.zero, weapon.Velocity, position, float3(velocity.x, 0, velocity.y))
|
||||
: position;
|
||||
}
|
||||
|
||||
// Where a target is now: an entity's position, a rock's pose at the shooter's height (everything here is planar).
|
||||
private static float3 PositionOf(Entity source, TargetRef target)
|
||||
{
|
||||
if (target.Chunk is ChunkId chunk)
|
||||
{
|
||||
var at = source.Zone.ChunkPose(chunk.Field, chunk.Index).xy;
|
||||
return float3(at.x, source.Position.y, at.y);
|
||||
}
|
||||
return target.Entity.Position;
|
||||
}
|
||||
|
||||
// How fast a target moves: an entity's velocity, a rock's from Zone.ChunkVelocity, the one owner.
|
||||
private static float2 VelocityOf(Entity source, TargetRef target) =>
|
||||
target.Chunk is ChunkId chunk ? source.Zone.ChunkVelocity(chunk) : target.Entity.Velocity;
|
||||
|
||||
// Cut 3, R1/R3: the targeting solution (0b table) -- pure, no draw, recomputed fresh on every call. AI
|
||||
// reads this to decide whether a shot is worth taking; the HUD reads it to draw a number; Fire below reads
|
||||
// it once, at the instant the trigger is pulled, and freezes the result into the shot (R10, Q6). Two reads
|
||||
|
|
@ -263,13 +271,13 @@ public static class FireControl
|
|||
// freezes (R10), gated exactly as HitProbability gates. Nothing about the target's facing or silhouette
|
||||
// enters here; that is Silhouette's and PSpread's job, priced fresh by HitProbability below and again,
|
||||
// live, by Commit at the commit tick. 0 the moment any gate closes.
|
||||
private static float PFire(Weapon weapon, Entity source, Entity target) => PFire(weapon, source, target, out _, out _, out _);
|
||||
private static float PFire(Weapon weapon, Entity source, TargetRef target) => PFire(weapon, source, target, out _, out _, out _);
|
||||
|
||||
// The `out range` overload is HitProbability's own gate call -- one computation of `target.Position -
|
||||
// source.Position`, not two. HitProbability used to recompute its own copy for PSpread, which is exactly
|
||||
// the kind of split authority Cut 3 named as the risk (two places computing the same vector, free to
|
||||
// drift or to have their subtraction order flipped in only one of them).
|
||||
private static float PFire(Weapon weapon, Entity source, Entity target, out float range) =>
|
||||
private static float PFire(Weapon weapon, Entity source, TargetRef target, out float range) =>
|
||||
PFire(weapon, source, target, out range, out _, out _);
|
||||
|
||||
// The one answer to "does the shooter hold valid targeting data on this target": visible, in range and locked
|
||||
|
|
@ -279,25 +287,29 @@ public static class FireControl
|
|||
// visible, out of range) already denies the target. Planar (R7: the simulation is 2D): the range gate, the
|
||||
// burst reach and the range clamp all measure the same distance, so a target's height never moves one of
|
||||
// them without the others.
|
||||
public static bool Designated(Weapon weapon, Entity source, Entity target, out float range)
|
||||
// Mining Cut 4b: a rock is designated by the same range gates, seen when Entity.ChunkVisible says so; a
|
||||
// LockWeapon (a launcher) is never designated on a rock (ruling Q12 = A, 2026-09-30).
|
||||
public static bool Designated(Weapon weapon, Entity source, TargetRef target, out float range)
|
||||
{
|
||||
range = 0f;
|
||||
if (target == null) return false;
|
||||
range = length((target.Position - source.Position).xz);
|
||||
return source.VisibleEntities.Contains(target) && range >= weapon.MinRange && range <= weapon.Range &&
|
||||
if (target.IsNone) return false;
|
||||
range = length((PositionOf(source, target) - source.Position).xz);
|
||||
var inRange = range >= weapon.MinRange && range <= weapon.Range;
|
||||
if (target.Chunk is ChunkId chunk) return inRange && !(weapon is LockWeapon) && source.ChunkVisible(chunk);
|
||||
return source.VisibleEntities.Contains(target.Entity) && inRange &&
|
||||
!(weapon is LockWeapon lockWeapon && !lockWeapon.IsLocked);
|
||||
}
|
||||
|
||||
// Whether this gun can engage this target: designated and inside the arc. The one test of it; PFire prices a shot
|
||||
// at zero unless it holds, and Solution reads it to decide whether the gun fires on the target. `designated` is
|
||||
// the answer above, kept apart for Solve's burst flight: a designated target out of arc still sets the burst range.
|
||||
public static bool Bears(Weapon weapon, Entity source, Entity target, out float range) =>
|
||||
public static bool Bears(Weapon weapon, Entity source, TargetRef target, out float range) =>
|
||||
Bears(weapon, source, target, out range, out _);
|
||||
|
||||
private static bool Bears(Weapon weapon, Entity source, Entity target, out float range, out bool designated)
|
||||
private static bool Bears(Weapon weapon, Entity source, TargetRef target, out float range, out bool designated)
|
||||
{
|
||||
designated = Designated(weapon, source, target, out range);
|
||||
return designated && InArc(weapon.Item, target.Position - source.Position);
|
||||
return designated && InArc(weapon.Item, PositionOf(source, target) - source.Position);
|
||||
}
|
||||
|
||||
// `designated` is the answer above. `inArc` is Bears, asked only once a target is designated.
|
||||
|
|
@ -305,13 +317,14 @@ public static class FireControl
|
|||
// (operator rulings 2026-09-30): without valid data it has no target and bursts at max range; with data but
|
||||
// out of arc it bursts at the target's range, along the aim. The HUD reads Designated and the
|
||||
// arc from Solve and Inspect and decides nothing.
|
||||
private static float PFire(Weapon weapon, Entity source, Entity target, out float range, out bool designated, out bool inArc)
|
||||
private static float PFire(Weapon weapon, Entity source, TargetRef target, out float range, out bool designated, out bool inArc)
|
||||
{
|
||||
inArc = Bears(weapon, source, target, out range, out designated);
|
||||
if (!inArc) return 0f;
|
||||
|
||||
var settings = source.ItemManager.GameplaySettings;
|
||||
var info = source.EntityInfoGathered.TryGetValue(target, out var gathered) ? gathered : 0f;
|
||||
var info = target.Chunk is ChunkId chunk ? source.ChunkInfo(chunk)
|
||||
: source.EntityInfoGathered.TryGetValue(target.Entity, out var gathered) ? gathered : 0f;
|
||||
return Accuracy(source) * PSensor(settings, Resolution(source), info);
|
||||
}
|
||||
|
||||
|
|
@ -364,22 +377,33 @@ public static class FireControl
|
|||
return (sil, pSpread, evasion);
|
||||
}
|
||||
|
||||
public static float HitProbability(Weapon weapon, Entity source, Entity target) =>
|
||||
public static float HitProbability(Weapon weapon, Entity source, TargetRef target) =>
|
||||
HitProbability(weapon, source, target, (AimTarget?) null);
|
||||
|
||||
// Priced on `aim` rather than the source's aim slot: the aim its pilot commands this step.
|
||||
public static float HitProbability(Weapon weapon, Entity source, Entity target, AimTarget aim) =>
|
||||
public static float HitProbability(Weapon weapon, Entity source, TargetRef target, AimTarget aim) =>
|
||||
HitProbability(weapon, source, target, (AimTarget?) aim);
|
||||
|
||||
// A null aim reads source.ResolvedAim, after the gate, so a gated-out shot reads nothing more.
|
||||
private static float HitProbability(Weapon weapon, Entity source, Entity target, AimTarget? aim)
|
||||
// Mining Cut 4b: a rock is priced by the same factors as a ship at equal range, precision and info -- its disc
|
||||
// through the same integrator, its evasion 0 (a rock is not a Ship), and no aim point: the disc is aimed at.
|
||||
private static float HitProbability(Weapon weapon, Entity source, TargetRef target, AimTarget? aim)
|
||||
{
|
||||
var pFire = PFire(weapon, source, target, out var range);
|
||||
if (pFire <= 0f) return 0f;
|
||||
|
||||
var targetHull = source.ItemManager.GetData(target.Hull) as HullData;
|
||||
var (sil, pSpread, evasion) = Forecast(weapon, source, target, targetHull, Precision(source), range, aim ?? source.ResolvedAim);
|
||||
return pFire * PMount(AngularVelocity(source, target), TrackingRate(weapon, source)) * pSpread * sil.POnHull * PDeviation(evasion, Tracking(source));
|
||||
var pMount = PMount(AngularVelocity(source, target), TrackingRate(weapon, source));
|
||||
if (target.Chunk is ChunkId chunk)
|
||||
{
|
||||
var mass = RockMass(source.Zone, chunk, Precision(source), out var span);
|
||||
var pSpreadRock = PSpread(weapon.Spread, span, range, source.ItemManager.GameplaySettings.SchematicCellSize);
|
||||
return pFire * pMount * pSpreadRock * mass * PDeviation(0f, Tracking(source));
|
||||
}
|
||||
|
||||
var entity = target.Entity;
|
||||
var targetHull = source.ItemManager.GetData(entity.Hull) as HullData;
|
||||
var (sil, pSpread, evasion) = Forecast(weapon, source, entity, targetHull, Precision(source), range, aim ?? source.ResolvedAim);
|
||||
return pFire * pMount * pSpread * sil.POnHull * PDeviation(evasion, Tracking(source));
|
||||
}
|
||||
|
||||
// Presentation only: the debug HUD's view of exactly the factors HitProbability multiplies. It computes
|
||||
|
|
@ -457,7 +481,7 @@ public static class FireControl
|
|||
// at Fire (R10); HitProbability and Inspect recompute it fresh, at the current source/target positions,
|
||||
// for their forecast. Below a length of 1e-6 (point-blank, source and intercept coincide) it falls back
|
||||
// to world +z, the same fallback Apply used before this cut (`:463`).
|
||||
public static float2 TravelDirection(Weapon weapon, Entity source, Entity target)
|
||||
public static float2 TravelDirection(Weapon weapon, Entity source, TargetRef target)
|
||||
{
|
||||
var toIntercept = (PredictedIntercept(weapon, source, target) - source.Position).xz;
|
||||
return lengthsq(toIntercept) < 1e-6f ? float2(0, 1) : normalize(toIntercept);
|
||||
|
|
@ -505,12 +529,21 @@ public static class FireControl
|
|||
// The commit price's deviation: what the target has already moved off the frozen forecast, plus what it can and
|
||||
// does move off the line of fire (Evasion). The second part applies at any flight time, so an instant weapon
|
||||
// (flight time 0, realized deviation 0) still feels a jinking target.
|
||||
public static float DeviationProbability(PendingShot shot, float now, out float deviation)
|
||||
// Mining Cut 4b: a rock's deviation is where its pose is now against that projection, with no evasion; its
|
||||
// zone is the shooter's (a shot that outlives its shooter is resolved by Step, which passes the zone it runs in).
|
||||
public static float DeviationProbability(PendingShot shot, float now, out float deviation) =>
|
||||
DeviationProbability(shot.Source?.Zone, shot, now, out deviation);
|
||||
|
||||
private static float DeviationProbability(Zone zone, PendingShot shot, float now, out float deviation)
|
||||
{
|
||||
deviation = 0f;
|
||||
if (shot.Target == null && shot.Chunk == null) return 1f;
|
||||
var elapsed = now - shot.FireTime;
|
||||
var predicted = shot.FireTargetPosition + shot.FireTargetVelocity * elapsed;
|
||||
deviation = shot.Target == null ? 0f : length((shot.Target.Position - predicted).xz) + Evasion(shot.Target, shot.TravelDirection);
|
||||
return shot.Target == null ? 1f : PDeviation(deviation, shot.Tracking);
|
||||
deviation = shot.Chunk is ChunkId chunk
|
||||
? length(zone.ChunkPose(chunk.Field, chunk.Index).xy - predicted.xz)
|
||||
: length((shot.Target.Position - predicted).xz) + Evasion(shot.Target, shot.TravelDirection);
|
||||
return PDeviation(deviation, shot.Tracking);
|
||||
}
|
||||
|
||||
// How far a target can and does move off the line of fire in the solution window: the term DeviationProbability
|
||||
|
|
@ -608,7 +641,7 @@ public static class FireControl
|
|||
// BurstReach the planar distance from the shooter at which the payload is delivered (a direct round's target, a fused
|
||||
// round's burst point, 0 for an untargeted direct round), which Fire's range factor reads for every outcome. Refused: a fused round whose blast
|
||||
// radius (its arming distance) exceeds the weapon's Range; it cannot be fired.
|
||||
public static FireSolution Solve(Weapon weapon, EquippedItem item, Entity source, Entity target)
|
||||
public static FireSolution Solve(Weapon weapon, EquippedItem item, Entity source, TargetRef target)
|
||||
{
|
||||
var pFire = PFire(weapon, source, target, out var fireRange, out var designated, out var inArc);
|
||||
|
||||
|
|
@ -639,14 +672,14 @@ public static class FireControl
|
|||
// that leaves the zone cancels it like any other. A round whose Range is shorter than the arming distance
|
||||
// cannot be fired at all: it is refused, per round, where Range is read after Execute refreshed it (a
|
||||
// burst's later rounds and a power-starved Range included).
|
||||
var engaged = fuse != null && !(designated && inArc) ? null : target;
|
||||
var engaged = fuse != null && !(designated && inArc) ? TargetRef.None : target;
|
||||
var origin = source.Position;
|
||||
var travelDirection = Solution(weapon, source, target).Direction;
|
||||
|
||||
var arming = fuse != null ? blastRadius : 0f;
|
||||
var burstPosition = engaged != null ? engaged.Position : origin;
|
||||
var burstPosition = !engaged.IsNone ? PositionOf(source, engaged) : origin;
|
||||
var flightDistance = 0f;
|
||||
if (fuse != null && engaged != null)
|
||||
if (fuse != null && !engaged.IsNone)
|
||||
{
|
||||
// Nothing bursts beyond max range: an intercept a receding target leads the round past Range is
|
||||
// pulled back along its own line to Range.
|
||||
|
|
@ -662,7 +695,7 @@ public static class FireControl
|
|||
burstPosition = Armed(origin, travelDirection, arming, origin + float3(travelDirection.x, 0, travelDirection.y) * reach);
|
||||
flightDistance = max(reach, arming);
|
||||
}
|
||||
else if (engaged != null) flightDistance = fireRange;
|
||||
else if (!engaged.IsNone) flightDistance = fireRange;
|
||||
|
||||
var solution = new FireSolution
|
||||
{
|
||||
|
|
@ -710,8 +743,7 @@ public static class FireControl
|
|||
Lay(weapon, item, source);
|
||||
return 0;
|
||||
}
|
||||
// Mining Cut 3: a chunk target fires as an untargeted round until Cut 4 routes shots at chunks.
|
||||
var target = source.Target.Value.Entity;
|
||||
var target = source.Target.Value;
|
||||
var now = zone.Time - age;
|
||||
|
||||
// R1's engage gate and the shooter-side probability, evaluated now and frozen: nothing at commit time
|
||||
|
|
@ -721,7 +753,7 @@ public static class FireControl
|
|||
var solution = Solve(weapon, item, source, target);
|
||||
if (solution.Outcome == FireOutcome.Refused) return 0;
|
||||
// The mount factor is frozen with the shooter-side probability: nothing at commit time re-reads the sweep.
|
||||
var pFire = solution.PFire * (solution.Engaged != null ? PMount(AngularVelocity(source, solution.Engaged), TrackingRate(weapon, source)) : 1f);
|
||||
var pFire = solution.PFire * (!solution.Engaged.IsNone ? PMount(AngularVelocity(source, solution.Engaged), TrackingRate(weapon, source)) : 1f);
|
||||
var fireRange = solution.FireRange;
|
||||
var engaged = solution.Engaged;
|
||||
var travelDirection = solution.TravelDirection;
|
||||
|
|
@ -732,10 +764,11 @@ public static class FireControl
|
|||
|
||||
var targetVelocity = float3.zero;
|
||||
var targetPosition = origin;
|
||||
if (engaged != null)
|
||||
if (!engaged.IsNone)
|
||||
{
|
||||
targetVelocity = float3(engaged.Velocity.x, 0, engaged.Velocity.y);
|
||||
targetPosition = engaged.Position - targetVelocity * age;
|
||||
var velocity = VelocityOf(source, engaged);
|
||||
targetVelocity = float3(velocity.x, 0, velocity.y);
|
||||
targetPosition = PositionOf(source, engaged) - targetVelocity * age;
|
||||
}
|
||||
var flightTime = weapon.Velocity > .01f ? solution.FlightDistance / weapon.Velocity : 0f;
|
||||
|
||||
|
|
@ -746,7 +779,8 @@ public static class FireControl
|
|||
{
|
||||
ShotId = zone.NextShotId(),
|
||||
Source = source,
|
||||
Target = engaged,
|
||||
Target = engaged.Entity,
|
||||
Chunk = engaged.Chunk,
|
||||
Weapon = item,
|
||||
Aimed = aim.Item,
|
||||
AimedMount = aim.Mount,
|
||||
|
|
@ -773,7 +807,7 @@ public static class FireControl
|
|||
// The commit horizon exists so the target's facing can be read late (Bearing timing). A round fired at
|
||||
// nothing has no facing to read, so it commits at Fire: its burst point is decided from the poses of
|
||||
// the moment it leaves, and a Contact round short of max range can arrive as early as it truly does.
|
||||
CommitTime = fuse != null && engaged == null ? now : now + max(0f, flightTime - commitHorizon),
|
||||
CommitTime = fuse != null && engaged.IsNone ? now : now + max(0f, flightTime - commitHorizon),
|
||||
BurstPosition = burstPosition,
|
||||
Fuse = fuse,
|
||||
BlastRadius = blastRadius,
|
||||
|
|
@ -861,7 +895,7 @@ public static class FireControl
|
|||
{
|
||||
var shot = shots[i];
|
||||
|
||||
var targetGone = shot.Target != null && !zone.Entities.Contains(shot.Target);
|
||||
var targetGone = TargetGone(zone, shot);
|
||||
if (targetGone)
|
||||
{
|
||||
// Cut 11 (Soul C4): a shot whose target has left the zone resolves as a miss whichever
|
||||
|
|
@ -908,7 +942,7 @@ public static class FireControl
|
|||
if (i < 0) continue;
|
||||
var shot = shots[i];
|
||||
|
||||
var targetGone = shot.Target != null && !zone.Entities.Contains(shot.Target);
|
||||
var targetGone = TargetGone(zone, shot);
|
||||
if (targetGone)
|
||||
{
|
||||
// As in the commit walk: a shot whose target left the zone in this step resolves as a miss.
|
||||
|
|
@ -930,6 +964,10 @@ public static class FireControl
|
|||
}
|
||||
}
|
||||
|
||||
// A shot's target has left the zone, or a rock it flies at no longer exists (broken in flight): the shot is a miss.
|
||||
private static bool TargetGone(Zone zone, PendingShot shot) =>
|
||||
(shot.Target != null && !zone.Entities.Contains(shot.Target)) || (shot.Chunk is ChunkId chunk && !zone.ChunkExists(chunk));
|
||||
|
||||
// R3/R4: the one roll. p_base was frozen at fire, PFire carrying the mount factor; the only thing measured live is how far the target has
|
||||
// actually strayed, by now, from where a straight-line projection of its fire-time velocity said it would
|
||||
// be -- forgiven by the targeting system's Tracking, likewise frozen at fire.
|
||||
|
|
@ -945,15 +983,25 @@ public static class FireControl
|
|||
// (bearing) -- is read live, here, at the commit tick. The Cut 7 guard extends to cover the new factors:
|
||||
// when PFire x pDeviation is already 0, no Silhouette is built. Shared by Commit's own roll and by the
|
||||
// debug HUD's forecast (TheHudEstimateIsTheCommitPrice) -- one function, one commit-time price.
|
||||
public static float CommitProbability(PendingShot shot, float now, out Silhouette sil)
|
||||
public static float CommitProbability(PendingShot shot, float now, out Silhouette sil) =>
|
||||
CommitProbability(shot.Source?.Zone, shot, now, out sil);
|
||||
|
||||
// Mining Cut 4b: a rock's price is PFire x PDeviation x PSpread(span 2R) x the disc's mass; it reads no hull.
|
||||
private static float CommitProbability(Zone zone, PendingShot shot, float now, out Silhouette sil)
|
||||
{
|
||||
sil = default;
|
||||
if (shot.Target == null) return 0f;
|
||||
if (shot.Target == null && shot.Chunk == null) return 0f;
|
||||
|
||||
var pDeviation = DeviationProbability(shot, now, out _);
|
||||
var pDeviation = DeviationProbability(zone, shot, now, out _);
|
||||
var p = shot.PFire * pDeviation;
|
||||
if (p <= 0f) return 0f;
|
||||
|
||||
if (shot.Chunk is ChunkId chunk)
|
||||
{
|
||||
var mass = RockMass(zone, chunk, shot.Precision, out var span);
|
||||
return p * PSpread(shot.Spread, span, shot.FireRange, zone.SchematicCellSize) * mass;
|
||||
}
|
||||
|
||||
var bearing = Bearing(shot.Target, shot.TravelDirection);
|
||||
var hullData = shot.Target.ItemManager.GetData(shot.Target.Hull) as HullData;
|
||||
sil = Silhouette(shot.Target, hullData, shot.Aimed, bearing, shot.Precision, null, shot.AimedMount);
|
||||
|
|
@ -984,7 +1032,7 @@ public static class FireControl
|
|||
// drawing once is the documented way to get a correlated first output, and a follow-up to give
|
||||
// CultMath.Random a mixed-seed entry point of its own is recorded for a separate CultLib cut.
|
||||
var random = new Random(SimulationDice.Mix((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u));
|
||||
var p = CommitProbability(shot, now, out var sil);
|
||||
var p = CommitProbability(zone, shot, now, out var sil);
|
||||
|
||||
var hit = p > 0f && random.NextFloat() < p;
|
||||
var cell = int2.zero;
|
||||
|
|
@ -993,7 +1041,8 @@ public static class FireControl
|
|||
var shielded = false;
|
||||
var shieldBroken = false;
|
||||
|
||||
if (hit)
|
||||
// A rock hit has no lane and no shield: Cell, Bearing and Lateral stay zero (Apply wears the rock).
|
||||
if (hit && shot.Chunk == null)
|
||||
{
|
||||
// Cut 12.2: the same silhouette pOnHull drew its mass from -- same bearing (live, at this commit
|
||||
// tick), same sigma (the frozen Precision). A shot that passed the roll always lands on metal (R3:
|
||||
|
|
@ -1189,6 +1238,12 @@ public static class FireControl
|
|||
return;
|
||||
}
|
||||
|
||||
if (shot.Chunk is ChunkId rock)
|
||||
{
|
||||
ApplyRockHit(zone, shot, rock);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (shot.Fuse)
|
||||
{
|
||||
case null:
|
||||
|
|
@ -1201,6 +1256,27 @@ public static class FireControl
|
|||
}
|
||||
}
|
||||
|
||||
// Mining Cut 4b: a committed hit on a rock. A round without a fuse wears the rock by its Damage (the falloff was
|
||||
// read at Fire) and publishes one absorb on the zone's ChunkCapabilities, at the rock's surface facing the shot
|
||||
// and before the wear shrinks it. A contact or delayed round detonates at the rock's centre as it arrives (the
|
||||
// arming rule kept), through Detonate, the one area owner.
|
||||
private static void ApplyRockHit(Zone zone, PendingShot shot, ChunkId rock)
|
||||
{
|
||||
if (!shot.Outcome.Hit) return;
|
||||
|
||||
var centre = zone.ChunkPose(rock.Field, rock.Index).xy;
|
||||
if (shot.Fuse == null)
|
||||
{
|
||||
var direction = float3(shot.TravelDirection.x, 0, shot.TravelDirection.y);
|
||||
var surface = centre - shot.TravelDirection * zone.ChunkRadius(rock);
|
||||
zone.ChunkCapabilities.PublishAbsorb(new AbsorbEvent(float3(surface.x, shot.FireOrigin.y, surface.y), direction, shot.Damage, shot.DamageType, AbsorbLayer.Hull));
|
||||
zone.Wear(rock, shot.Damage);
|
||||
return;
|
||||
}
|
||||
var point = Armed(shot.FireOrigin, shot.TravelDirection, shot.BlastRadius, float3(centre.x, shot.FireOrigin.y, centre.y)).xz;
|
||||
Detonate(zone, point, shot.BlastRadius, shot.Damage, shot.DamageType);
|
||||
}
|
||||
|
||||
// Cut 12.3 (docs/fire-control-cut.md, "How a direct hit travels"): replaces Entity.ApplyHit's 0.5-step
|
||||
// march and Entity.DamageSchematic's even split over the whole hit shape. Spread is width (Q12-2 = A):
|
||||
// 2n+1 parallel lanes, sharing the shot's damage evenly over whichever lanes actually meet metal -- the
|
||||
|
|
@ -1593,6 +1669,7 @@ public static class FireControl
|
|||
ShotId = shot.ShotId,
|
||||
Source = shot.Source,
|
||||
Target = shot.Target,
|
||||
Chunk = shot.Chunk,
|
||||
Weapon = shot.Weapon,
|
||||
Result = result,
|
||||
Shielded = shielded,
|
||||
|
|
@ -1876,9 +1953,7 @@ public static class FireControl
|
|||
var aimPoint = ResolveAimPoint(target, hull, aimed, mount);
|
||||
var a = dot(aimPoint, ell);
|
||||
|
||||
var pOnHull = 0f;
|
||||
for (var i = 0; i < count; i++)
|
||||
pOnHull += Phi((intervals[i].Hi - a) / sigma) - Phi((intervals[i].Lo - a) / sigma);
|
||||
var pOnHull = Mass(new ReadOnlySpan<Interval>(intervals, 0, count), a, sigma);
|
||||
|
||||
var span = count > 0 ? intervals[count - 1].Hi - intervals[0].Lo : 0f;
|
||||
|
||||
|
|
@ -1894,6 +1969,27 @@ public static class FireControl
|
|||
};
|
||||
}
|
||||
|
||||
// The one Gaussian integrator: the mass a normal of `sigma` about `aim` puts inside the merged, disjoint
|
||||
// `intervals`. A hull's shadow and a rock's disc (one interval about its aim) both read it.
|
||||
private static float Mass(ReadOnlySpan<Interval> intervals, float aim, float sigma)
|
||||
{
|
||||
var mass = 0f;
|
||||
foreach (var interval in intervals)
|
||||
mass += Phi((interval.Hi - aim) / sigma) - Phi((interval.Lo - aim) / sigma);
|
||||
return mass;
|
||||
}
|
||||
|
||||
// A rock is a disc of radius R schematic cells aimed at its centre: one interval [-R, R] about the aim, span 2R.
|
||||
// The one price of it, read by HitProbability's forecast and by Commit, at the rock's radius now.
|
||||
private static float RockMass(Zone zone, ChunkId chunk, float precision, out float span)
|
||||
{
|
||||
var radius = zone.ChunkCells(chunk);
|
||||
span = 2f * radius;
|
||||
Span<Interval> disc = stackalloc Interval[1];
|
||||
disc[0] = new Interval { Lo = -radius, Hi = radius };
|
||||
return saturate(Mass(disc, 0f, Sigma(precision)));
|
||||
}
|
||||
|
||||
// Phi(z) = 1/2 (1 + erf(z / sqrt(2))): the standard normal CDF, CultMath's erf the one function underneath
|
||||
// every Gaussian-mass sum this cut computes.
|
||||
private static float Phi(float z) => .5f * (1f + erf(z * (1f / SQRT2)));
|
||||
|
|
@ -2054,6 +2150,9 @@ public struct PendingShot
|
|||
public int ShotId;
|
||||
public Entity Source;
|
||||
public Entity Target;
|
||||
// Mining Cut 4b: the rock a shot flies at, frozen at Fire; at most one of Target and Chunk is set. Unity reads
|
||||
// Target only, so a rock shot draws untargeted until the Unity follow-up (mining-unity-chunk-presentation).
|
||||
public ChunkId? Chunk;
|
||||
public EquippedItem Weapon;
|
||||
public EquippedItem Aimed;
|
||||
// The mount the aim was at, when it was at a mount rather than an item (Entity.ResolvedAim at Fire), frozen with Aimed:
|
||||
|
|
@ -2113,7 +2212,7 @@ public struct PendingShot
|
|||
|
||||
// A fused round with no target: where it bursts is decided at its commit (the first hull its aim line meets), so its
|
||||
// damage reads the falloff there rather than at Fire.
|
||||
public bool DeliveredAtCommit => Fuse != null && Target == null;
|
||||
public bool DeliveredAtCommit => Fuse != null && Target == null && Chunk == null;
|
||||
}
|
||||
|
||||
// What a round does when fired (FireControl.Solve): Direct resolves against its target, Burst detonates at a
|
||||
|
|
@ -2147,7 +2246,7 @@ public struct FireSolution
|
|||
public float FireRange;
|
||||
// Whether the shooter holds valid targeting data on the target (Designated); the HUD and the agents read it here.
|
||||
public bool Designated;
|
||||
public Entity Engaged;
|
||||
public TargetRef Engaged;
|
||||
public float2 TravelDirection;
|
||||
public float3 BurstPosition;
|
||||
public WeaponFuse? Fuse;
|
||||
|
|
@ -2313,6 +2412,7 @@ public sealed class ShotOutcome
|
|||
public int ShotId;
|
||||
public Entity Source;
|
||||
public Entity Target;
|
||||
public ChunkId? Chunk;
|
||||
public EquippedItem Weapon;
|
||||
// What the commit decided, typed: a shot rolled and missed, rolled and hit, or was a fused round with no
|
||||
// target and so burst without rolling, or a proximity round whose roll missed but which detonates anyway
|
||||
|
|
|
|||
|
|
@ -715,6 +715,24 @@ public class Zone
|
|||
return AsteroidBelts[belt].Pose(index, time, parentPosition, size);
|
||||
}
|
||||
|
||||
// Mining Cut 4b: the one owner of a chunk's planar velocity, the central difference of the pose function at zone
|
||||
// time. Derived on every read, never stored: the intercept, the fire-time projection and the mount's sweep all
|
||||
// read it here.
|
||||
public float2 ChunkVelocity(ChunkId chunk)
|
||||
{
|
||||
const double h = .05;
|
||||
var ahead = ChunkPose(chunk.Field, chunk.Index, _time + h).xy;
|
||||
var behind = ChunkPose(chunk.Field, chunk.Index, _time - h).xy;
|
||||
return (ahead - behind) / (float) (2 * h);
|
||||
}
|
||||
|
||||
// A chunk's radius in schematic cells: what visibility and a shot's silhouette both measure it in.
|
||||
public float ChunkCells(ChunkId chunk) => ChunkRadius(chunk) / SchematicCellSize;
|
||||
|
||||
// Mining Cut 4b: absorbed impacts on chunks, published by FireControl once per rock hit -- the chunk's own
|
||||
// Entity.Capabilities, since a chunk is not an Entity. Presentations subscribe; nothing here decides.
|
||||
public readonly CapabilityEvents ChunkCapabilities = new CapabilityEvents();
|
||||
|
||||
// Mining Cut 3 (docs/mining-cut-refresh.md): how bright a chunk is -- Reflector's rule (cross-section × the light
|
||||
// on it), with the field kind's reflectivity per schematic cell times the chunk's area in cells. Computed when
|
||||
// asked; nothing about it is stored. A worn chunk shrinks and dims; a broken one, or one of a kindless belt, is dark.
|
||||
|
|
@ -723,7 +741,7 @@ public class Zone
|
|||
if (!ChunkExists(chunk)) return 0f;
|
||||
var kind = _itemManager.ItemData.Get(((AsteroidBeltData) Planets[chunk.Field]).Kind);
|
||||
if (kind == null) return 0f;
|
||||
var cells = ChunkRadius(chunk) / _itemManager.GameplaySettings.SchematicCellSize;
|
||||
var cells = ChunkCells(chunk);
|
||||
return kind.CrossSection * PI * cells * cells * GetLight(ChunkPose(chunk.Field, chunk.Index).xy);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue