Restore weapon resource transactions
This commit is contained in:
parent
0f07aaafb5
commit
91ececd147
7 changed files with 212 additions and 40 deletions
|
|
@ -604,6 +604,15 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
}
|
||||
}
|
||||
];
|
||||
agent.BehaviorStates =
|
||||
[
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = "fixture", OwnerIndex = 0, BehaviorIndex = 0,
|
||||
BehaviorKind = "Capacitor", CapacitorCharge = 50, CapacitorCapacity = 50, CapacitorEfficiency = 1
|
||||
}
|
||||
];
|
||||
agent.CargoContents = [Cargo(("test-ammo", 2, 0, 0))];
|
||||
var weaponPayload = new AetheriaRuntimeBehaviorPayload(
|
||||
0,
|
||||
"LockWeapon",
|
||||
|
|
@ -611,7 +620,13 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
[
|
||||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(7)),
|
||||
new AetheriaRuntimeBehaviorField(6, PerformanceStat(145)),
|
||||
new AetheriaRuntimeBehaviorField(9, PerformanceStat(5)),
|
||||
new AetheriaRuntimeBehaviorField(10, PerformanceStat(3)),
|
||||
new AetheriaRuntimeBehaviorField(12, new AetheriaRuntimeBehaviorValue(
|
||||
"item-key", "", 0, false, "", "test-ammo",
|
||||
Array.Empty<AetheriaRuntimeBehaviorValue>(), Array.Empty<AetheriaRuntimeBehaviorMapEntry>())),
|
||||
new AetheriaRuntimeBehaviorField(13, Number(2)),
|
||||
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
|
||||
new AetheriaRuntimeBehaviorField(16, PerformanceStat(330)),
|
||||
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.2)),
|
||||
new AetheriaRuntimeBehaviorField(21, PerformanceStat(2)),
|
||||
|
|
@ -626,7 +641,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var target = Entity(1, 105, "raider");
|
||||
target.Kind = "station";
|
||||
target.StatGrids = [Grid("hull", 24), Grid("shield", 0), Grid("heat", 0)];
|
||||
target.StatGrids = [Grid("hull", 7), Grid("shield", 0), Grid("heat", 0)];
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "agent-attack-smoke",
|
||||
|
|
@ -695,6 +710,14 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
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");
|
||||
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(!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.###} " +
|
||||
|
|
|
|||
|
|
@ -16598,6 +16598,9 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root)
|
|||
"StepCombat(run, zone, entities, intents, deltaSeconds, settings, projectilePhysics, catalog, frameId)",
|
||||
"AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)",
|
||||
"EnsureWeaponState(",
|
||||
"CommitWeaponRound(attacker, weapon)",
|
||||
"AetheriaRuntimeCargoTransactions.TryRemoveQuantity(",
|
||||
"CommitEnergy(entity, weapon.Energy)",
|
||||
"SpawnProjectile(zone, attacker, target, weapon, settings)",
|
||||
"projectilePhysics.Step(zone, entities, deltaSeconds)",
|
||||
"Damage(target, hit.Projectile.Damage)"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Stat("Energy", 9),
|
||||
Stat("Heat", 10),
|
||||
Stat("Visibility", 11),
|
||||
Integer("MagazineSize", 13),
|
||||
Number("ReloadTime", 14),
|
||||
Stat("Spread", 15),
|
||||
Stat("Velocity", 16)
|
||||
};
|
||||
|
|
@ -154,6 +156,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Number);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Integer(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Integer);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeBehaviorFieldMetadata Temperature(string name, int key)
|
||||
{
|
||||
return new AetheriaRuntimeBehaviorFieldMetadata(name, key, AetheriaRuntimeBehaviorFieldValueKind.Temperature);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeCargoTransactions
|
||||
{
|
||||
public static bool TryRemoveQuantity(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
int cargoIndex,
|
||||
string itemKey,
|
||||
int x,
|
||||
int y,
|
||||
int quantity,
|
||||
out AetheriaRuntimeLoadoutItemSlotCommit slot)
|
||||
{
|
||||
slot = null!;
|
||||
var cargoContents = (entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToArray();
|
||||
if (cargoIndex < 0 || cargoIndex >= cargoContents.Length || cargoContents[cargoIndex] == null)
|
||||
return false;
|
||||
var items = (cargoContents[cargoIndex].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
|
||||
var itemIndex = items.FindIndex(candidate => candidate?.Item != null &&
|
||||
string.Equals(candidate.Item.ItemKey, itemKey, StringComparison.Ordinal) &&
|
||||
candidate.X == x && candidate.Y == y);
|
||||
if (itemIndex < 0) return false;
|
||||
var source = items[itemIndex];
|
||||
var sourceQuantity = Math.Max(1, source.Item.Quantity);
|
||||
if (quantity <= 0 || quantity > sourceQuantity) return false;
|
||||
slot = Clone(source, quantity);
|
||||
if (quantity == sourceQuantity) items.RemoveAt(itemIndex);
|
||||
else source.Item.Quantity = sourceQuantity - quantity;
|
||||
cargoContents[cargoIndex] = new AetheriaRuntimeCargoBayLoadoutCommit { Items = items.ToArray() };
|
||||
entity!.CargoContents = cargoContents;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryFind(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
string itemKey,
|
||||
out int cargoIndex,
|
||||
out int x,
|
||||
out int y)
|
||||
{
|
||||
var cargo = entity?.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
||||
for (var bay = 0; bay < cargo.Count; bay++)
|
||||
foreach (var slot in cargo[bay]?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
{
|
||||
if (slot?.Item == null || !string.Equals(slot.Item.ItemKey, itemKey, StringComparison.Ordinal)) continue;
|
||||
cargoIndex = bay; x = slot.X; y = slot.Y; return true;
|
||||
}
|
||||
cargoIndex = x = y = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeLoadoutItemSlotCommit Clone(AetheriaRuntimeLoadoutItemSlotCommit source, int quantity) => new()
|
||||
{
|
||||
X = source.X, Y = source.Y,
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = source.Item.ItemKey, Quality = source.Item.Quality, Durability = source.Item.Durability,
|
||||
Quantity = quantity, Enabled = source.Item.Enabled, OverrideShutdown = source.Item.OverrideShutdown,
|
||||
Temperature = source.Item.Temperature
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1858,38 +1858,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
int quantity,
|
||||
out AetheriaRuntimeLoadoutItemSlotCommit slot)
|
||||
{
|
||||
slot = null!;
|
||||
var cargoContents = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToArray();
|
||||
if (cargoIndex < 0 || cargoIndex >= cargoContents.Length || cargoContents[cargoIndex] == null)
|
||||
return false;
|
||||
|
||||
var items = (cargoContents[cargoIndex].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
|
||||
var itemIndex = items.FindIndex(candidate => IsCargoSlotMatch(candidate, itemKey, x, y));
|
||||
if (itemIndex < 0)
|
||||
return false;
|
||||
|
||||
var source = items[itemIndex];
|
||||
var sourceQuantity = Math.Max(1, source.Item?.Quantity ?? 1);
|
||||
if (quantity <= 0 || quantity > sourceQuantity)
|
||||
return false;
|
||||
|
||||
slot = CloneSlot(source);
|
||||
slot.Item.Quantity = quantity;
|
||||
if (quantity == sourceQuantity)
|
||||
{
|
||||
items.RemoveAt(itemIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
source.Item.Quantity = sourceQuantity - quantity;
|
||||
}
|
||||
|
||||
cargoContents[cargoIndex] = new AetheriaRuntimeCargoBayLoadoutCommit
|
||||
{
|
||||
Items = items.ToArray()
|
||||
};
|
||||
entity.CargoContents = cargoContents;
|
||||
return true;
|
||||
return AetheriaRuntimeCargoTransactions.TryRemoveQuantity(
|
||||
entity, cargoIndex, itemKey, x, y, quantity, out slot);
|
||||
}
|
||||
|
||||
private static void AddCargoItem(
|
||||
|
|
|
|||
|
|
@ -244,6 +244,15 @@ namespace GameCult.Aetheria.State.Verse
|
|||
foreach (var weapon in weapons)
|
||||
{
|
||||
weapon.State.Firing = false;
|
||||
if (weapon.State.Reloading)
|
||||
{
|
||||
weapon.State.ReloadProgress = Math.Max(0, weapon.State.ReloadProgress - deltaSeconds);
|
||||
if (weapon.State.ReloadProgress <= 0)
|
||||
{
|
||||
weapon.State.Reloading = false;
|
||||
weapon.State.Ammo = weapon.MagazineSize;
|
||||
}
|
||||
}
|
||||
weapon.State.CooldownProgress = Math.Max(0, weapon.State.CooldownProgress - deltaSeconds);
|
||||
weapon.State.CoolingDown = weapon.State.CooldownProgress > 0;
|
||||
}
|
||||
|
|
@ -270,9 +279,12 @@ 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.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;
|
||||
|
|
@ -448,7 +460,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
{
|
||||
var state = EnsureWeaponState(entity, AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
behavior.EquipmentIndex, behavior.BehaviorIndex, behavior.Payload.Kind, settings);
|
||||
behavior.EquipmentIndex, behavior.BehaviorIndex, behavior.Payload.Kind,
|
||||
Math.Max(0, (int)Math.Round(ReadNumber(behavior.Payload, 13))), settings);
|
||||
return new ResolvedWeapon(
|
||||
state,
|
||||
behavior.Item.ItemKey,
|
||||
|
|
@ -457,6 +470,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
PositiveOr(behavior.EvaluateStat(19), settings.WeaponCooldownSeconds),
|
||||
PositiveOr(behavior.EvaluateStat(16), settings.ProjectileSpeed),
|
||||
Math.Max(0, behavior.EvaluateStat(10)),
|
||||
Math.Max(0, behavior.EvaluateStat(9)),
|
||||
ReadItemKey(behavior.Payload, 12),
|
||||
Math.Max(0, (int)Math.Round(ReadNumber(behavior.Payload, 13))),
|
||||
PositiveOr(ReadNumber(behavior.Payload, 14), settings.WeaponCooldownSeconds),
|
||||
PositiveOr(behavior.EvaluateStat(21), settings.WeaponLockSpeed),
|
||||
Math.Max(0, behavior.EvaluateStat(22)),
|
||||
PositiveOr(behavior.EvaluateStat(23), settings.WeaponLockAngleDegrees),
|
||||
|
|
@ -468,11 +485,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
{
|
||||
var state = EnsureWeaponState(entity, "daemon-simulation", entity.EntityIndex, 0, "ProjectileWeapon", settings);
|
||||
var state = EnsureWeaponState(entity, "daemon-simulation", entity.EntityIndex, 0, "ProjectileWeapon", -1, settings);
|
||||
return new ResolvedWeapon(state,
|
||||
IsPlayerOwned(entity) ? "vanguard-bolt" : "raider-bolt",
|
||||
ResolveProjectileDamage(entity, settings), settings.AttackRange, settings.WeaponCooldownSeconds,
|
||||
settings.ProjectileSpeed, ResolveProjectileDamage(entity, settings) * settings.ProjectileHeatScale,
|
||||
0, "", -1, settings.WeaponCooldownSeconds,
|
||||
settings.WeaponLockSpeed, settings.WeaponLockSensorImpact, settings.WeaponLockAngleDegrees,
|
||||
settings.WeaponLockDirectionImpact, settings.WeaponLockDecayPerSecond);
|
||||
}
|
||||
|
|
@ -483,6 +501,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
int ownerIndex,
|
||||
int behaviorIndex,
|
||||
string behaviorKind,
|
||||
int initialAmmo,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
{
|
||||
var states = (entity.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>()).ToList();
|
||||
|
|
@ -500,7 +519,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
OwnerIndex = ownerIndex,
|
||||
BehaviorIndex = behaviorIndex,
|
||||
BehaviorKind = behaviorKind,
|
||||
Ammo = -1,
|
||||
Ammo = initialAmmo,
|
||||
BurstRemaining = 0,
|
||||
BurstInterval = settings.WeaponCooldownSeconds,
|
||||
LockTargetEntityIndex = -1
|
||||
|
|
@ -571,14 +590,92 @@ namespace GameCult.Aetheria.State.Verse
|
|||
private static double PositiveOr(double value, double fallback) =>
|
||||
double.IsFinite(value) && value > 0 ? value : fallback;
|
||||
|
||||
private static string ReadItemKey(AetheriaRuntimeBehaviorPayload payload, int key)
|
||||
{
|
||||
var value = (payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value;
|
||||
return value?.ItemKeyValue ?? value?.StringValue ?? "";
|
||||
}
|
||||
|
||||
private static double ReadNumber(AetheriaRuntimeBehaviorPayload payload, int key)
|
||||
{
|
||||
return (payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value?.NumberValue ?? 0;
|
||||
}
|
||||
|
||||
private static WeaponRoundResult CommitWeaponRound(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
ResolvedWeapon weapon)
|
||||
{
|
||||
if (weapon.MagazineSize > 1 && weapon.State.Ammo <= 0)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(weapon.AmmoItemKey))
|
||||
{
|
||||
if (!AetheriaRuntimeCargoTransactions.TryFind(entity, weapon.AmmoItemKey,
|
||||
out var cargoIndex, out var x, out var y) ||
|
||||
!AetheriaRuntimeCargoTransactions.TryRemoveQuantity(
|
||||
entity, cargoIndex, weapon.AmmoItemKey, x, y, 1, out _))
|
||||
return WeaponRoundResult.NoAmmo;
|
||||
}
|
||||
weapon.State.Reloading = true;
|
||||
weapon.State.ReloadProgress = weapon.ReloadTime;
|
||||
return WeaponRoundResult.ReloadStarted;
|
||||
}
|
||||
|
||||
if (!CanSupplyEnergy(entity, weapon.Energy))
|
||||
return WeaponRoundResult.InsufficientEnergy;
|
||||
CommitEnergy(entity, weapon.Energy);
|
||||
if (weapon.MagazineSize > 1)
|
||||
weapon.State.Ammo--;
|
||||
return WeaponRoundResult.Fired;
|
||||
}
|
||||
|
||||
private static bool CanSupplyEnergy(AetheriaRuntimeEntitySnapshotCommit entity, double demand)
|
||||
{
|
||||
if (demand < 0.01) return true;
|
||||
var states = entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
||||
var charge = states.Where(value => value != null && value.BehaviorKind == "Capacitor")
|
||||
.Sum(value => Math.Max(0, value.CapacitorCharge));
|
||||
return charge >= demand || states.Any(value => value != null && value.BehaviorKind == "Reactor");
|
||||
}
|
||||
|
||||
private static void CommitEnergy(AetheriaRuntimeEntitySnapshotCommit entity, double demand)
|
||||
{
|
||||
if (demand < 0.01) return;
|
||||
var capacitors = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && value.BehaviorKind == "Capacitor" && value.CapacitorCharge > 0)
|
||||
.ToList();
|
||||
var remaining = demand;
|
||||
while (remaining > 0.000001 && capacitors.Count > 0)
|
||||
{
|
||||
var share = remaining / capacitors.Count;
|
||||
foreach (var capacitor in capacitors.ToArray())
|
||||
{
|
||||
var drained = Math.Min(share, capacitor.CapacitorCharge);
|
||||
capacitor.CapacitorCharge -= drained;
|
||||
remaining -= drained;
|
||||
if (capacitor.CapacitorCharge <= 0.000001) capacitors.Remove(capacitor);
|
||||
}
|
||||
}
|
||||
var reactors = (entity.BehaviorStates ?? Array.Empty<AetheriaRuntimeBehaviorStateCommit>())
|
||||
.Where(value => value != null && value.BehaviorKind == "Reactor")
|
||||
.ToArray();
|
||||
if (remaining > 0.000001 && reactors.Length > 0)
|
||||
foreach (var reactor in reactors) reactor.ReactorDraw += remaining / reactors.Length;
|
||||
}
|
||||
|
||||
private enum WeaponRoundResult { Fired, ReloadStarted, InsufficientEnergy, NoAmmo }
|
||||
|
||||
private sealed class ResolvedWeapon
|
||||
{
|
||||
public ResolvedWeapon(AetheriaRuntimeWeaponStateCommit state, string itemKey, double damage, double range,
|
||||
double cooldown, double projectileSpeed, double heat, double lockSpeed, double lockSensorImpact,
|
||||
double cooldown, double projectileSpeed, double heat, double energy, string ammoItemKey,
|
||||
int magazineSize, double reloadTime, double lockSpeed, double lockSensorImpact,
|
||||
double lockAngleDegrees, double lockDirectionImpact, double lockDecayPerSecond)
|
||||
{
|
||||
State = state; ItemKey = itemKey; Damage = damage; Range = range; Cooldown = cooldown;
|
||||
ProjectileSpeed = projectileSpeed; Heat = heat; LockSpeed = lockSpeed;
|
||||
ProjectileSpeed = projectileSpeed; Heat = heat; Energy = energy; AmmoItemKey = ammoItemKey;
|
||||
MagazineSize = magazineSize; ReloadTime = reloadTime; LockSpeed = lockSpeed;
|
||||
LockSensorImpact = lockSensorImpact; LockAngleDegrees = lockAngleDegrees;
|
||||
LockDirectionImpact = lockDirectionImpact; LockDecayPerSecond = lockDecayPerSecond;
|
||||
}
|
||||
|
|
@ -590,6 +687,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double Cooldown { get; }
|
||||
public double ProjectileSpeed { get; }
|
||||
public double Heat { get; }
|
||||
public double Energy { get; }
|
||||
public string AmmoItemKey { get; }
|
||||
public int MagazineSize { get; }
|
||||
public double ReloadTime { 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 now resolve equipped typed-catalog behaviors for damage, range, cooldown, projectile velocity, heat and lock parameters; temporal state belongs to the equipped behavior; Ymir owns flight/contact and daemon owns damage; catalog-free fixtures retain an explicit synthetic fallback | energy/ammunition/burst/resource transactions 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 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 |
|
||||
| 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