From 664e4c2de46bb22ce328749d450aa0cb0eb6eb13 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 11 Jul 2026 20:16:39 +0100 Subject: [PATCH] Restore progressive weapon lock --- Aetheria.State.Daemon.Smoke/Program.cs | 8 ++++ .../AetheriaRuntimeDaemonSimulation.cs | 47 +++++++++++++++++-- ...AetheriaRuntimeDaemonSimulationSettings.cs | 29 +++++++++++- docs/original-game-migration-matrix.md | 4 +- 4 files changed, 79 insertions(+), 9 deletions(-) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 4bc47d350..006f0a3cb 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -509,6 +509,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks }; var sawTargetCommand = false; var sawFireCommand = false; + var sawPartialLock = false; + var firstLaunchFrame = -1; for (var frame = 0; frame < 60; frame++) { var tick = AetheriaRuntimeDaemonTickRunner.Tick( @@ -526,12 +528,18 @@ internal sealed class AetheriaDaemonYmirSmokeChecks }); sawTargetCommand |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":target", StringComparison.Ordinal)); sawFireCommand |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":fire", StringComparison.Ordinal)); + var weapon = (agent.WeaponStates ?? Array.Empty()).Single(); + sawPartialLock |= weapon.LockProgress > 0 && weapon.LockProgress < 0.99; + if (firstLaunchFrame < 0 && run.GameEvents.Any(value => value.Kind == "projectile.launched")) + firstLaunchFrame = frame; if (string.Equals(run.AgentTasks.Single().Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal)) break; } Require(sawTargetCommand, "attack agent must target through the shared target command"); Require(sawFireCommand, "attack agent must fire through the shared weapon-group command"); + Require(sawPartialLock, "attack agent must acquire a persisted partial weapon lock before firing"); + Require(firstLaunchFrame > 1, "accepted fire intent must not bypass progressive weapon lock acquisition"); Require(run.GameEvents.Any(value => value.Kind == "projectile.launched" && value.SourceEntityIndex == agent.EntityIndex), "accepted fire control must emit authoritative projectile launch chronology"); Require(!target.IsActive, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index 51c5fd8de..80d815e3a 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -249,16 +249,16 @@ namespace GameCult.Aetheria.State.Verse !IsAlive(target) || !Hostile(attacker, target)) { + weaponState.LockTargetEntityIndex = -1; + weaponState.LockProgress = 0; continue; } - if (DistanceSq(attacker, target) > settings.AttackRange * settings.AttackRange) + UpdateWeaponLock(attacker, target, weaponState, deltaSeconds, settings); + if (DistanceSq(attacker, target) > settings.AttackRange * settings.AttackRange || + weaponState.LockProgress <= 0.99) continue; - Face(attacker, target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ); - weaponState.LockTargetEntityIndex = target.EntityIndex; - weaponState.LockProgress = 1.0; - if (weaponState.CooldownProgress > 0) continue; @@ -308,6 +308,43 @@ namespace GameCult.Aetheria.State.Verse } } + private static void UpdateWeaponLock( + AetheriaRuntimeEntitySnapshotCommit attacker, + AetheriaRuntimeEntitySnapshotCommit target, + AetheriaRuntimeWeaponStateCommit weaponState, + double deltaSeconds, + AetheriaRuntimeDaemonSimulationSettings settings) + { + if (weaponState.LockTargetEntityIndex != target.EntityIndex) + { + weaponState.LockTargetEntityIndex = target.EntityIndex; + weaponState.LockProgress = 0; + } + + var targetDirection = Normalize(target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ); + var lookDirection = Normalize(attacker.DirectionX, attacker.DirectionY); + var dot = Math.Max(-1.0, Math.Min(1.0, + targetDirection.X * lookDirection.X + targetDirection.Y * lookDirection.Y)); + var angleDegrees = Math.Acos(dot) * 180.0 / Math.PI; + if (angleDegrees >= settings.WeaponLockAngleDegrees) + { + weaponState.LockProgress = Clamp01( + weaponState.LockProgress - deltaSeconds * settings.WeaponLockDecayPerSecond); + return; + } + + var contact = (attacker.Contacts ?? Array.Empty()) + .FirstOrDefault(candidate => candidate != null && candidate.TargetEntityIndex == target.EntityIndex); + var information = Clamp01(contact?.InfoGathered ?? 0); + var directionalQuality = Math.Max(0, 1.0 - angleDegrees / 90.0); + var acquisition = + Math.Pow(directionalQuality, settings.WeaponLockDirectionImpact) * + deltaSeconds * + settings.WeaponLockSpeed * + Math.Pow(information, settings.WeaponLockSensorImpact); + weaponState.LockProgress = Clamp01(weaponState.LockProgress + acquisition); + } + private static void PrepareProjectiles( AetheriaRuntimeZoneSnapshotCommit zone, IReadOnlyDictionary entities, diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs index a6dc8148d..9bb6b8aa5 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs @@ -29,7 +29,12 @@ namespace GameCult.Aetheria.State.Verse playerEntityHull: 120.0, raiderEntityHull: 80.0, stationShield: 120.0, - entityShield: 45.0); + entityShield: 45.0, + weaponLockSpeed: 2.0, + weaponLockSensorImpact: 1.0, + weaponLockAngleDegrees: 45.0, + weaponLockDirectionImpact: 1.0, + weaponLockDecayPerSecond: 1.0); public AetheriaRuntimeDaemonSimulationSettings( double pawnSpeed, @@ -52,7 +57,12 @@ namespace GameCult.Aetheria.State.Verse double playerEntityHull, double raiderEntityHull, double stationShield, - double entityShield) + double entityShield, + double weaponLockSpeed = AetheriaDefaultRaw.WeaponLockSpeed, + double weaponLockSensorImpact = AetheriaDefaultRaw.WeaponLockSensorImpact, + double weaponLockAngleDegrees = AetheriaDefaultRaw.WeaponLockAngleDegrees, + double weaponLockDirectionImpact = AetheriaDefaultRaw.WeaponLockDirectionImpact, + double weaponLockDecayPerSecond = AetheriaDefaultRaw.WeaponLockDecayPerSecond) { PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed); RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed); @@ -75,6 +85,11 @@ namespace GameCult.Aetheria.State.Verse RaiderEntityHull = PositiveOr(raiderEntityHull, AetheriaDefaultRaw.RaiderEntityHull); StationShield = NonNegativeOr(stationShield, AetheriaDefaultRaw.StationShield); EntityShield = NonNegativeOr(entityShield, AetheriaDefaultRaw.EntityShield); + WeaponLockSpeed = PositiveOr(weaponLockSpeed, AetheriaDefaultRaw.WeaponLockSpeed); + WeaponLockSensorImpact = NonNegativeOr(weaponLockSensorImpact, AetheriaDefaultRaw.WeaponLockSensorImpact); + WeaponLockAngleDegrees = PositiveOr(weaponLockAngleDegrees, AetheriaDefaultRaw.WeaponLockAngleDegrees); + WeaponLockDirectionImpact = NonNegativeOr(weaponLockDirectionImpact, AetheriaDefaultRaw.WeaponLockDirectionImpact); + WeaponLockDecayPerSecond = NonNegativeOr(weaponLockDecayPerSecond, AetheriaDefaultRaw.WeaponLockDecayPerSecond); } public double PawnSpeed { get; } @@ -98,6 +113,11 @@ namespace GameCult.Aetheria.State.Verse public double RaiderEntityHull { get; } public double StationShield { get; } public double EntityShield { get; } + public double WeaponLockSpeed { get; } + public double WeaponLockSensorImpact { get; } + public double WeaponLockAngleDegrees { get; } + public double WeaponLockDirectionImpact { get; } + public double WeaponLockDecayPerSecond { get; } private static double PositiveOr(double value, double fallback) { @@ -137,6 +157,11 @@ namespace GameCult.Aetheria.State.Verse public const double RaiderEntityHull = 80.0; public const double StationShield = 120.0; public const double EntityShield = 45.0; + public const double WeaponLockSpeed = 2.0; + public const double WeaponLockSensorImpact = 1.0; + public const double WeaponLockAngleDegrees = 45.0; + public const double WeaponLockDirectionImpact = 1.0; + public const double WeaponLockDecayPerSecond = 1.0; } } } diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 0b276eeaf..8056563b4 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -19,11 +19,11 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity. | Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | owner-exists; wired; projected; not proven | loadout round trip and reconnect | | Flight intent | `Ship`, `Thruster`, `AetherDrive`, input paths | daemon intent + Ymir physics | analog movement/look/tractor | EveUnity input driver | movement intents feed Ymir-owned body integration; authored thrust allocation, torque, energy and heat remain simplified | prove original drive/equipment behavior ordering and runtime interpolation | | Collision | Unity physics and entity handlers | Ymir | collision/result facts | visual interpolation only | Ymir.Core world stepping wired for entity bodies, radial fields and contacts; missing-owner negative proof passes | publish authoritative collision events and prove Unity cannot write transforms back | -| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; not parity | visibility/nearest/explicit/lock cases | +| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; daemon weapon lock now acquires and decays from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock | target-switch reset, angular decay, visibility/nearest/explicit cases, and runtime lock feedback | | Projectile flight | weapon/projectile classes | Ymir | projectile entity/event projection | native projectile visual | Ymir.Core adapter wired for spawn/travel/contact/despawn; native presentation and broader weapon parity incomplete | prove every authored projectile family and runtime visual lifecycle | | Shield/hull damage | energy-funded shield, armor cells, equipment and hull | daemon | entity status + damage events | meters, impacts, damage VFX | synthetic scalar owner wired; not parity | typed absorption, penetration, item damage and death causes | | Thermal/energy | cell conduction and capacitor/reactor/radiator network | daemon | ship/equipment status and warnings | schematic/post effects | cell conduction/radiation wired; energy network and medical thresholds remain reference | capacitor/reactor/radiator ordering, shared energy and medical timelines | -| Weapons | ServerShared behaviors + Unity effects | daemon + Ymir where spatial | action capability, weapon state, attack events | effects/audio/HUD | one synthetic weapon wired; original engine reference-only | each weapon family | +| Weapons | ServerShared behaviors + Unity effects | daemon + Ymir where spatial | action capability, weapon state, attack events | effects/audio/HUD | one synthetic projectile weapon wired through progressive fossil-derived lock, cooldown, heat, Ymir flight/contact, and daemon damage; original behavior families remain reference-only | catalog-authored weapon stats and each weapon family | | Destruction/drops | entity death and pickup paths | daemon | death/drop events and world entities | effects and pickups | fragments wired; chronology and original drop transaction absent | exactly-once drops and cleanup | | Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; shield contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; 30s daemon lifetime; instant cargo teleport deleted | prove generic runtime beam, contact, success and rejection feedback | | Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | daemon path wired/projected; Ymir placement and parity unproven | priority, eligibility, placement and reconnect |