Restore instant weapon burst timing
This commit is contained in:
parent
91ececd147
commit
c31aabe88e
5 changed files with 68 additions and 20 deletions
|
|
@ -625,9 +625,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(12, new AetheriaRuntimeBehaviorValue(
|
||||
"item-key", "", 0, false, "", "test-ammo",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(), Array.Empty<AetheriaRuntimeBehaviorMapEntry>())),
|
||||
new AetheriaRuntimeBehaviorField(13, Number(2)),
|
||||
new AetheriaRuntimeBehaviorField(13, Number(6)),
|
||||
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
|
||||
new AetheriaRuntimeBehaviorField(16, PerformanceStat(330)),
|
||||
new AetheriaRuntimeBehaviorField(17, PerformanceStat(3)),
|
||||
new AetheriaRuntimeBehaviorField(18, PerformanceStat(0.3)),
|
||||
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.2)),
|
||||
new AetheriaRuntimeBehaviorField(21, PerformanceStat(2)),
|
||||
new AetheriaRuntimeBehaviorField(22, PerformanceStat(1)),
|
||||
|
|
@ -705,19 +707,21 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Require(run.GameEvents.Any(value => value.Kind == "projectile.launched" && value.SourceEntityIndex == agent.EntityIndex),
|
||||
"accepted fire control must emit authoritative projectile launch chronology");
|
||||
Require(run.GameEvents.Any(value => value.Kind == "projectile.launched" &&
|
||||
value.ItemKey == "test-lock-cannon" && Math.Abs(value.ScalarValue - 7) < 0.000001),
|
||||
"daemon combat must launch the equipped catalog weapon with its authored damage");
|
||||
value.ItemKey == "test-lock-cannon" && Math.Abs(value.ScalarValue - (7.0 / 3.0)) < 0.000001),
|
||||
"daemon combat must divide authored damage across the configured burst rounds");
|
||||
Require((agent.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>()).Any(value => value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind &&
|
||||
value.OwnerIndex == 0 && value.BehaviorKind == "LockWeapon"),
|
||||
"weapon progress must belong to the equipped authored behavior instead of a synthetic entity weapon");
|
||||
var authoredWeapon = agent.WeaponStates.Single(value =>
|
||||
value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind);
|
||||
RequireEqual(1, authoredWeapon.Ammo,
|
||||
"a committed authored weapon round must decrement exactly one magazine round");
|
||||
RequireEqual(3, authoredWeapon.Ammo,
|
||||
"a three-round authored burst must decrement one magazine round per committed projectile");
|
||||
RequireNear(45, agent.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
|
||||
0.000001, "a committed authored weapon round must drain its energy from canonical capacitor state");
|
||||
RequireEqual(2, CargoQuantity(agent, "test-ammo"),
|
||||
"cargo ammunition must pay for reload, not each magazine round");
|
||||
Require(run.GameEvents.Count(value => value.Kind == "projectile.launched" && value.ItemKey == "test-lock-cannon") >= 3,
|
||||
"authored burst count and interval must emit every due round through the projectile owner");
|
||||
Require(!target.IsActive,
|
||||
$"attack task must end through daemon damage after Ymir projectile contacts; hull={Stat(target, "hull"):0.###} " +
|
||||
$"agent={agent.PositionX:0.###},{agent.PositionZ:0.###} target={target.PositionX:0.###},{target.PositionZ:0.###} " +
|
||||
|
|
|
|||
|
|
@ -16599,6 +16599,9 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
|
|||
"AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)",
|
||||
"EnsureWeaponState(",
|
||||
"CommitWeaponRound(attacker, weapon)",
|
||||
"weapon.State.BurstRemaining = weapon.BurstCount",
|
||||
"weapon.State.BurstTimer += deltaSeconds",
|
||||
"weapon.SingleAmmoBurst",
|
||||
"AetheriaRuntimeCargoTransactions.TryRemoveQuantity(",
|
||||
"CommitEnergy(entity, weapon.Energy)",
|
||||
"SpawnProjectile(zone, attacker, target, weapon, settings)",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
Number,
|
||||
Integer,
|
||||
Boolean,
|
||||
PerformanceStat,
|
||||
Temperature
|
||||
}
|
||||
|
|
@ -62,7 +63,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
};
|
||||
|
||||
private static readonly AetheriaRuntimeBehaviorFieldMetadata[] InstantWeaponFields =
|
||||
With(WeaponFields, Stat("BurstCount", 17), Stat("BurstTime", 18), Stat("Cooldown", 19));
|
||||
With(WeaponFields, Stat("BurstCount", 17), Stat("BurstTime", 18), Stat("Cooldown", 19), Boolean("SingleAmmoBurst", 20));
|
||||
|
||||
private static readonly AetheriaRuntimeBehaviorFieldMetadata[] LockWeaponFields =
|
||||
With(InstantWeaponFields, Stat("LockSpeed", 21), Stat("SensorImpact", 22), Stat("LockAngle", 23),
|
||||
|
|
@ -161,6 +162,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Integer);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Boolean(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Boolean);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Temperature(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Temperature);
|
||||
|
|
|
|||
|
|
@ -279,18 +279,37 @@ namespace GameCult.Aetheria.State.Verse
|
|||
UpdateWeaponLock(attacker, target, weapon, deltaSeconds, settings);
|
||||
if (DistanceSq(attacker, target) > weapon.Range * weapon.Range ||
|
||||
weapon.State.LockProgress <= 0.99 ||
|
||||
weapon.State.CooldownProgress > 0 ||
|
||||
weapon.State.Reloading)
|
||||
continue;
|
||||
|
||||
if (CommitWeaponRound(attacker, weapon) != WeaponRoundResult.Fired)
|
||||
continue;
|
||||
var projectile = SpawnProjectile(zone, attacker, target, weapon, settings);
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit { EventId = $"projectile:{projectile.ProjectileId}:launched", Kind = "projectile.launched", FrameId = frameId, ZoneIndex = zone.ZoneIndex, SourceEntityIndex = attacker.EntityIndex, TargetEntityIndex = target.EntityIndex, SubjectKey = projectile.ProjectileId, ItemKey = projectile.WeaponKind, ScalarValue = projectile.Damage, PositionX = projectile.PositionX, PositionZ = projectile.PositionZ });
|
||||
weapon.State.Firing = true;
|
||||
weapon.State.CoolingDown = true;
|
||||
weapon.State.CooldownProgress = weapon.Cooldown;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.Heat);
|
||||
if (weapon.State.BurstRemaining <= 0)
|
||||
{
|
||||
if (weapon.State.CooldownProgress > 0)
|
||||
continue;
|
||||
if (weapon.SingleAmmoBurst && CommitWeaponRound(attacker, weapon) != WeaponRoundResult.Fired)
|
||||
continue;
|
||||
weapon.State.BurstRemaining = weapon.BurstCount;
|
||||
weapon.State.BurstInterval = weapon.BurstTime / weapon.BurstCount;
|
||||
weapon.State.BurstTimer = 0;
|
||||
weapon.State.CoolingDown = true;
|
||||
weapon.State.CooldownProgress = weapon.Cooldown;
|
||||
}
|
||||
|
||||
weapon.State.BurstTimer += deltaSeconds;
|
||||
while (weapon.State.BurstRemaining > 0 && weapon.State.BurstTimer > 0)
|
||||
{
|
||||
if (!weapon.SingleAmmoBurst && CommitWeaponRound(attacker, weapon) != WeaponRoundResult.Fired)
|
||||
{
|
||||
weapon.State.BurstRemaining = 0;
|
||||
break;
|
||||
}
|
||||
weapon.State.BurstRemaining--;
|
||||
weapon.State.BurstTimer -= weapon.State.BurstInterval;
|
||||
var projectile = SpawnProjectile(zone, attacker, target, weapon, settings);
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit { EventId = $"projectile:{projectile.ProjectileId}:launched", Kind = "projectile.launched", FrameId = frameId, ZoneIndex = zone.ZoneIndex, SourceEntityIndex = attacker.EntityIndex, TargetEntityIndex = target.EntityIndex, SubjectKey = projectile.ProjectileId, ItemKey = projectile.WeaponKind, ScalarValue = projectile.Damage, PositionX = projectile.PositionX, PositionZ = projectile.PositionZ });
|
||||
weapon.State.Firing = true;
|
||||
AetheriaRuntimeThermalSimulation.AddHeat(attacker, weapon.Heat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -465,15 +484,19 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return new ResolvedWeapon(
|
||||
state,
|
||||
behavior.Item.ItemKey,
|
||||
PositiveOr(behavior.EvaluateStat(2), ResolveProjectileDamage(entity, settings)),
|
||||
PositiveOr(behavior.EvaluateStat(2), ResolveProjectileDamage(entity, settings)) /
|
||||
Math.Max(1, (int)Math.Round(behavior.EvaluateStat(17))),
|
||||
PositiveOr(behavior.EvaluateStat(6), settings.AttackRange),
|
||||
PositiveOr(behavior.EvaluateStat(19), settings.WeaponCooldownSeconds),
|
||||
PositiveOr(behavior.EvaluateStat(16), settings.ProjectileSpeed),
|
||||
Math.Max(0, behavior.EvaluateStat(10)),
|
||||
Math.Max(0, behavior.EvaluateStat(9)),
|
||||
Math.Max(0, behavior.EvaluateStat(10)) / Math.Max(1, (int)Math.Round(behavior.EvaluateStat(17))),
|
||||
Math.Max(0, behavior.EvaluateStat(9)) / Math.Max(1, (int)Math.Round(behavior.EvaluateStat(17))),
|
||||
ReadItemKey(behavior.Payload, 12),
|
||||
Math.Max(0, (int)Math.Round(ReadNumber(behavior.Payload, 13))),
|
||||
PositiveOr(ReadNumber(behavior.Payload, 14), settings.WeaponCooldownSeconds),
|
||||
Math.Max(1, (int)Math.Round(behavior.EvaluateStat(17))),
|
||||
Math.Max(0, behavior.EvaluateStat(18)),
|
||||
ReadBool(behavior.Payload, 20),
|
||||
PositiveOr(behavior.EvaluateStat(21), settings.WeaponLockSpeed),
|
||||
Math.Max(0, behavior.EvaluateStat(22)),
|
||||
PositiveOr(behavior.EvaluateStat(23), settings.WeaponLockAngleDegrees),
|
||||
|
|
@ -491,6 +514,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
ResolveProjectileDamage(entity, settings), settings.AttackRange, settings.WeaponCooldownSeconds,
|
||||
settings.ProjectileSpeed, ResolveProjectileDamage(entity, settings) * settings.ProjectileHeatScale,
|
||||
0, "", -1, settings.WeaponCooldownSeconds,
|
||||
1, 0, false,
|
||||
settings.WeaponLockSpeed, settings.WeaponLockSensorImpact, settings.WeaponLockAngleDegrees,
|
||||
settings.WeaponLockDirectionImpact, settings.WeaponLockDecayPerSecond);
|
||||
}
|
||||
|
|
@ -603,6 +627,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.FirstOrDefault(field => field != null && field.Key == key)?.Value?.NumberValue ?? 0;
|
||||
}
|
||||
|
||||
private static bool ReadBool(AetheriaRuntimeBehaviorPayload payload, int key)
|
||||
{
|
||||
return (payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value?.BoolValue ?? false;
|
||||
}
|
||||
|
||||
private static WeaponRoundResult CommitWeaponRound(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
ResolvedWeapon weapon)
|
||||
|
|
@ -670,12 +700,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
public ResolvedWeapon(AetheriaRuntimeWeaponStateCommit state, string itemKey, double damage, double range,
|
||||
double cooldown, double projectileSpeed, double heat, double energy, string ammoItemKey,
|
||||
int magazineSize, double reloadTime, double lockSpeed, double lockSensorImpact,
|
||||
int magazineSize, double reloadTime, int burstCount, double burstTime, bool singleAmmoBurst,
|
||||
double lockSpeed, double lockSensorImpact,
|
||||
double lockAngleDegrees, double lockDirectionImpact, double lockDecayPerSecond)
|
||||
{
|
||||
State = state; ItemKey = itemKey; Damage = damage; Range = range; Cooldown = cooldown;
|
||||
ProjectileSpeed = projectileSpeed; Heat = heat; Energy = energy; AmmoItemKey = ammoItemKey;
|
||||
MagazineSize = magazineSize; ReloadTime = reloadTime; LockSpeed = lockSpeed;
|
||||
BurstCount = burstCount; BurstTime = burstTime; SingleAmmoBurst = singleAmmoBurst;
|
||||
LockSensorImpact = lockSensorImpact; LockAngleDegrees = lockAngleDegrees;
|
||||
LockDirectionImpact = lockDirectionImpact; LockDecayPerSecond = lockDecayPerSecond;
|
||||
}
|
||||
|
|
@ -691,6 +723,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public string AmmoItemKey { get; }
|
||||
public int MagazineSize { get; }
|
||||
public double ReloadTime { get; }
|
||||
public int BurstCount { get; }
|
||||
public double BurstTime { get; }
|
||||
public bool SingleAmmoBurst { get; }
|
||||
public double LockSpeed { get; }
|
||||
public double LockSensorImpact { get; }
|
||||
public double LockAngleDegrees { get; }
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| 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 | selected weapon groups resolve equipped typed-catalog behaviors for damage, range, cooldown, projectile velocity, heat, lock, energy, magazine and reload parameters; one committed round atomically validates ammo and energy, drains canonical capacitor/reactor state, decrements the magazine, then launches through Ymir and adds heat; empty magazines consume reserve cargo through the shared cargo transaction and enter reload without firing; temporal state belongs to the equipped behavior; catalog-free fixtures retain an explicit synthetic fallback | burst timing/single-ammo-burst semantics, reload boundary timeline, visibility/wear, and each remaining weapon family |
|
||||
| Weapons | ServerShared behaviors + Unity effects | daemon + Ymir where spatial | action capability, weapon state, attack events | effects/audio/HUD | selected weapon groups resolve equipped typed-catalog behaviors for damage, range, cooldown, projectile velocity, heat, lock, energy, magazine, reload, burst count/time and single-ammo-burst parameters; persistent burst timer/remaining state emits every due round while cooldown overlaps from trigger time; damage/heat/energy divide across rounds and resource payment occurs once per burst or once per round as authored; each committed transaction drains canonical capacitor/reactor state and magazine before Ymir launch; empty magazines consume reserve cargo through the shared cargo transaction and enter reload without firing; catalog-free fixtures retain an explicit synthetic fallback | strict reload/cooldown boundary timeline, visibility/wear, and each remaining 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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue