Run sensor visibility mechanics in daemon
This commit is contained in:
parent
d2926cc782
commit
0724ee3cf8
14 changed files with 561 additions and 105 deletions
|
|
@ -175,6 +175,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
DockedRefitSurfacePublishesGenericCommands,
|
||||
FossilVelocityBehaviorsRunInDaemonFlightStep,
|
||||
ReflectorUsesSharedStellarLightFieldWithoutAccumulating,
|
||||
SensorPingIsActorScopedEnergyGatedAndReconnectable,
|
||||
TractorRampsAndPullsThroughYmirWithoutTeleportingCargo,
|
||||
PickupIsCapacityCheckedExactlyOnceAndExpires,
|
||||
TradePurchaseDerivesAcceptanceFromDaemonState,
|
||||
|
|
@ -461,6 +462,88 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
"Reflector visibility must remain an inspectable daemon-owned source for Eve and reconnect");
|
||||
}
|
||||
|
||||
private static void SensorPingIsActorScopedEnergyGatedAndReconnectable()
|
||||
{
|
||||
var sensorItem = CatalogItem("sensor-ping-array", SensorPayload(
|
||||
sensitivity: 0.01,
|
||||
pingBoost: 1,
|
||||
pingEnergy: 5,
|
||||
pingVisibility: 4,
|
||||
pingRange: 200,
|
||||
pingCooldown: 1,
|
||||
pingDuration: 1,
|
||||
pingRadiusExponent: 1));
|
||||
var capacitorItem = CatalogItem("sensor-ping-capacitor", CapacitorPayload(10, 1));
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot([sensorItem, capacitorItem], [], []);
|
||||
|
||||
static AetheriaRuntimeEntitySnapshotCommit Observer(int index, string sensorKey, string capacitorKey)
|
||||
{
|
||||
var entity = Entity(index, 0, "player");
|
||||
entity.Equipment =
|
||||
[
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = sensorKey, Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = capacitorKey, Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
entity.BehaviorStates =
|
||||
[
|
||||
new AetheriaRuntimeBehaviorStateCommit
|
||||
{
|
||||
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
||||
OwnerIndex = 1,
|
||||
BehaviorIndex = 0,
|
||||
BehaviorKind = "Capacitor",
|
||||
CapacitorCharge = 10,
|
||||
CapacitorCapacity = 10,
|
||||
CapacitorEfficiency = 1
|
||||
}
|
||||
];
|
||||
return entity;
|
||||
}
|
||||
|
||||
var actor = Observer(0, sensorItem.ItemKey, capacitorItem.ItemKey);
|
||||
var other = Observer(1, sensorItem.ItemKey, capacitorItem.ItemKey);
|
||||
var target = Entity(2, 40, "raider");
|
||||
target.Visibility = 1;
|
||||
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [actor, other, target] };
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "sensor-ping-smoke",
|
||||
Zones = [zone]
|
||||
};
|
||||
|
||||
AetheriaRuntimeSensorSimulation.StepZone(
|
||||
run, zone, [actor, other, target], [], catalog, 0.25, 0.5, 0.1, 1);
|
||||
AetheriaRuntimeSensorSimulation.StepZone(
|
||||
run,
|
||||
zone,
|
||||
[actor, other, target],
|
||||
[new AetheriaRuntimeDaemonSensorPingIntent { ActorEntityKey = "zone.0.entity.0" }],
|
||||
catalog,
|
||||
0.25,
|
||||
0.5,
|
||||
0.1,
|
||||
2);
|
||||
|
||||
var sensor = actor.BehaviorStates.Single(state => state.BehaviorKind == AetheriaRuntimeBehaviorKinds.Sensor);
|
||||
var otherSensor = other.BehaviorStates.Single(state => state.BehaviorKind == AetheriaRuntimeBehaviorKinds.Sensor);
|
||||
Require(sensor.Pinging && !otherSensor.Pinging,
|
||||
"sensor ping intent must retain actor identity instead of activating every sensor");
|
||||
RequireEqual(2, sensor.PingedEntityCount,
|
||||
"expanding ping must persist every crossed entity identity for reconnect-safe exactly-once boost");
|
||||
Require(sensor.PingedEntityIndices.SequenceEqual(new[] { 1, 2 }),
|
||||
"pinged entity identities must survive in stable order rather than only an unverifiable count");
|
||||
RequireNear(5, actor.BehaviorStates.Single(state => state.BehaviorKind == "Capacitor").CapacitorCharge,
|
||||
0.000001, "accepted ping must consume its authored energy exactly once");
|
||||
Require(actor.Contacts.Single(contact => contact.TargetEntityIndex == 2).Visible,
|
||||
"ping boost must cross the daemon-owned detection threshold");
|
||||
RequireNear(4, actor.StatGrids.Single(grid => grid.Name == "transient-visibility:sensor:0:0").Values.Single(),
|
||||
0.000001, "ping must publish its authored transient visibility source");
|
||||
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "sensor.ping.started"),
|
||||
"accepted ping must emit one provider-owned start event");
|
||||
}
|
||||
|
||||
private static void DockingUsesRealBaysAndFossilUndockRules()
|
||||
{
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
|
|
@ -1612,6 +1695,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
[
|
||||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(20)),
|
||||
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
||||
new AetheriaRuntimeBehaviorField(11, PerformanceStat(8)),
|
||||
new AetheriaRuntimeBehaviorField(15, PerformanceStat(0)),
|
||||
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
||||
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
||||
|
|
@ -1651,6 +1735,13 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.InstantWeapon);
|
||||
Require(!state.TriggerPending,
|
||||
"instant weapon request must clear after the committed burst begins");
|
||||
var weaponVisibility = source.StatGrids.Single(grid =>
|
||||
grid.Name == "transient-visibility:weapon:equipment:0:0");
|
||||
RequireNear(8, weaponVisibility.Values.Single(), 0.000001,
|
||||
"committed instant shot must publish its authored visibility as a named transient source");
|
||||
AetheriaRuntimeVisibilitySimulation.BeginTick([source], 1, 0.5);
|
||||
RequireNear(8 * Math.Exp(-0.5), weaponVisibility.Values.Single(), 0.000001,
|
||||
"weapon visibility must use the fossil exponential source decay rather than vanish with the effect");
|
||||
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
||||
new AetheriaRuntimeDaemonFrameDocument { FrameId = 12, Run = run },
|
||||
new AetheriaRuntimeDaemonHealthDocument(),
|
||||
|
|
@ -3010,6 +3101,30 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
new AetheriaRuntimeBehaviorField(2, PerformanceStat(efficiency))
|
||||
]);
|
||||
|
||||
private static AetheriaRuntimeBehaviorPayload SensorPayload(
|
||||
double sensitivity = 1,
|
||||
double pingBoost = 1,
|
||||
double pingEnergy = 0,
|
||||
double pingVisibility = 3,
|
||||
double pingRange = 200,
|
||||
double pingCooldown = 1,
|
||||
double pingDuration = 2,
|
||||
double pingRadiusExponent = 0.5) => new(
|
||||
0, AetheriaRuntimeBehaviorKinds.Sensor, 0,
|
||||
[
|
||||
new AetheriaRuntimeBehaviorField(3, PerformanceStat(sensitivity)),
|
||||
new AetheriaRuntimeBehaviorField(4, Curve(
|
||||
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
||||
new AetheriaRuntimeCurveKey(1, 1, 0, 0))),
|
||||
new AetheriaRuntimeBehaviorField(5, PerformanceStat(pingBoost)),
|
||||
new AetheriaRuntimeBehaviorField(6, PerformanceStat(pingEnergy)),
|
||||
new AetheriaRuntimeBehaviorField(7, PerformanceStat(pingVisibility)),
|
||||
new AetheriaRuntimeBehaviorField(8, PerformanceStat(pingRange)),
|
||||
new AetheriaRuntimeBehaviorField(9, PerformanceStat(pingCooldown)),
|
||||
new AetheriaRuntimeBehaviorField(10, Number(pingDuration)),
|
||||
new AetheriaRuntimeBehaviorField(11, Number(pingRadiusExponent))
|
||||
]);
|
||||
|
||||
private static AetheriaRuntimeCatalogItem CatalogItem(string itemKey, params AetheriaRuntimeBehaviorPayload[] payloads) => new(
|
||||
itemKey, itemKey, "equipment", "", "", 0, 1, 1, 1, 1,
|
||||
1, 1, 1, Array.Empty<AetheriaRuntimeShapeCell>(),
|
||||
|
|
@ -3297,7 +3412,9 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
}
|
||||
},
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-attack-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
{ ItemKey = "test-attack-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{ ItemKey = "test-attack-sensor", Quality = 1, Durability = 1, Enabled = true } }
|
||||
];
|
||||
agent.BehaviorStates =
|
||||
[
|
||||
|
|
@ -3338,9 +3455,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
var catalog = EquipThrusterBank(
|
||||
[agent],
|
||||
CatalogItem("test-lock-cannon", weaponPayload),
|
||||
CatalogItem("test-attack-capacitor", CapacitorPayload(50, 1)));
|
||||
CatalogItem("test-attack-capacitor", CapacitorPayload(50, 1)),
|
||||
CatalogItem("test-attack-sensor", SensorPayload()));
|
||||
var target = Entity(1, 105, "raider");
|
||||
target.Kind = "station";
|
||||
target.Visibility = 10;
|
||||
target.StatGrids = [Grid("hull", 14), Grid("shield", 0), Grid("heat", 0)];
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5182,7 +5182,7 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.IsTrue(result.Intents.Behaviors[0].Active);
|
||||
Assert.AreEqual(1, result.Intents.Consumables.Count);
|
||||
Assert.AreEqual("repair-gel", result.Intents.Consumables[0].ItemKey);
|
||||
Assert.IsTrue(result.Intents.SensorPingRequested);
|
||||
Assert.AreEqual("zone.0.entity.0", result.Intents.SensorPings.Single().ActorEntityKey);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public const string VelocityLimit = "VelocityLimit";
|
||||
public const string Reflector = "Reflector";
|
||||
public const string Visibility = "Visibility";
|
||||
public const string Sensor = "Sensor";
|
||||
|
||||
private static readonly ISet<string> WeaponKinds = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ namespace GameCult.Aetheria.State.Verse
|
|||
new List<AetheriaRuntimeDaemonDockingIntent>();
|
||||
public List<AetheriaRuntimeDaemonWormholeIntent> Wormholes { get; } =
|
||||
new List<AetheriaRuntimeDaemonWormholeIntent>();
|
||||
public bool SensorPingRequested { get; set; }
|
||||
public List<AetheriaRuntimeDaemonSensorPingIntent> SensorPings { get; } =
|
||||
new List<AetheriaRuntimeDaemonSensorPingIntent>();
|
||||
|
||||
public bool HasAny =>
|
||||
Movements.Count > 0 ||
|
||||
SensorPingRequested ||
|
||||
SensorPings.Count > 0 ||
|
||||
WeaponGroups.Count > 0 ||
|
||||
Behaviors.Count > 0 ||
|
||||
Consumables.Count > 0 ||
|
||||
|
|
@ -47,6 +48,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public bool Active { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonSensorPingIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonBehaviorIntent
|
||||
{
|
||||
public string ActorEntityKey { get; set; } = "";
|
||||
|
|
|
|||
|
|
@ -1484,10 +1484,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonCommandDocument command,
|
||||
AetheriaRuntimeDaemonIntentState intents)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ResolveActorEntityKey(run, command)))
|
||||
var actor = ResolveActorEntityKey(run, command);
|
||||
if (string.IsNullOrWhiteSpace(actor))
|
||||
return false;
|
||||
|
||||
intents.SensorPingRequested = true;
|
||||
intents.SensorPings.Add(new AetheriaRuntimeDaemonSensorPingIntent
|
||||
{
|
||||
ActorEntityKey = actor
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
StepPickupLifetimes(run, zone, frameId, deltaSeconds);
|
||||
|
||||
EnsureStats(entities, settings, catalog);
|
||||
AetheriaRuntimeVisibilitySimulation.BeginTick(
|
||||
entities, deltaSeconds, settings.VisibilityDecay);
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
||||
|
|
@ -92,9 +94,19 @@ namespace GameCult.Aetheria.State.Verse
|
|||
StepCombat(run, zone, entities, intents, deltaSeconds, settings, worldPhysics, catalog,
|
||||
frameId, simulationTimeSeconds, simulationStepIndex);
|
||||
AetheriaRuntimeVisibilitySimulation.StepZone(zone, entities, catalog, renderSettings);
|
||||
var activeRenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
|
||||
AetheriaRuntimeSensorSimulation.StepZone(
|
||||
run,
|
||||
zone,
|
||||
entities,
|
||||
intents?.SensorPings,
|
||||
catalog,
|
||||
deltaSeconds,
|
||||
settings.TargetInfoDecay,
|
||||
activeRenderSettings.TargetDetectionInfoThreshold,
|
||||
frameId);
|
||||
AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
|
||||
AetheriaRuntimeSurveySimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
|
||||
RefreshContacts(entities, settings, catalog);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -700,6 +712,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
weapon.State.Firing = true;
|
||||
weapon.State.CoolingDown = true;
|
||||
weapon.State.CooldownProgress = weapon.Cooldown;
|
||||
PublishWeaponVisibility(attacker, weapon);
|
||||
AetheriaRuntimeThermalSimulation.AddHeatToEquipment(attacker, catalog,
|
||||
weapon.State.OwnerIndex, weapon.Heat);
|
||||
ApplyWeaponWear(attacker, weapon.State, 1);
|
||||
|
|
@ -1438,6 +1451,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
PositiveOr(behavior.EvaluateStat(6), 1),
|
||||
Math.Max(0, behavior.EvaluateStat(9)),
|
||||
Math.Max(0, behavior.EvaluateStat(10)),
|
||||
Math.Max(0, behavior.EvaluateStat(11)),
|
||||
ReadItemKey(behavior.Payload, 12), magazineSize,
|
||||
PositiveOr(ReadNumber(behavior.Payload, 14), 1),
|
||||
PositiveOr(ReadNumber(behavior.Payload, 17), 1),
|
||||
|
|
@ -1483,6 +1497,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.Any(value => value != null && string.Equals(value.ShotId, shotId, StringComparison.Ordinal)))
|
||||
return;
|
||||
|
||||
PublishWeaponVisibility(attacker, weapon);
|
||||
|
||||
var contact = (attacker.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.FirstOrDefault(value => value != null && value.TargetEntityIndex == target.EntityIndex);
|
||||
var information = Clamp01(contact?.InfoGathered ?? 0);
|
||||
|
|
@ -1583,6 +1599,18 @@ namespace GameCult.Aetheria.State.Verse
|
|||
frameId, settings, damage.CockpitDestroyed ? "cockpit-destroyed" : "hull-destroyed");
|
||||
}
|
||||
|
||||
private static void PublishWeaponVisibility(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
ResolvedWeapon weapon)
|
||||
{
|
||||
if (weapon.Visibility <= 0)
|
||||
return;
|
||||
AetheriaRuntimeVisibilitySimulation.SetTransientSource(
|
||||
entity,
|
||||
$"weapon:{weapon.State.OwnerKind}:{weapon.State.OwnerIndex}:{weapon.State.BehaviorIndex}",
|
||||
weapon.Visibility);
|
||||
}
|
||||
|
||||
private static void PublishEquipmentDestroyedEvents(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
|
|
@ -1801,7 +1829,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Math.Max(0, value.EvaluateStat(22)), Math.Max(0, value.EvaluateStat(23)),
|
||||
ReadNumber(value.Payload, 25), PositiveOr(ReadNumber(value.Payload, 26), 1),
|
||||
PositiveOr(ReadNumber(value.Payload, 27), 1), PositiveOr(ReadNumber(value.Payload, 29), 1),
|
||||
PositiveOr(ReadNumber(value.Payload, 31), 1), PositiveOr(ReadNumber(value.Payload, 32), 1)))
|
||||
PositiveOr(ReadNumber(value.Payload, 30), 1), PositiveOr(ReadNumber(value.Payload, 31), 1),
|
||||
PositiveOr(ReadNumber(value.Payload, 32), 1)))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
|
@ -1850,6 +1879,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
PositiveOr(behavior.EvaluateStat(23), settings.WeaponLockAngleDegrees),
|
||||
PositiveOr(behavior.EvaluateStat(24), settings.WeaponLockDirectionImpact),
|
||||
Math.Max(0, behavior.EvaluateStat(25)),
|
||||
Math.Max(0, behavior.EvaluateStat(11)),
|
||||
Math.Max(0, behavior.EvaluateStat(5)),
|
||||
Math.Max(0, behavior.EvaluateStat(15)),
|
||||
ReadEnumName(behavior.Payload, 1, "Kinetic"),
|
||||
|
|
@ -1870,7 +1900,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
1, 0, false,
|
||||
settings.WeaponLockSpeed, settings.WeaponLockSensorImpact, settings.WeaponLockAngleDegrees,
|
||||
settings.WeaponLockDirectionImpact, settings.WeaponLockDecayPerSecond,
|
||||
0, 0, "Kinetic", 0, 0);
|
||||
0, 0, 0, "Kinetic", 0, 0);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeWeaponStateCommit EnsureWeaponState(
|
||||
|
|
@ -2011,7 +2041,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
int magazineSize, double reloadTime, int burstCount, double burstTime, bool singleAmmoBurst,
|
||||
double lockSpeed, double lockSensorImpact,
|
||||
double lockAngleDegrees, double lockDirectionImpact, double lockDecayPerSecond,
|
||||
double minRange = 0, double spread = 0, string damageType = "Kinetic",
|
||||
double visibility = 0, double minRange = 0, double spread = 0, string damageType = "Kinetic",
|
||||
double penetration = 0, double damageSpread = 0)
|
||||
{
|
||||
State = state; ItemKey = itemKey; Damage = damage; Range = range; Cooldown = cooldown;
|
||||
|
|
@ -2020,7 +2050,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
BurstCount = burstCount; BurstTime = burstTime; SingleAmmoBurst = singleAmmoBurst;
|
||||
LockSensorImpact = lockSensorImpact; LockAngleDegrees = lockAngleDegrees;
|
||||
LockDirectionImpact = lockDirectionImpact; LockDecayPerSecond = lockDecayPerSecond;
|
||||
MinRange = minRange; Spread = spread;
|
||||
Visibility = visibility; MinRange = minRange; Spread = spread;
|
||||
DamageType = damageType; Penetration = penetration; DamageSpread = damageSpread;
|
||||
}
|
||||
|
||||
|
|
@ -2043,6 +2073,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double LockAngleDegrees { get; }
|
||||
public double LockDirectionImpact { get; }
|
||||
public double LockDecayPerSecond { get; }
|
||||
public double Visibility { get; }
|
||||
public double MinRange { get; }
|
||||
public double Spread { get; }
|
||||
public string DamageType { get; }
|
||||
|
|
@ -2053,19 +2084,19 @@ namespace GameCult.Aetheria.State.Verse
|
|||
private sealed class ResolvedConstantWeapon
|
||||
{
|
||||
public ResolvedConstantWeapon(AetheriaRuntimeWeaponStateCommit state, string itemKey,
|
||||
bool selected, double damage, double range, double energy, double heat, string ammoItemKey,
|
||||
bool selected, double damage, double range, double energy, double heat, double visibility, string ammoItemKey,
|
||||
int magazineSize, double reloadTime, double ammoIntervalDuration,
|
||||
double minRange, double spread)
|
||||
{
|
||||
State = state; ItemKey = itemKey; Selected = selected; Damage = damage; Range = range;
|
||||
Energy = energy; Heat = heat; AmmoItemKey = ammoItemKey;
|
||||
Energy = energy; Heat = heat; Visibility = visibility; AmmoItemKey = ammoItemKey;
|
||||
MagazineSize = magazineSize; ReloadTime = reloadTime;
|
||||
AmmoIntervalDuration = ammoIntervalDuration; MinRange = minRange; Spread = spread;
|
||||
}
|
||||
|
||||
public ResolvedWeapon ResolutionShot(double deltaSeconds) => new ResolvedWeapon(
|
||||
State, ItemKey, Damage * deltaSeconds, Range, 0, 1, 0, 0, "", 0, 0,
|
||||
1, 0, false, 1, 0, 180, 0, 0, MinRange, Spread, "Kinetic", 0, 0);
|
||||
1, 0, false, 1, 0, 180, 0, 0, Visibility, MinRange, Spread, "Kinetic", 0, 0);
|
||||
|
||||
public AetheriaRuntimeWeaponStateCommit State { get; }
|
||||
public string ItemKey { get; }
|
||||
|
|
@ -2074,6 +2105,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double Range { get; }
|
||||
public double Energy { get; }
|
||||
public double Heat { get; }
|
||||
public double Visibility { get; }
|
||||
public string AmmoItemKey { get; }
|
||||
public int MagazineSize { get; }
|
||||
public double ReloadTime { get; }
|
||||
|
|
@ -2108,12 +2140,13 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public ResolvedChargedWeapon(ResolvedWeapon baseWeapon, AetheriaRuntimeLoadoutItemCommit item,
|
||||
bool requested, double chargeTime, double chargeEnergy, double chargeHeat,
|
||||
double failureCharge, double failureDamage, double damageMultiplier,
|
||||
double burstMultiplier, double velocityMultiplier, double heatMultiplier)
|
||||
double burstMultiplier, double visibilityMultiplier, double velocityMultiplier, double heatMultiplier)
|
||||
{
|
||||
Base = baseWeapon; Item = item; Requested = requested; ChargeTime = chargeTime;
|
||||
ChargeEnergy = chargeEnergy; ChargeHeat = chargeHeat; FailureCharge = failureCharge;
|
||||
FailureDamage = failureDamage; DamageMultiplier = damageMultiplier;
|
||||
BurstMultiplier = burstMultiplier; VelocityMultiplier = velocityMultiplier;
|
||||
BurstMultiplier = burstMultiplier; VisibilityMultiplier = visibilityMultiplier;
|
||||
VelocityMultiplier = velocityMultiplier;
|
||||
HeatMultiplier = heatMultiplier;
|
||||
}
|
||||
public ResolvedWeapon CommittedShot()
|
||||
|
|
@ -2125,6 +2158,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Base.Energy * Base.BurstCount / count, Base.AmmoItemKey, Base.MagazineSize, Base.ReloadTime,
|
||||
count, Base.BurstTime, Base.SingleAmmoBurst, Base.LockSpeed, Base.LockSensorImpact,
|
||||
Base.LockAngleDegrees, Base.LockDirectionImpact, Base.LockDecayPerSecond,
|
||||
Base.Visibility * VisibilityMultiplier,
|
||||
Base.MinRange, Base.Spread, Base.DamageType, Base.Penetration, Base.DamageSpread);
|
||||
}
|
||||
public ResolvedWeapon Base { get; }
|
||||
|
|
@ -2137,42 +2171,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double FailureDamage { get; }
|
||||
public double DamageMultiplier { get; }
|
||||
public double BurstMultiplier { get; }
|
||||
public double VisibilityMultiplier { get; }
|
||||
public double VelocityMultiplier { get; }
|
||||
public double HeatMultiplier { get; }
|
||||
}
|
||||
|
||||
private static void RefreshContacts(
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
foreach (var observer in entities)
|
||||
{
|
||||
var sensorRange = ResolveSensorRange(observer, settings, catalog);
|
||||
var contacts = new List<AetheriaRuntimeEntityContactCommit>();
|
||||
foreach (var target in entities)
|
||||
{
|
||||
if (target.EntityIndex == observer.EntityIndex)
|
||||
continue;
|
||||
|
||||
var distance = Math.Sqrt(DistanceSq(observer, target));
|
||||
var visible = distance <= sensorRange;
|
||||
if (!visible && !Hostile(observer, target))
|
||||
continue;
|
||||
|
||||
contacts.Add(new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = target.EntityIndex,
|
||||
InfoGathered = visible ? 1.0 : 0.25,
|
||||
Hostile = Hostile(observer, target),
|
||||
Visible = visible
|
||||
});
|
||||
}
|
||||
|
||||
observer.Contacts = contacts;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureStats(
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
|
|
@ -2509,30 +2512,6 @@ namespace GameCult.Aetheria.State.Verse
|
|||
string.Equals(right.FactionKey, "raider", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private static double ResolveSensorRange(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (!entity.IsActive)
|
||||
return 0;
|
||||
|
||||
var equipmentStates = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.Where(state => state != null)
|
||||
.ToDictionary(state => state.EquipmentIndex);
|
||||
var sensitivitySquared = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Sensor")
|
||||
.Select(sensor => Math.Max(0, sensor.EvaluateStat(
|
||||
3,
|
||||
equipmentStates.TryGetValue(sensor.EquipmentIndex, out var state)
|
||||
? state.ThermalPerformance
|
||||
: 1)))
|
||||
.Select(sensitivity => sensitivity * sensitivity)
|
||||
.Sum();
|
||||
return sensitivitySquared > 0
|
||||
? settings.EntitySensorRange * Math.Sqrt(sensitivitySquared)
|
||||
: settings.EntitySensorRange;
|
||||
}
|
||||
|
||||
private static double DefaultHull(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
|
|
|
|||
|
|
@ -108,7 +108,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
double wormholeExitCurveStart = AetheriaDefaultRaw.WormholeExitCurveStart,
|
||||
double wormholeExitVelocity = AetheriaDefaultRaw.WormholeExitVelocity,
|
||||
double wormholeExitRadius = AetheriaDefaultRaw.WormholeExitRadius,
|
||||
double wormholeDepth = AetheriaDefaultRaw.WormholeDepth)
|
||||
double wormholeDepth = AetheriaDefaultRaw.WormholeDepth,
|
||||
double visibilityDecay = AetheriaDefaultRaw.VisibilityDecay,
|
||||
double targetInfoDecay = AetheriaDefaultRaw.TargetInfoDecay)
|
||||
{
|
||||
PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed);
|
||||
RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed);
|
||||
|
|
@ -161,6 +163,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
WormholeExitVelocity = PositiveOr(wormholeExitVelocity, AetheriaDefaultRaw.WormholeExitVelocity);
|
||||
WormholeExitRadius = PositiveOr(wormholeExitRadius, AetheriaDefaultRaw.WormholeExitRadius);
|
||||
WormholeDepth = PositiveOr(wormholeDepth, AetheriaDefaultRaw.WormholeDepth);
|
||||
VisibilityDecay = NonNegativeOr(visibilityDecay, AetheriaDefaultRaw.VisibilityDecay);
|
||||
TargetInfoDecay = NonNegativeOr(targetInfoDecay, AetheriaDefaultRaw.TargetInfoDecay);
|
||||
}
|
||||
|
||||
public double PawnSpeed { get; }
|
||||
|
|
@ -216,6 +220,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double WormholeExitVelocity { get; }
|
||||
public double WormholeExitRadius { get; }
|
||||
public double WormholeDepth { get; }
|
||||
public double VisibilityDecay { get; }
|
||||
public double TargetInfoDecay { get; }
|
||||
|
||||
private static double PositiveOr(double value, double fallback)
|
||||
{
|
||||
|
|
@ -285,6 +291,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public const double WormholeExitVelocity = 20.0;
|
||||
public const double WormholeExitRadius = 50.0;
|
||||
public const double WormholeDepth = 1000.0;
|
||||
public const double VisibilityDecay = 0.5;
|
||||
public const double TargetInfoDecay = 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -339,10 +339,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonIntentState commands,
|
||||
AetheriaRuntimeDaemonIntentState agents)
|
||||
{
|
||||
var merged = new AetheriaRuntimeDaemonIntentState
|
||||
{
|
||||
SensorPingRequested = commands.SensorPingRequested || agents.SensorPingRequested
|
||||
};
|
||||
var merged = new AetheriaRuntimeDaemonIntentState();
|
||||
merged.Movements.AddRange(commands.Movements);
|
||||
merged.Movements.AddRange(agents.Movements);
|
||||
merged.WeaponGroups.AddRange(commands.WeaponGroups);
|
||||
|
|
@ -355,6 +352,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
merged.Docking.AddRange(agents.Docking);
|
||||
merged.Wormholes.AddRange(commands.Wormholes);
|
||||
merged.Wormholes.AddRange(agents.Wormholes);
|
||||
merged.SensorPings.AddRange(commands.SensorPings);
|
||||
merged.SensorPings.AddRange(agents.SensorPings);
|
||||
return merged;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,271 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeSensorSimulation
|
||||
{
|
||||
public static void StepZone(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IReadOnlyList<AetheriaRuntimeDaemonSensorPingIntent>? pingIntents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double deltaSeconds,
|
||||
double targetInfoDecay,
|
||||
double detectionThreshold,
|
||||
long frameId)
|
||||
{
|
||||
if (catalog == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
||||
foreach (var observer in entities)
|
||||
{
|
||||
if (observer == null || !observer.IsActive)
|
||||
continue;
|
||||
|
||||
var contacts = (observer.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.Where(contact => contact != null)
|
||||
.ToDictionary(contact => contact.TargetEntityIndex);
|
||||
var previouslyVisible = contacts.Values
|
||||
.Where(contact => contact.Visible)
|
||||
.Select(contact => contact.TargetEntityIndex)
|
||||
.ToHashSet();
|
||||
var sensors = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(
|
||||
observer, catalog, AetheriaRuntimeBehaviorKinds.Sensor);
|
||||
var pingRequested = (pingIntents ?? Array.Empty<AetheriaRuntimeDaemonSensorPingIntent>())
|
||||
.Any(intent => intent != null && ActorMatches(
|
||||
intent.ActorEntityKey, zone.ZoneIndex, observer.EntityIndex));
|
||||
if (pingRequested && sensors.Count > 0)
|
||||
TryStartPing(run, zone, observer, sensors[sensors.Count - 1], catalog, frameId);
|
||||
|
||||
foreach (var sensor in sensors)
|
||||
StepSensor(run, zone, observer, sensor, entities, contacts, catalog,
|
||||
deltaSeconds, targetInfoDecay, frameId);
|
||||
|
||||
observer.Contacts = entities
|
||||
.Where(target => target != null && target.EntityIndex != observer.EntityIndex)
|
||||
.Select(target =>
|
||||
{
|
||||
contacts.TryGetValue(target.EntityIndex, out var current);
|
||||
var information = Clamp01(current?.InfoGathered ?? 0);
|
||||
return new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = target.EntityIndex,
|
||||
InfoGathered = information,
|
||||
Hostile = Hostile(observer, target),
|
||||
Visible = information > detectionThreshold
|
||||
};
|
||||
})
|
||||
.OrderBy(contact => contact.TargetEntityIndex)
|
||||
.ToArray();
|
||||
if (observer.TargetEntityIndex >= 0 &&
|
||||
previouslyVisible.Contains(observer.TargetEntityIndex) &&
|
||||
!observer.Contacts.Any(contact =>
|
||||
contact.TargetEntityIndex == observer.TargetEntityIndex && contact.Visible))
|
||||
observer.TargetEntityIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryStartPing(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit observer,
|
||||
AetheriaRuntimeEquippedBehavior sensor,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
long frameId)
|
||||
{
|
||||
if (sensor.State.PingCooldown >= 0)
|
||||
return;
|
||||
var thermal = ThermalPerformance(observer, sensor.EquipmentIndex);
|
||||
if (!AetheriaRuntimeEnergySimulation.TryConsume(
|
||||
observer, catalog, Math.Max(0, sensor.EvaluateStat(6, thermal))))
|
||||
return;
|
||||
|
||||
sensor.State.Pinging = true;
|
||||
sensor.State.PingCooldown = 1;
|
||||
sensor.State.PingLerp = 0;
|
||||
sensor.State.PingRadius = 0;
|
||||
sensor.State.PingedEntityIndices = Array.Empty<int>();
|
||||
sensor.State.PingedEntityCount = 0;
|
||||
AetheriaRuntimeVisibilitySimulation.SetTransientSource(
|
||||
observer,
|
||||
$"sensor:{sensor.EquipmentIndex}:{sensor.BehaviorIndex}",
|
||||
sensor.EvaluateStat(7, thermal));
|
||||
AppendPingEvent(run, zone, observer, sensor, frameId, "sensor.ping.started");
|
||||
}
|
||||
|
||||
private static void StepSensor(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit observer,
|
||||
AetheriaRuntimeEquippedBehavior sensor,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IDictionary<int, AetheriaRuntimeEntityContactCommit> contacts,
|
||||
AetheriaRuntimeCatalogSnapshot catalog,
|
||||
double deltaSeconds,
|
||||
double targetInfoDecay,
|
||||
long frameId)
|
||||
{
|
||||
var thermal = ThermalPerformance(observer, sensor.EquipmentIndex);
|
||||
if (sensor.State.Pinging)
|
||||
{
|
||||
var duration = Math.Max(0.000001, ReadNumber(sensor, 10, 2));
|
||||
var exponent = ReadNumber(sensor, 11, 0.5);
|
||||
sensor.State.PingLerp += deltaSeconds / duration;
|
||||
sensor.State.PingRadius = Math.Max(0, sensor.EvaluateStat(8, thermal)) *
|
||||
Math.Pow(Math.Max(0, sensor.State.PingLerp), exponent);
|
||||
if (sensor.State.PingLerp > 1)
|
||||
{
|
||||
sensor.State.Pinging = false;
|
||||
AppendPingEvent(run, zone, observer, sensor, frameId, "sensor.ping.ended");
|
||||
}
|
||||
}
|
||||
|
||||
var cooldownDuration = Math.Max(0.000001, sensor.EvaluateStat(9, thermal));
|
||||
sensor.State.PingCooldown -= deltaSeconds / cooldownDuration;
|
||||
var pinged = new HashSet<int>(sensor.State.PingedEntityIndices ?? Array.Empty<int>());
|
||||
var forward = SensorDirection(observer, sensor);
|
||||
var sensitivity = Math.Max(0, sensor.EvaluateStat(3, thermal));
|
||||
var pingBoost = Math.Max(0, sensor.EvaluateStat(5, thermal));
|
||||
var curve = ReadCurveKeys(sensor, 4);
|
||||
foreach (var target in entities)
|
||||
{
|
||||
if (target == null || target.EntityIndex == observer.EntityIndex)
|
||||
continue;
|
||||
|
||||
var dx = target.PositionX - observer.PositionX;
|
||||
var dz = target.PositionZ - observer.PositionZ;
|
||||
var distance = Math.Sqrt(dx * dx + dz * dz);
|
||||
var previous = contacts.TryGetValue(target.EntityIndex, out var contact)
|
||||
? contact.InfoGathered
|
||||
: 0;
|
||||
double next;
|
||||
if (!pinged.Contains(target.EntityIndex) && distance < sensor.State.PingRadius)
|
||||
{
|
||||
pinged.Add(target.EntityIndex);
|
||||
next = Clamp01(previous + target.Visibility * sensitivity * pingBoost * distance);
|
||||
}
|
||||
else
|
||||
{
|
||||
var direction = Normalize(dx, dz, forward.X, forward.Y);
|
||||
var angle = Math.Acos(Clamp(forward.X * direction.X + forward.Y * direction.Y, -1, 1));
|
||||
var angularSensitivity = curve.Count == 0
|
||||
? 0
|
||||
: AetheriaRuntimeDaemonItemStatQueries.SampleCurve(curve, angle / Math.PI);
|
||||
next = Clamp01(previous + target.Visibility * sensitivity * angularSensitivity *
|
||||
deltaSeconds / Math.Max(0.000001, distance));
|
||||
}
|
||||
next *= 1 - Math.Max(0, targetInfoDecay) * deltaSeconds;
|
||||
contacts[target.EntityIndex] = new AetheriaRuntimeEntityContactCommit
|
||||
{
|
||||
TargetEntityIndex = target.EntityIndex,
|
||||
InfoGathered = next,
|
||||
Hostile = Hostile(observer, target)
|
||||
};
|
||||
}
|
||||
sensor.State.PingedEntityIndices = pinged.OrderBy(index => index).ToArray();
|
||||
sensor.State.PingedEntityCount = pinged.Count;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeCurveKey> ReadCurveKeys(
|
||||
AetheriaRuntimeEquippedBehavior behavior,
|
||||
int key)
|
||||
{
|
||||
var value = (behavior.Payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value;
|
||||
return ExtractCurveKeys(value).ToArray();
|
||||
}
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeCurveKey> ExtractCurveKeys(
|
||||
AetheriaRuntimeBehaviorValue? value)
|
||||
{
|
||||
var children = value?.Children ?? Array.Empty<AetheriaRuntimeBehaviorValue>();
|
||||
if (children.Count >= 4 && children.Take(4).All(child =>
|
||||
child != null && string.Equals(child.Kind, "number", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
yield return new AetheriaRuntimeCurveKey(
|
||||
children[0].NumberValue,
|
||||
children[1].NumberValue,
|
||||
children[2].NumberValue,
|
||||
children[3].NumberValue);
|
||||
yield break;
|
||||
}
|
||||
foreach (var child in children)
|
||||
foreach (var key in ExtractCurveKeys(child))
|
||||
yield return key;
|
||||
}
|
||||
|
||||
private static (double X, double Y) SensorDirection(
|
||||
AetheriaRuntimeEntitySnapshotCommit observer,
|
||||
AetheriaRuntimeEquippedBehavior sensor)
|
||||
{
|
||||
var direction = Normalize(observer.DirectionX, observer.DirectionY, 0, 1);
|
||||
return AetheriaRuntimeEquipmentRotation.RotateQuarter(
|
||||
direction.X,
|
||||
direction.Y,
|
||||
AetheriaRuntimeEquipmentRotation.QuarterTurns(sensor.Slot.Rotation));
|
||||
}
|
||||
|
||||
private static double ReadNumber(
|
||||
AetheriaRuntimeEquippedBehavior behavior,
|
||||
int key,
|
||||
double fallback)
|
||||
{
|
||||
var value = (behavior.Payload.Fields ?? Array.Empty<AetheriaRuntimeBehaviorField>())
|
||||
.FirstOrDefault(field => field != null && field.Key == key)?.Value;
|
||||
return value == null || !double.IsFinite(value.NumberValue) ? fallback : value.NumberValue;
|
||||
}
|
||||
|
||||
private static void AppendPingEvent(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit observer,
|
||||
AetheriaRuntimeEquippedBehavior sensor,
|
||||
long frameId,
|
||||
string kind)
|
||||
{
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"{kind}:{zone.ZoneIndex}:{observer.EntityIndex}:{sensor.EquipmentIndex}:{frameId}",
|
||||
Kind = kind,
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
SourceEntityIndex = observer.EntityIndex,
|
||||
SubjectKey = sensor.Item.ItemKey
|
||||
});
|
||||
}
|
||||
|
||||
private static bool ActorMatches(string actorEntityKey, int zoneIndex, int entityIndex) =>
|
||||
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
|
||||
actorEntityKey, out var actorZoneIndex, out var actorEntityIndex) &&
|
||||
actorZoneIndex == zoneIndex && actorEntityIndex == entityIndex;
|
||||
|
||||
private static double ThermalPerformance(AetheriaRuntimeEntitySnapshotCommit entity, int equipmentIndex) =>
|
||||
(entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
|
||||
.FirstOrDefault(state => state != null && state.EquipmentIndex == equipmentIndex)?
|
||||
.ThermalPerformance ?? 1;
|
||||
|
||||
private static bool Hostile(
|
||||
AetheriaRuntimeEntitySnapshotCommit left,
|
||||
AetheriaRuntimeEntitySnapshotCommit right) =>
|
||||
string.Equals(left.FactionKey, "player", StringComparison.OrdinalIgnoreCase) !=
|
||||
string.Equals(right.FactionKey, "player", StringComparison.OrdinalIgnoreCase) &&
|
||||
(string.Equals(left.FactionKey, "raider", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(right.FactionKey, "raider", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static (double X, double Y) Normalize(
|
||||
double x, double y, double fallbackX, double fallbackY)
|
||||
{
|
||||
var length = Math.Sqrt(x * x + y * y);
|
||||
return length <= 0.000001 ? (fallbackX, fallbackY) : (x / length, y / length);
|
||||
}
|
||||
|
||||
private static double Clamp01(double value) => Clamp(value, 0, 1);
|
||||
private static double Clamp(double value, double minimum, double maximum) =>
|
||||
value < minimum ? minimum : value > maximum ? maximum : value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7d92fabacd804a47b1823a42aad8a73b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1511,5 +1511,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(56)]
|
||||
public string MiningToolAsteroidBeltKey { get; set; } = "";
|
||||
|
||||
[Key(57)]
|
||||
public IReadOnlyList<int> PingedEntityIndices { get; set; } = Array.Empty<int>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,51 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
public static class AetheriaRuntimeVisibilitySimulation
|
||||
{
|
||||
private const string TransientVisibilityPrefix = "transient-visibility:";
|
||||
private const string ReflectorVisibilityGrid = "reflector-visibility";
|
||||
private const string EquipmentVisibilityGrid = "equipment-visibility";
|
||||
|
||||
public static void BeginTick(
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit>? entities,
|
||||
double deltaSeconds,
|
||||
double visibilityDecay)
|
||||
{
|
||||
if (deltaSeconds <= 0)
|
||||
return;
|
||||
foreach (var entity in entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
{
|
||||
if (entity == null)
|
||||
continue;
|
||||
var grids = (entity.StatGrids ?? Array.Empty<AetheriaRuntimeEntityStatGridCommit>()).ToList();
|
||||
foreach (var grid in grids.Where(candidate =>
|
||||
candidate != null &&
|
||||
(candidate.Name ?? "").StartsWith(TransientVisibilityPrefix, StringComparison.Ordinal)).ToArray())
|
||||
{
|
||||
var previous = grid.Values.FirstOrDefault();
|
||||
var next = previous * Math.Exp(-Math.Max(0, visibilityDecay) * deltaSeconds);
|
||||
if (next < 0.1)
|
||||
{
|
||||
next = 0;
|
||||
grids.Remove(grid);
|
||||
}
|
||||
else
|
||||
{
|
||||
grid.Values = [next];
|
||||
}
|
||||
entity.Visibility = Math.Max(0, entity.Visibility - previous) + next;
|
||||
}
|
||||
entity.StatGrids = grids;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetTransientSource(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
string sourceKey,
|
||||
double value)
|
||||
{
|
||||
SetVisibilitySource(entity, TransientVisibilityPrefix + (sourceKey ?? ""), Math.Max(0, value));
|
||||
}
|
||||
|
||||
public static void StepZone(
|
||||
AetheriaRuntimeZoneSnapshotCommit? zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit>? entities,
|
||||
|
|
|
|||
|
|
@ -292,18 +292,24 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
equipped-item and active-consumable behavior instances through narrow
|
||||
behavior-owned restore methods; construction/loadout blueprints do not own
|
||||
behavior replay.
|
||||
- Passive contact reach is daemon-owned and derived from enabled installed
|
||||
`Sensor` behavior sensitivity. Arrays combine by root-sum-square, so larger
|
||||
hulls can buy reach by devoting real equipment volume, mass, power, cooling,
|
||||
and computation to sensing. Entity kind is not an input. Cargo is not
|
||||
installed equipment. Hull-authored `Sensors` hardpoints own the physical
|
||||
installation envelope: a smaller same-type array may occupy a larger mount,
|
||||
while generated loadouts choose the largest compatible footprint before
|
||||
applying faction, distance, and price weighting. Station-scale reach emerges
|
||||
from station-scale hardpoints and gear rather than a station multiplier or
|
||||
sensor-count branch. Contact refresh consumes this derived reach but does
|
||||
not write `Visibility`; emitted signature remains owned by thermal,
|
||||
propulsion, weapons, reflectors, explicit visibility sources, and pings.
|
||||
- Sensor contact information is daemon-owned. Each operational installed
|
||||
`Sensor` behavior executes the fossil information equation against target
|
||||
visibility, distance, observer-relative angle, its authored sensitivity
|
||||
curve, and fixed simulation time, then applies the authored information
|
||||
decay. Contacts become visible only when accumulated information crosses the
|
||||
provider-owned detection threshold. Entity kind is not an input. Cargo is
|
||||
not installed equipment. Hull-authored `Sensors` hardpoints own only the
|
||||
installation envelope; generated loadouts still choose compatible equipment
|
||||
through the ordinary footprint, faction, distance, and price rules.
|
||||
- Active ping is the same behavior's actor-scoped daemon command path. The last
|
||||
operational installed sensor owns the request, energy gate, cooldown,
|
||||
expanding radius, and persistent set of already-crossed target indices.
|
||||
A target receives the fossil ping boost and named transient signature once
|
||||
per expansion crossing, including after reconnect; clients cannot replay the
|
||||
boost by resending or rehydrating a request. Manual and future autonomous
|
||||
pings must enter this same typed intent. The removed actorless ping flag and
|
||||
range-based perfect-information refresh are forbidden writers and no longer
|
||||
decide contacts.
|
||||
- The daemon observation query owns docked sensor sharing. A docked controlled
|
||||
craft's effective contact picture merges its own contacts with the dock
|
||||
parent's contacts, retains the primary sensor source on every projected row,
|
||||
|
|
@ -1069,17 +1075,22 @@ projectile lead, hardpoint gating, progressive lock, and target destruction.
|
|||
thrust/turn feedback still
|
||||
require proof.
|
||||
|
||||
### Authoritative Reflected Visibility Contract
|
||||
### Authoritative Visibility Contract
|
||||
|
||||
- Owner: the Aetheria daemon owns each entity's equipment and reflected stellar
|
||||
signature. It evaluates operational `Visibility` and `Reflector` behaviors
|
||||
after energy and thermal state settle and before contact refresh. Eve and
|
||||
Unity only present the resulting visibility and named sources.
|
||||
- Inputs: entity position, equipped reflector performance and thermal state,
|
||||
daemon-owned sun mass, sun light-radius multiplier, the provider render
|
||||
settings light-radius curve, and resolved orbit positions.
|
||||
- Outputs: aggregate entity visibility plus inspectable, reconnectable
|
||||
`equipment-visibility` and `reflector-visibility` source grids.
|
||||
- Owner: the Aetheria daemon owns each entity's emitted, weapon, ping, and
|
||||
reflected stellar signature. It decays named transient sources, evaluates
|
||||
operational `Visibility` and `Reflector` behaviors after energy and thermal
|
||||
state settle, applies authored weapon visibility when a shot commits, and
|
||||
executes sensor pings before contact projection. Eve and Unity only present
|
||||
the resulting visibility and named sources.
|
||||
- Inputs: fixed simulation time, authored visibility-decay setting, entity
|
||||
position, equipped visibility and reflector performance, thermal state,
|
||||
committed weapon visibility (including the charged-weapon multiplier), ping
|
||||
visibility, daemon-owned sun mass, sun light-radius multiplier, the provider
|
||||
render settings light-radius curve, and resolved orbit positions.
|
||||
- Outputs: aggregate entity visibility plus inspectable, reconnectable constant
|
||||
equipment, reflector, weapon, and ping source grids. Transient source values
|
||||
follow the fossil exponential decay and disappear below its cutoff.
|
||||
- Shared-field invariant: daemon visibility and environment rendering resolve
|
||||
body positions through `AetheriaRuntimeOrbitQueries`; gravity-center overrides
|
||||
and nested authored orbits cannot silently put the visible sun and its
|
||||
|
|
@ -1090,11 +1101,13 @@ projectile lead, hardpoint gating, progressive lock, and target destruction.
|
|||
parameters are levers; they do not decide whether another entity detects the
|
||||
reflector.
|
||||
- Forbidden writers: contact refresh cannot overwrite emitted signature, and
|
||||
clients cannot submit visibility, stellar light, or reflector results.
|
||||
clients cannot submit visibility, transient decay, stellar light, ping
|
||||
crossings, weapon flashes, or reflector results.
|
||||
- Verification: daemon smoke proves evaluated constant equipment visibility, a
|
||||
displaced authored sun orbit, the fossil pulse equation, evaluated reflector
|
||||
cross-section, named source publication, and a second tick with no visibility
|
||||
accumulation.
|
||||
cross-section, named source publication without accumulation, actor-scoped
|
||||
energy-gated ping with reconnect-stable crossed-target state, committed weapon
|
||||
visibility, and exact exponential transient decay.
|
||||
- Open parity: active consumable reflector effects and complete visibility-
|
||||
source cardinality remain to be migrated.
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome; the released full-session profile starts with no pickup, destroys a deterministic target, observes its canonical cargo drop, tractors it into a Ymir Begin contact, and proves exactly one `ymir-fact:` collection receipt plus cargo `0` to `1` | richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; shared evaluated-stat query proven; execution families remain partial | every behavior family, ordering, resource transaction and modifier lifecycle |
|
||||
| Sensors/visibility | installed sensor sensitivity, `Visibility`, `Reflector`, decaying signature sources, stellar light, and observer information thresholds | daemon | contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | passive reach derives from enabled installed Sensor behaviors with diminishing-return array composition regardless of entity kind; station-scale arrays are ordinary gear installed into larger hull-authored sensor hardpoints, with smaller same-type arrays remaining valid fallbacks; generation prefers the largest fitting footprint and contains no station/ship sensor-count branch. Operational `Visibility` equipment publishes evaluated constant signature; reflectors multiply evaluated cross-section by the fossil exponent-8 stellar `PowerPulse` after thermal settlement. Both named sources are reconnectable and do not accumulate. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance; the old station range survives only as obsolete snapshot compatibility | active consumable visibility/reflector effects, source cardinality, transient decay, ping, classification timelines, and canonical catalog station-array data |
|
||||
| Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data |
|
||||
| AI/tasks | corporation task scheduler plus agent state machines | daemon | commander task board, corporation survey ledger, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Cross-zone task and home routes now survive the full daemon-owned wormhole timeline instead of instant transfer; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing | cancellation, reconnect, multi-client command transfer, and model-local barrel-direction metadata |
|
||||
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, `ItemUsage`, and stateful `Cooldown` payloads after energy tick initialization; every effect and payload state has stable authored identity; cooldowns update before the ordered chain, require state strictly below zero, reset to one on success, and continue aging behind earlier failures; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity reconciles presentation by effect identity and cannot restore active behavior state by list index | catalog revision pinning, stat modifiers, remaining behavior families, and reconnect presentation proof |
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue