Fire on solutions: FireControl.Solution is each gun's one solution; the trigger fires on it without the aim
Bears is extracted from PFire; ArcPermitsFire passes on a fused gun, a solution, or a free aim in arc. Solve flies along Solution's direction. ArticulationPoint loses its yaw clamp and follows a per-group aim point placed from the group's solution. Arcs gets its new brief; Broadside is new. Unverified: first commit of the cut, not yet built. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4d664b537f
commit
e103b90aea
13 changed files with 370 additions and 50 deletions
|
|
@ -112,14 +112,10 @@ public abstract class Weapon : Behavior, IActivatedBehavior
|
|||
// so a weapon is never safed against one.
|
||||
public bool StanceAllowsFire => Entity.Target.Value.Entity == null || Entity.IsHostileTo(Entity.Target.Value.Entity);
|
||||
|
||||
// Cut 3 (docs/fire-control-cut.md, Q2): player fire is arc-gated like everyone else's. This is the same
|
||||
// predicate (FireControl.InArc) Combat.cs and TurretController.cs gate AI and turret fire with, read at
|
||||
// the one point every shooter's trigger passes through (InstantWeapon.Trigger) -- not a parallel check, and
|
||||
// not the AI's own AgentMinHitProbability heuristic, which is a "worth it" decision layered above this
|
||||
// gate, not the gate itself. With no target set, behaviour is unchanged (nothing to bear on).
|
||||
// Cut 5, 5.4 (Soul finding 11): the point-blank special case this used to carry moved into InArc itself --
|
||||
// one bearing test, one owner, so every caller (not just this one) gets a point-blank shot that bears.
|
||||
// FireControl owns the gate, including its one exemption: a fused weapon fires out of arc.
|
||||
// The trigger gate for player and AI shooters alike, read at the one point every shooter's trigger passes
|
||||
// through (InstantWeapon.Trigger). A gun fires on its solution (FireControl.Solution) without the aim, else
|
||||
// along the aim when the aim is inside its arc, else it holds. An AI shooter's aim is its intercept, so its
|
||||
// fire is unchanged. FireControl owns the gate, including its exemption: a fused weapon fires out of arc.
|
||||
public bool ArcAllowsFire => FireControl.ArcPermitsFire(this, Entity);
|
||||
|
||||
public Weapon(WeaponData data, EquippedItem item) : base(data, item)
|
||||
|
|
|
|||
|
|
@ -67,24 +67,18 @@ public static class FireControl
|
|||
return blastRadius > 0f ? data.Fuse : null;
|
||||
}
|
||||
|
||||
// The one arc gate on the trigger. A fused weapon fires whatever the arc says: a target it cannot bear on is
|
||||
// no reason to keep it silent, the round flies along the arc-clamped aim and bursts at the target's range
|
||||
// (Fire; operator ruling 2026-09-30, "the user is hoping that the arc gets the weapon close enough to still
|
||||
// splash some damage"). A weapon that only hits what it fires at still refuses. Fire reads InArc itself to
|
||||
// choose that flight, so the two share the one bearing test and differ only in this exemption.
|
||||
// Mining Cut 3: a chunk target bears through its own position, on the plane the bearing test reads.
|
||||
// The one gate on the trigger (operator rulings fire-on-solutions, controls-solution-trigger, 2026-10-06). A gun
|
||||
// fires on its solution without the aim, else free along the aim if the aim is inside its arc, else it holds.
|
||||
// A fused weapon fires whatever the arc says: a target it cannot bear on is no reason to keep it silent, the
|
||||
// round flies along the arc-clamped aim and bursts at the target's range (Fire; operator ruling 2026-09-30,
|
||||
// "the user is hoping that the arc gets the weapon close enough to still splash some damage"). A mine layer
|
||||
// lays a body, not a round, and is never arc-gated. The price of a free shot at a target that does not bear is
|
||||
// HitProbability's: zero.
|
||||
public static bool ArcPermitsFire(Weapon weapon, Entity shooter)
|
||||
{
|
||||
var target = shooter.Target.Value;
|
||||
if (target.IsNone) return true;
|
||||
float3 toTarget;
|
||||
if (target.Chunk is ChunkId chunk)
|
||||
{
|
||||
var at = shooter.Zone.ChunkPose(chunk.Field, chunk.Index).xy;
|
||||
toTarget = float3(at.x, 0, at.y) - shooter.Position;
|
||||
}
|
||||
else toTarget = target.Entity.Position - shooter.Position;
|
||||
return FuseOf(weapon.Item, out _) != null || IsMineLayer(weapon) || InArc(weapon.Item, toTarget);
|
||||
if (FuseOf(weapon.Item, out _) != null || IsMineLayer(weapon)) return true;
|
||||
var solution = Solution(weapon, shooter, shooter.Target.Value);
|
||||
return solution.Bears || solution.Free;
|
||||
}
|
||||
|
||||
// Whether the shooter's next round from this weapon is refused: the weapons ask before they spend anything, so
|
||||
|
|
@ -131,6 +125,33 @@ public static class FireControl
|
|||
return float2(mount.x * cos(turn) - mount.y * sin(turn), mount.x * sin(turn) + mount.y * cos(turn));
|
||||
}
|
||||
|
||||
// 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
|
||||
// traverse instantly in the simulation. Traverse speed belongs to the link (ruling arc-and-traverse-on-the-link),
|
||||
// which will make this stateful behind the same signature. 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);
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
|
||||
// Cut 2, R5: whether `observer` has resolved `item` (which must belong to some entity in the zone) well
|
||||
// enough to aim at it. Derived fresh from EntityInfoGathered on every call -- nothing caches a reveal
|
||||
// result, so decay drops it the moment info falls back below tier, with no clearing loop anywhere.
|
||||
|
|
@ -276,21 +297,26 @@ public static class FireControl
|
|||
!(weapon is LockWeapon lockWeapon && !lockWeapon.IsLocked);
|
||||
}
|
||||
|
||||
// `designated` is the answer above. `inArc` is the bearing test, asked only once a target is designated.
|
||||
// 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) =>
|
||||
Bears(weapon, source, target, out range, out _);
|
||||
|
||||
private static bool Bears(Weapon weapon, Entity source, Entity target, out float range, out bool designated)
|
||||
{
|
||||
designated = Designated(weapon, source, target, out range);
|
||||
return designated && InArc(weapon.Item, target.Position - source.Position);
|
||||
}
|
||||
|
||||
// `designated` is the answer above. `inArc` is Bears, asked only once a target is designated.
|
||||
// PFire prices a shot at zero unless both hold. Fire reads them to decide what a fused round flies at
|
||||
// (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)
|
||||
{
|
||||
range = 0f;
|
||||
designated = false;
|
||||
inArc = false;
|
||||
if (target == null) return 0f;
|
||||
|
||||
designated = Designated(weapon, source, target, out range);
|
||||
if (!designated) return 0f;
|
||||
inArc = InArc(weapon.Item, target.Position - source.Position);
|
||||
inArc = Bears(weapon, source, target, out range, out designated);
|
||||
if (!inArc) return 0f;
|
||||
|
||||
var settings = source.ItemManager.GameplaySettings;
|
||||
|
|
@ -615,7 +641,7 @@ public static class FireControl
|
|||
// burst's later rounds and a power-starved Range included).
|
||||
var engaged = fuse != null && !(designated && inArc) ? null : target;
|
||||
var origin = source.Position;
|
||||
var travelDirection = engaged != null ? TravelDirection(weapon, source, engaged) : AimDirection(item, source);
|
||||
var travelDirection = Solution(weapon, source, target).Direction;
|
||||
|
||||
var arming = fuse != null ? blastRadius : 0f;
|
||||
var burstPosition = engaged != null ? engaged.Position : origin;
|
||||
|
|
@ -1989,6 +2015,16 @@ public enum FireOutcome
|
|||
Refused
|
||||
}
|
||||
|
||||
// A gun's solution against its subject (FireControl.Solution): whether the gun bears on the subject, whether the
|
||||
// aim is free (inside the arc), and the direction a round from it travels.
|
||||
public struct GunSolution
|
||||
{
|
||||
public Entity Subject;
|
||||
public bool Bears;
|
||||
public bool Free;
|
||||
public float2 Direction;
|
||||
}
|
||||
|
||||
// Solve's answer. Outcome and BurstReach (planar distance from the shooter to the burst point, read for Burst
|
||||
// only) are what the HUD shows; the rest is what Fire freezes into the PendingShot.
|
||||
public struct FireSolution
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ using static ItemRotation;
|
|||
public sealed class Arcs : Scenario
|
||||
{
|
||||
public override string Name => "Arcs";
|
||||
public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the bow " +
|
||||
"hull can be hit, nothing fires at the stern one through the ship, the turret tracks you all the way round.";
|
||||
public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the hull keeps its " +
|
||||
"heading while you aim at the stern hull, and the forward guns hold. Designate the bow hull and fire with the " +
|
||||
"aim anywhere: the forward guns fire on it and hit. The turret tracks you all the way round.";
|
||||
public override uint Seed => 103;
|
||||
public override bool Ambient => false;
|
||||
|
||||
|
|
|
|||
37
Assets/Scripts/ServerShared/Scenarios/Broadside.cs
Normal file
37
Assets/Scripts/ServerShared/Scenarios/Broadside.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using static CultMath.math;
|
||||
using static ItemRotation;
|
||||
|
||||
public sealed class Broadside : Scenario
|
||||
{
|
||||
public override string Name => "Broadside";
|
||||
public override string Brief => "A hull with a port gun and a starboard gun in separate groups, bare hulls abeam to port and starboard and one ahead. " +
|
||||
"Verify: designate the starboard hull, aim ahead and fire the starboard group: it fires on the hull without the aim. " +
|
||||
"Fire the port group: it holds, because nothing bears and the aim is outside its arc. Undesignate and aim to port: " +
|
||||
"the port group fires free along the aim.";
|
||||
public override uint Seed => 104;
|
||||
public override bool Ambient => false;
|
||||
|
||||
// Where the targets sit: the player is at the origin facing +z.
|
||||
public static readonly CultMath.float2 Port = float2(-400, 0), Starboard = float2(400, 0), Ahead = float2(0, 400);
|
||||
|
||||
public override Galaxy Generate(GalaxyStage stage) => stage.Prelude();
|
||||
|
||||
public override void Stage(ScenarioStage stage)
|
||||
{
|
||||
var broadside = stage.Fit("LonginusX",
|
||||
("Cockpit 2x2", int2(2, 6), None),
|
||||
("Traction", int2(2, 3), None),
|
||||
("Core Power", int2(2, 1), None),
|
||||
("GT 3K", int2(0, 5), CounterClockwise),
|
||||
("FastBlast+-", int2(4, 8), Clockwise),
|
||||
("Iapyx", int2(1, 2), CounterClockwise),
|
||||
("Iapyx", int2(4, 2), Clockwise),
|
||||
("not if i see you first", int2(3, 10), None),
|
||||
("Fire Control Array", int2(2, 5), None));
|
||||
|
||||
stage.Player(broadside, float2(0, 0), facing: float2(0, 1));
|
||||
stage.Place(stage.Bare("LonginusX"), Port);
|
||||
stage.Place(stage.Bare("LonginusX"), Starboard);
|
||||
stage.Place(stage.Bare("LonginusX"), Ahead);
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/Scenarios/Broadside.cs.meta
Normal file
11
Assets/Scripts/ServerShared/Scenarios/Broadside.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9fb4b5cf800b4ccdae5d6c15448df36e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -6,6 +6,6 @@ public static class Scenarios
|
|||
|
||||
public static readonly Scenario[] Tests =
|
||||
{
|
||||
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul()
|
||||
new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Broadside(), new Duel(), new LauncherAngles(), new LongHaul()
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue