Restore progressive weapon lock

This commit is contained in:
Meta Vens 2026-07-11 20:16:39 +01:00
commit 664e4c2de4
4 changed files with 79 additions and 9 deletions

View file

@ -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<AetheriaRuntimeWeaponStateCommit>()).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,

View file

@ -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<AetheriaRuntimeEntityContactCommit>())
.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<int, AetheriaRuntimeEntitySnapshotCommit> entities,

View file

@ -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;
}
}
}

View file

@ -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 |