Aetheria/Assets/Scripts/ServerShared/Settings.cs

320 lines
15 KiB
C#
Raw Normal View History

/* 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 System.Collections;
using System.Collections.Generic;
using MessagePack;
using Newtonsoft.Json;
using CultMath;
using static CultMath.math;
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class PlanetSettings
{
public ExponentialCurve LightRadius;
public ExponentialCurve BodyRadius;
// The closest a child may orbit its parent, in u, by the parent's mass, and the Plummer softening of the parent's
// well (the steepest slope, 0.5 c / eps, sits at the Roche limit). Authored like BodyRadius, never per body.
public ExponentialCurve RocheLimit = new ExponentialCurve();
// A body's well strength by its mass (GravityField): c in u^2/s^2. The pull is c d / (d^2 + eps^2) inside a family and
// c(family mass) beyond it (WellTree); eps is the Roche limit.
public ExponentialCurve WellStrength = new ExponentialCurve();
// The one global exaggeration of gravity height: height = potential / K, in u/s^2 per unit of height.
public float HeightExaggeration = 1f;
public ExponentialLerp AsteroidSize;
public ExponentialLerp AsteroidHitpoints;
public ExponentialLerp AsteroidRespawnTime;
public float SecureAreaRadiusMultiplier = .45f;
// The global flow stardust and nebula follow and the sim knows (FlowField). Zero flow by default.
public FlowSettings Flow = new FlowSettings();
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class GalaxyShapeSettings
{
public int Arms = 4;
public float Twist = 10;
public float TwistExponent = 2;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class SectorBackgroundSettings
{
public float NoiseAmplitude;
public float NoiseOffset;
public float NoiseGain;
public float NoiseLacunarity;
public float NoiseFrequency;
public float NoisePosition;
public float CloudExponent;
public float CloudAmplitude;
public float fBm(float2 p, int octaves)
{
float freq = NoiseFrequency, amp = .5f;
float sum = 0;
for(int i = 0; i < octaves; i++)
{
if(i<4)
sum += (1-abs(snoise(p * freq))) * amp;
else sum += abs(snoise(p * freq)) * amp;
freq *= NoiseLacunarity;
amp *= NoiseGain;
}
return (sum + NoiseOffset)*NoiseAmplitude;
}
public float CloudDensity(float2 uv)
{
float noise = fBm(uv + NoisePosition, 10);
return pow(noise, CloudExponent) * CloudAmplitude;
}
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class SectorGenerationSettings
{
public float LinkDensity = .5f;
public int ZoneCount = 128;
// Zones per resource province (Provinces.Partition): calibration, not a gate.
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
public int MegaCount;
public int BossCount;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class TutorialGenerationSettings
{
public string ProtagonistFaction;
public string AntagonistFaction;
public string BufferFaction;
public string[] NeutralFactions;
public string QuestFaction;
public float LinkDensity = .5f;
public int ZoneCount = 32;
// Zones per resource province (Provinces.Partition): calibration, not a gate.
public int ProvinceZones = Provinces.DefaultZonesPerProvince;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class ZoneGenerationSettings
{
public float MassFloor = 1;
public float SunMass = 10000;
public float GasGiantMass = 2000;
public float PlanetMass = 100f;
public int SatellitePasses = 4;
public float SatelliteCreationMassFloor = 100;
public float SatelliteCreationProbability = .25f;
public float BinaryCreationProbability = .25f;
public float RosetteProbability = .25f;
public ExponentialLerp ZoneRadius;
public ExponentialLerp ZoneMass;
public ExponentialLerp SubZoneCount;
public float ZoneBoundaryRadius;
public float BeltProbability = .05f;
public float BeltMassCeiling = 500f;
public ExponentialCurve AsteroidBeltWidth;
public ExponentialCurve AsteroidCount;
public ExponentialLerp AsteroidRotationSpeed;
// Trojan swarms (ruling trojans-ride-host-period-with-stations): the chance a planet or gas giant with a body for a parent
// carries one, each swarm's length along the orbit (turns) and radial half-width (a fraction of the host's distance), and
// the rocks round each of L4 and L5 by host distance. 0 here: a settings object built in code draws none.
public float TrojanProbability = 0f;
public float TrojanArc = .07f;
public float TrojanHalfWidth = .06f;
public ExponentialCurve TrojanCount = new ExponentialCurve { Exponent = 1f, Multiplier = .04f, Constant = 8f };
// Laned belts (rulings lanes-two-sided-visual-margin, lane-narrow): the chance a belt candidate that kept its body and has
// room on both sides carries a laned belt, and the lane's half-width in the host's body radii (1.5 by ruling). LaneProbability
// is 0 here: a settings object built in code draws no lane.
public float LaneProbability = 0f;
public float LaneBodyRadii = 1.5f;
public float ResourceDensityMinimum = .1f;
public float ResourceDensityMaximum = 1.5f;
public float SunColorSaturation = .75f;
public float SunSecondaryColorDistance = .25f;
public float SunLightSaturation = .5f;
public float SunFogTintSaturation = .5f;
// The green band no sun colour may take, as fractions of the hue circle (75 to 165 degrees: chartreuse through spring green).
public float SunHueExcludedStart = 75f / 360f;
public float SunHueExcludedEnd = 165f / 360f;
public ExponentialLerp GasGiantBandCount;
public float GasGiantBandColorSeparation = .25f;
public float GasGiantBandAltColorChance = .25f;
public ExponentialLerp GasGiantBandSaturation;
public ExponentialLerp GasGiantBandBrightness;
public string[] NameData;
// Non-hostile ships for hand-testing combat/targeting: belong to a galaxy faction that is
// neither the zone owner nor the zone's nearest faction, so derived hostility rules leave
// them non-hostile to the player. Zero eligible factions means none are spawned.
public int NeutralWandererCount = 2;
}
[Serializable, MessagePackObject(keyAsPropertyName: true), JsonObject]
public class GameplaySettings
{
public EntitySettings DefaultEntitySettings;
public RarityTier[] Tiers;
public ExponentialLerp QualityPriceModifier;
// What a station pays for goods, as a fraction of their price, scaled further by the item's condition.
public float SellFraction = .5f;
// What a station charges to restore worn gear, as a fraction of the price of the wear.
public float RepairFraction = .25f;
public int StartingCredits = 15000000;
public float DurabilityQualityExponent = 2;
public float DurabilityQualityMin = 2;
public float DurabilityQualityMax = .25f;
public float ThermalQualityExponent = 2;
public float ThermalQualityMin = 2;
public float ThermalQualityMax = .25f;
public float DefaultShutdownPerformance = .25f;
// Free-floating bodies (floating items, mines): drag on a launch velocity, drag on the force-driven drift, and
// how strongly the zone force accelerates it.
public float FloatingBodyDrag = .05f;
public float FloatingBodyLaunchDrag = .2f;
public float FloatingBodyGravity = 1f;
// Loot: the chance each equipped item (the hull excepted) drops when its carrier dies, the launch speed of a
// dropped item, and how many seconds a floating item lasts.
public float LootDropProbability = .25f;
public float LootDropVelocity = 25f;
public float FloatingItemLifetime = 30f;
public float SevereHeatstrokeRiskThreshold = .25f;
2021-03-05 14:31:18 +00:00
public float WormholeDepth = 1000;
public float WormholeExitVelocity = 20;
public float WormholeExitRadius = 50;
public float WormholeAnimationDuration = 4;
public float WormholeExitCurveStart = .8f;
public float ThermalWearExponent = .01f;
public float DeltaTempWearExponent = .01f;
2021-03-05 14:31:18 +00:00
public float QualityWearExponent = 2;
public int WeaponGroupCount = 6;
public float WarpDistance = 25;
public float DockingDistance = 25;
// Units per second one pilot gives to a ship within DockingDistance; a commodity unit or one crafted item.
public float TransferRate = 2;
public float ProductionPersonalityLerp = .05f;
public float MessageDuration = 4f;
public float TargetPersistenceDuration = 3;
public ExponentialLerp StartingGearQuality;
public float HeatRadiationExponent = 1;
public float HeatRadiationMultiplier = 1;
public float HeatConductionMultiplier = 1;
public ExponentialCurve TemperatureEmissionCurve;
public float HeatstrokeTemperature = 330;
public float HeatstrokeMultiplier = .00001f;
public float HeatstrokeExponent = 2;
public float HeatstrokeRecoverySpeed = .2f;
public float HeatstrokeControlLimit = .75f;
public float HypothermiaTemperature = 273;
public float HypothermiaMultiplier = .00001f;
public float HypothermiaExponent = 2;
public float HypothermiaRecoverySpeed = .2f;
public float HypothermiaControlLimit = .75f;
public float LockIndicatorNoiseAmplitude = 50f;
public ExponentialLerp LockIndicatorFrequency;
public ExponentialLerp LockSpinSpeed;
public float TorqueMultiplier;
public float VisibilityDecay;
// Seconds a reveal's bearing keeps a ship Detected after its last act with no track (Entity.ContactOf).
public float BearingMemory = 6;
public float TargetInfoDecay;
public float TargetDetectionInfoThreshold;
// Also the Identified threshold: the track level at which a ship's hull class, and with it its layout, is known.
public float TargetGearInfoThreshold;
public float ConvergenceMinimumDistance;
public float FiringArc = 120;
// Cut 2 (docs/fire-control-cut.md, Q4): what an entity with no working targeting system fires with --
// the ceiling FireControl.Accuracy falls back to (Resolution 1, Precision 0 alongside it). Authored low
// on purpose ("really, really bad," operator 2026-09-19): .05 against the two catalog designs' own
// Accuracy ranges (the authoring spec this cut produces proposes .45-.6 for the 1-cell design and
// .65-.85 for the 2-cell one) leaves unaided fire capable of hitting something close, slow and
// unaware, and little else. One authored setting, easy to find and turn -- the operator rules the real
// figure in play.
public float UnaidedAccuracy = .05f;
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.1, Soul finding 3): the unaided fallback for Tracking, alongside
// UnaidedAccuracy above -- an entity with no working targeting system used to fall back to Tracking 0,
// which Commit turned into a hard wall (any nonzero deviation was an automatic miss). Ten world units of
// fire-time-projection deviation forgiveness is the first guess and the operator's knob, the same shape
// as UnaidedAccuracy: authored deliberately bad, not authored broken.
public float UnaidedTracking = 10f;
Cut 6d: the dart-throw kernel replaces the coin-flip landing rule FireControl.Commit used to place a hit with a coin flip between exactly the aimed item and a uniform-random hull cell -- no spatial term, so a near-miss on a stern item was as likely to land in the bow as the stern. One Gaussian kernel, in hull-schematic cell units, now decides both halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over every occupied cell, sigma = 1/Precision. HitProbability folds the kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6 unaffected); Commit draws the landing cell from the same weights. A shot that passes the roll always lands on metal -- the off-hull chance is priced into that one roll, never resolved as a second miss stage. The uniform-random branch is deleted outright, not demoted. Precision now feeds every shot's pOnHull, aimed or not, so its unaided fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/ UnaidedTracking floors. pOnHull is saturated: the discrete sum over- estimates the continuous kernel's integral once sigma drops below about one cell, and a mass share cannot exceed the whole kernel. Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and TargetingSystemTests authored Precision as 0 (or relied on the old 0 fallback) purely as a don't-care value under the coin-flip rule, where it only gated the aimed-item branch. Under the kernel it drives every shot's sigma, so those fixtures are re-authored to keep pOnHull near its ceiling and stay pinned on what they actually test. mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's return line and the deleted coin-flip branch are updated to the equivalent lines/mutations on the new code.
2026-09-19 21:37:39 +02:00
// Cut 6d (docs/fire-control-cut.md): the unaided fallback for Precision, alongside UnaidedAccuracy and
// UnaidedTracking above -- FireControl.Precision falls back to this the same way it already falls back to
// UnaidedAccuracy/UnaidedTracking. Precision is now a grouping tightness (FireControl.Sigma = 1/Precision,
// in hull-schematic cell units), not the old coin-flip probability, so the unaided floor has to be
// authored on that scale. .3 -> sigma 3.33 cells: against the live catalog's own hulls (Longinus 6x17,
Cut 6d: the dart-throw kernel replaces the coin-flip landing rule FireControl.Commit used to place a hit with a coin flip between exactly the aimed item and a uniform-random hull cell -- no spatial term, so a near-miss on a stern item was as likely to land in the bow as the stern. One Gaussian kernel, in hull-schematic cell units, now decides both halves of "where will this shot go": w(cell) = exp(-d^2/2*sigma^2) over every occupied cell, sigma = 1/Precision. HitProbability folds the kernel's on-hull mass share (pOnHull) into PBase at fire time (R10/Q6 unaffected); Commit draws the landing cell from the same weights. A shot that passes the roll always lands on metal -- the off-hull chance is priced into that one roll, never resolved as a second miss stage. The uniform-random branch is deleted outright, not demoted. Precision now feeds every shot's pOnHull, aimed or not, so its unaided fallback (FireControl.Precision) reads GameplaySettings.UnaidedPrecision instead of a bare 0 -- the same shape as the existing UnaidedAccuracy/ UnaidedTracking floors. pOnHull is saturated: the discrete sum over- estimates the continuous kernel's integral once sigma drops below about one cell, and a mass share cannot exceed the whole kernel. Existing fixtures across FireAuthorityTests, FireControlCut4/5/6Tests and TargetingSystemTests authored Precision as 0 (or relied on the old 0 fallback) purely as a don't-care value under the coin-flip rule, where it only gated the aimed-item branch. Under the kernel it drives every shot's sigma, so those fixtures are re-authored to keep pOnHull near its ceiling and stay pinned on what they actually test. mutation_tests_fire_control_cut3.sh's two anchors into HitProbability's return line and the deleted coin-flip branch are updated to the equivalent lines/mutations on the new code.
2026-09-19 21:37:39 +02:00
// Zenith 12x12, Turret 8x8) that puts pOnHull at the hull's own centre of mass around .5-.84 -- broadly
// sprayed across the whole silhouette (sigma is a large fraction of the hull's own width) while still
// landing on the ship more often than not. A tighter floor (e.g. .1, sigma 10) was tried first and
// rejected: it drove pOnHull for the same hulls down to .08-.18, reading as "can't hit the broad side of a
// barn" rather than "sprays the silhouette." First guess, the same deliberately-bad shape as its two
// siblings; the operator rules the real figure in play.
public float UnaidedPrecision = .3f;
public float AgentRangeExponent = .25f;
public float AgentForwardLerp = .5f;
public float AgentMaxForwardDistance = 50;
Fire control Cut 3: hit authority moves to FireControl Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and Lightning) and replaces it with a single owner in the same cut: FireControl now decides whether a shot hits, Zone owns the pending-shot lifecycle, and Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies nothing. FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor * pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the weapon's payload and the shooter's resolved targeting stats at the instant the trigger is pulled (Q6), computes the predicted intercept through the one shared PredictedIntercept function (CombatState now calls the same function instead of duplicating first_order_intercept), and queues a PendingShot. FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon -- deviation from the predicted intercept, measured once, live, at that instant -- and applies it at ArrivalTime. A shot whose flight is shorter than the horizon commits and resolves in the same tick it fires. Rolls draw from the shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc or otherwise gated shot draws nothing. Player fire is arc-gated through the same predicate AI and turret fire use (Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every shooter's trigger passes through), closing Q2. Adds GameplaySettings.CommitHorizon, SchematicCellSize and AgentMinHitProbability (first guesses; the headless suite is the tuning harness, operator play smoke is the arbiter). FORK reported, not worked around silently: HullCollider.SendHit/SendSplash cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs, ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them from their own live Physics queries -- that's Cut 4's migration. They fire into the void today: nothing subscribes to Hit/Splash any more for the four weapon kinds this cut owns. tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its evasion window, short-flight degradation, the frozen-payload snapshot, aimed hits, the item-then-hull damage order, the planar penetration march and the shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are updated for the new AI gate and OnFire(int) signature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
// Cut 3 (docs/fire-control-cut.md): first guesses: the headless fixture is the tuning harness, the
// operator smoke is the arbiter (§ Risks).
//
// How long before impact a shot's outcome commits (R4). Deviation counts up to this horizon; after it
// the result is frozen and the rest of the flight is pure choreography.
public float CommitHorizon = .5f;
// The window of fire control's solution: how far ahead a target's reach counts (Reach, Evasion) and the time
// constant of the manoeuvre track that observes it. A fire-control constant, not a weapon or missile stat. First
// guess; the operator rules the real figure in play.
public float SolutionWindow = .75f;
Fire control Cut 3: hit authority moves to FireControl Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and Lightning) and replaces it with a single owner in the same cut: FireControl now decides whether a shot hits, Zone owns the pending-shot lifecycle, and Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies nothing. FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor * pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the weapon's payload and the shooter's resolved targeting stats at the instant the trigger is pulled (Q6), computes the predicted intercept through the one shared PredictedIntercept function (CombatState now calls the same function instead of duplicating first_order_intercept), and queues a PendingShot. FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon -- deviation from the predicted intercept, measured once, live, at that instant -- and applies it at ArrivalTime. A shot whose flight is shorter than the horizon commits and resolves in the same tick it fires. Rolls draw from the shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc or otherwise gated shot draws nothing. Player fire is arc-gated through the same predicate AI and turret fire use (Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every shooter's trigger passes through), closing Q2. Adds GameplaySettings.CommitHorizon, SchematicCellSize and AgentMinHitProbability (first guesses; the headless suite is the tuning harness, operator play smoke is the arbiter). FORK reported, not worked around silently: HullCollider.SendHit/SendSplash cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs, ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them from their own live Physics queries -- that's Cut 4's migration. They fire into the void today: nothing subscribes to Hit/Splash any more for the four weapon kinds this cut owns. tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its evasion window, short-flight degradation, the frozen-payload snapshot, aimed hits, the item-then-hull damage order, the planar penetration march and the shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are updated for the new AI gate and OnFire(int) signature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
// World units per hull-schematic cell, used only by FireControl.HitProbability's angular-size term
// (pSpread) to turn a hull's cell footprint into a real-world silhouette size at range.
public float SchematicCellSize = 2f;
// Restitution of a hull striking a hull (Contacts.Step): the speed at which a pair separates along the contact
// normal is this times the speed at which it closed. 0 is perfectly inelastic, 1 perfectly elastic.
public float ContactRestitution = .5f;
// Heights above the field of the ship plane, the orbital layer (stations, turrets: every OrbitalEntity) and the floater
// layer (loot, mines): orbitals above ships and floaters below them, so no ship ever touches either (ruling
// aetheria-gravity:orbitals-above-floaters-below).
public float ShipLayerHeight = 3f;
public float OrbitalLayerHeight = 15f;
public float FloaterLayerHeight = 0f;
Fire control Cut 3: hit authority moves to FireControl Deletes the Unity physics hit path (HullCollider.Hit/Splash, and the raycast/shield/SendHit blocks in Projectile, GuidedProjectile, Laser and Lightning) and replaces it with a single owner in the same cut: FireControl now decides whether a shot hits, Zone owns the pending-shot lifecycle, and Entity.DamageSchematic/ApplyHit (moved verbatim from EntityInstance) apply the result. Presentation subscribes to Zone.ShotCommitted/ShotResolved and applies nothing. FireControl.HitProbability is the pure targeting solution (Accuracy * pSensor * pSpread, gated by detection/range/lock/arc). FireControl.Fire freezes the weapon's payload and the shooter's resolved targeting stats at the instant the trigger is pulled (Q6), computes the predicted intercept through the one shared PredictedIntercept function (CombatState now calls the same function instead of duplicating first_order_intercept), and queues a PendingShot. FireControl.Step commits each shot's outcome at ArrivalTime - CommitHorizon -- deviation from the predicted intercept, measured once, live, at that instant -- and applies it at ArrivalTime. A shot whose flight is shorter than the horizon commits and resolves in the same tick it fires. Rolls draw from the shooter's own ItemManager.Random (Q5, seeded and reproducible); an out-of-arc or otherwise gated shot draws nothing. Player fire is arc-gated through the same predicate AI and turret fire use (Weapon.ArcAllowsFire, checked in InstantWeapon.Trigger -- the one point every shooter's trigger passes through), closing Q2. Adds GameplaySettings.CommitHorizon, SchematicCellSize and AgentMinHitProbability (first guesses; the headless suite is the tuning harness, operator play smoke is the arbiter). FORK reported, not worked around silently: HullCollider.SendHit/SendSplash cannot be deleted yet without breaking ConstantLaser.cs, ConstantLightning.cs, ConstantParticleWeapon.cs, HitscanEffect.cs and Mine.cs, which still call them from their own live Physics queries -- that's Cut 4's migration. They fire into the void today: nothing subscribes to Hit/Splash any more for the four weapon kinds this cut owns. tests/Aetheria.Shared.Tests/FireAuthorityTests.cs pins the roll's seeding, the gate-consumes-no-draw rule, resolve-on-arrival, the commit horizon and its evasion window, short-flight degradation, the frozen-payload snapshot, aimed hits, the item-then-hull damage order, the planar penetration march and the shield-absorbs branch. FireControlTests.cs and InputCapacitorTests.cs are updated for the new AI gate and OnFire(int) signature. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:07:07 +02:00
// The floor an AI's own predicted hit probability (FireControl.HitProbability) must clear before it counts
// 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;
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): 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]
public class RarityTier
{
public string Name;
public float Quality;
public float3 Color;
}