diff --git a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts index 3876d003f..190de6ad0 100644 --- a/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts +++ b/Aetheria.Rts.Web/Electron/aetheria-rts-generated-bindings.ts @@ -264,7 +264,8 @@ export type AetheriaRuntimeDaemonFrameDocumentSlot = | "duplicateImportedFactIds" | "cumulativeImportedFactIds" | "cumulativeRejectedImportedFactIds" - | "renderSettings"; + | "renderSettings" + | "simulationSettings"; export const aetheriaRuntimeDaemonFrameDocumentSlots: Record = { schema: 0, @@ -289,6 +290,7 @@ export const aetheriaRuntimeDaemonFrameDocumentSlots: Record { rtsVerseId, "--daemon-id", rtsDaemonId, - "--rts-cultmesh-port", + "--client-cultmesh-port", rtsCultMeshPort.toString(), - "--rts-cultmesh-advertise-host", + "--client-cultmesh-advertise-host", rtsCultMeshAdvertiseHost, "--tick-interval-ms", "20", @@ -536,7 +536,7 @@ function stringOr(value: unknown, fallback: string): string { function requireClient(): AetheriaCultMeshClient { if (!rtsClient) - throw new Error("Aetheria RTS CultMesh client is not initialized."); + throw new Error("Aetheria CultMesh client is not initialized."); return rtsClient; } diff --git a/Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1 b/Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1 index fde245d4d..a6a531ecc 100644 --- a/Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1 +++ b/Aetheria.Rts.Web/scripts/verify-stage7c-local-runtime.ps1 @@ -21,7 +21,7 @@ try { $daemonOutput = dotnet run --project (Join-Path $repoRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj") -- ` --state $statePath ` --once ` - --rts-cultmesh-port 0 2>&1 + --client-cultmesh-port 0 2>&1 } finally { $ErrorActionPreference = $previousErrorActionPreference @@ -32,12 +32,12 @@ try { } foreach ($line in $daemonOutput) { - if ($line -match "Aetheria RTS CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)") { + if ($line -match "Aetheria client CultMesh endpoint: (rudp://127\.0\.0\.1:\d+)") { $endpoint = $Matches[1] } } if ([string]::IsNullOrWhiteSpace($endpoint)) { - Write-Error "Stage 7C verifier failed: one-shot daemon did not report an RTS CultMesh endpoint." + Write-Error "Stage 7C verifier failed: one-shot daemon did not report a client CultMesh endpoint." } $smokeScript = Join-Path $tempRoot "stage7c-smoke.mjs" diff --git a/Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1 b/Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1 index 3cd3883d1..0547236c4 100644 --- a/Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1 +++ b/Aetheria.Rts.Web/scripts/verify-stage7d-remote-cultmesh.ps1 @@ -33,9 +33,9 @@ try { "--", "--state", $statePath, - "--rts-cultmesh-host", + "--client-cultmesh-host", "127.0.0.1", - "--rts-cultmesh-port", + "--client-cultmesh-port", $port, "--tick-interval-ms", "20", diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 9ebe5e55f..a6ce7564d 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -38,22 +38,22 @@ discoveryHost.Update(verseHost); await PublishRuntimeSessionAsync(node, options, startedAtUtc, "starting").ConfigureAwait(false); await PublishStateSurfacesAsync(node, options, startedAtUtc).ConfigureAwait(false); var latestFrame = await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest).ReadAsync().ConfigureAwait(false); -using var cultMeshRudpHost = StartRtsCultMeshHost(node, options, () => latestFrame); -using var rtsPumpCancellation = new CancellationTokenSource(); -var rtsPump = RunRtsCultMeshPumpAsync(cultMeshRudpHost, rtsPumpCancellation.Token); +using var cultMeshRudpHost = StartClientCultMeshHost(node, options, () => latestFrame); +using var clientPumpCancellation = new CancellationTokenSource(); +var clientPump = RunClientCultMeshPumpAsync(cultMeshRudpHost, clientPumpCancellation.Token); var nextApiPublicationUtc = DateTimeOffset.UtcNow; var firstTick = await TickAsync(node, options, latestFrame, buildPublications: true).ConfigureAwait(false); -ThrowIfRtsPumpFaulted(rtsPump); +ThrowIfClientPumpFaulted(clientPump); latestFrame = firstTick.Frame; nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval); Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}."); -Console.WriteLine($"Aetheria RTS CultMesh endpoint: rudp://{options.RtsCultMeshAdvertiseHost}:{cultMeshRudpHost.LocalEndPoint.Port}"); +Console.WriteLine($"Aetheria client CultMesh endpoint: rudp://{options.ClientCultMeshAdvertiseHost}:{cultMeshRudpHost.LocalEndPoint.Port}"); if (options.Once) { await PublishRuntimeSessionAsync(node, options, startedAtUtc, "completed").ConfigureAwait(false); - rtsPumpCancellation.Cancel(); - await rtsPump.ConfigureAwait(false); + clientPumpCancellation.Cancel(); + await clientPump.ConfigureAwait(false); return; } var stopped = new TaskCompletionSource(); @@ -69,7 +69,7 @@ Console.WriteLine("Aetheria Verse daemon is running. Press Ctrl+C to stop."); var nextTickUtc = DateTimeOffset.UtcNow.Add(options.TickInterval); while (!stopped.Task.IsCompleted) { - ThrowIfRtsPumpFaulted(rtsPump); + ThrowIfClientPumpFaulted(clientPump); var delay = nextTickUtc - DateTimeOffset.UtcNow; if (delay > TimeSpan.Zero) { @@ -80,7 +80,7 @@ while (!stopped.Task.IsCompleted) var buildPublications = DateTimeOffset.UtcNow >= nextApiPublicationUtc; var tick = await TickAsync(node, options, latestFrame, buildPublications).ConfigureAwait(false); - ThrowIfRtsPumpFaulted(rtsPump); + ThrowIfClientPumpFaulted(clientPump); latestFrame = tick.Frame; nextTickUtc += options.TickInterval; if (nextTickUtc < DateTimeOffset.UtcNow - options.TickInterval) @@ -96,19 +96,19 @@ while (!stopped.Task.IsCompleted) } await PublishRuntimeSessionAsync(node, options, startedAtUtc, "stopping").ConfigureAwait(false); -rtsPumpCancellation.Cancel(); -await rtsPump.ConfigureAwait(false); +clientPumpCancellation.Cancel(); +await clientPump.ConfigureAwait(false); Console.WriteLine("Aetheria Verse daemon stopping."); -static void ThrowIfRtsPumpFaulted(Task rtsPump) +static void ThrowIfClientPumpFaulted(Task clientPump) { - if (!rtsPump.IsCompleted) + if (!clientPump.IsCompleted) return; - if (rtsPump.IsFaulted) - throw new InvalidOperationException("Aetheria RTS CultMesh pump faulted.", rtsPump.Exception); + if (clientPump.IsFaulted) + throw new InvalidOperationException("Aetheria client CultMesh pump faulted.", clientPump.Exception); - throw new InvalidOperationException("Aetheria RTS CultMesh pump stopped unexpectedly."); + throw new InvalidOperationException("Aetheria client CultMesh pump stopped unexpectedly."); } static async Task TickAsync( @@ -177,6 +177,7 @@ static async Task TickAsync( CumulativeRejectedCommandIds = currentFrame?.CumulativeRejectedCommandIds ?? currentFrame?.RejectedCommandIds ?? Array.Empty(), Catalog = node.RuntimeCatalog().Latest(), RenderSettings = options.RenderSettings, + SimulationSettings = options.SimulationSettings, StarbridgeScenario = starbridgeScenario, StarbridgeSession = starbridgeSession, BuildPublications = buildPublications, @@ -250,19 +251,19 @@ static async Task PublishCommittedCommandFactsAsync( } } -static RudpCultNetSchemaServer StartRtsCultMeshHost( +static RudpCultNetSchemaServer StartClientCultMeshHost( AetheriaStateNode node, AetheriaDaemonHostOptions options, Func latestFrame) { var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(ParseBindAddress(options.RtsCultMeshHost), options.RtsCultMeshPort)); + socket.Bind(new IPEndPoint(ParseBindAddress(options.ClientCultMeshHost), options.ClientCultMeshPort)); var server = new RudpCultNetSchemaServer(new RudpCultNetSchemaServerOptions { - RuntimeId = $"{options.DaemonId}.rts", + RuntimeId = $"{options.DaemonId}.client", Socket = socket, ConnectionId = 0x43554c54, - TransportId = "aetheria-rts-rudp", + TransportId = "aetheria-client-rudp", MaxFragmentBytes = 1200, MaxPendingReliablePackets = 512 }); @@ -869,7 +870,7 @@ static Task ReadEveSurfacePublicationAsync(AetheriaStateNod }; } -static async Task RunRtsCultMeshPumpAsync( +static async Task RunClientCultMeshPumpAsync( RudpCultNetSchemaServer server, CancellationToken cancellationToken) { @@ -1573,14 +1574,14 @@ static Dictionary BuildOdinProviderAdvertisement( new Dictionary(StringComparer.Ordinal) { ["transport"] = "cultmesh-rudp", - ["address"] = $"rudp://{options.RtsCultMeshAdvertiseHost}:{options.RtsCultMeshPort}", + ["address"] = $"rudp://{options.ClientCultMeshAdvertiseHost}:{options.ClientCultMeshPort}", ["resolver"] = "provider-cultmesh-rudp", ["role"] = "cultmesh-snapshot" }, new Dictionary(StringComparer.Ordinal) { ["transport"] = "cultmesh-rudp", - ["address"] = $"rudp://{options.RtsCultMeshAdvertiseHost}:{options.RtsCultMeshPort}", + ["address"] = $"rudp://{options.ClientCultMeshAdvertiseHost}:{options.ClientCultMeshPort}", ["schemaId"] = AetheriaRuntimeDaemonSchemas.CultMeshCdnAssetBlob, ["resolver"] = "provider-cultmesh-rudp", ["role"] = "cultmesh-cdn" @@ -2383,9 +2384,9 @@ internal sealed class AetheriaDaemonHostOptions public string SessionId { get; init; } = "local"; public string VerseId { get; init; } = "aetheria.local"; public string CultMeshAddress { get; init; } = "cultmesh://aetheria.local/eve/providers/aetheria.daemon"; - public string RtsCultMeshHost { get; init; } = "127.0.0.1"; - public string RtsCultMeshAdvertiseHost { get; init; } = "127.0.0.1"; - public int RtsCultMeshPort { get; init; } = 3076; + public string ClientCultMeshHost { get; init; } = "127.0.0.1"; + public string ClientCultMeshAdvertiseHost { get; init; } = "127.0.0.1"; + public int ClientCultMeshPort { get; init; } = 3076; public string AetheriaResourcesRoot { get; init; } = ""; public string OdinCultMeshUri { get; init; } = "cultmesh://odin/rendezvous/provider-catalog"; public bool EnableOdinAnnouncements { get; init; } = true; @@ -2394,6 +2395,8 @@ internal sealed class AetheriaDaemonHostOptions public double FixedDeltaSeconds { get; init; } = 0.02; public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; init; } = AetheriaRuntimeDaemonRenderSettings.AetheriaDefault; + public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; init; } = + AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault; public bool Once { get; init; } public static AetheriaDaemonHostOptions Parse(IReadOnlyList args) @@ -2408,9 +2411,9 @@ internal sealed class AetheriaDaemonHostOptions var daemonId = ReadOption(args, "--daemon-id"); var verseId = ReadOption(args, "--verse-id"); var cultMeshAddress = ReadOption(args, "--cultmesh-address"); - var rtsCultMeshHost = ReadOption(args, "--rts-cultmesh-host"); - var rtsCultMeshAdvertiseHost = ReadOption(args, "--rts-cultmesh-advertise-host"); - var rtsCultMeshPort = ReadNonNegativeInt(args, "--rts-cultmesh-port") ?? 3076; + var clientCultMeshHost = ReadOption(args, "--client-cultmesh-host"); + var clientCultMeshAdvertiseHost = ReadOption(args, "--client-cultmesh-advertise-host"); + var clientCultMeshPort = ReadNonNegativeInt(args, "--client-cultmesh-port") ?? 3076; var aetheriaResourcesRoot = ReadOption(args, "--aetheria-resources-root"); RejectRemovedOption(args, "--peer-cultmesh-endpoint", "Odin-discovered CultMesh peer documents"); RejectRemovedOption(args, "--odin-cultmesh-rudp", "--odin-cultmesh-uri"); @@ -2432,11 +2435,11 @@ internal sealed class AetheriaDaemonHostOptions CultMeshAddress = string.IsNullOrWhiteSpace(cultMeshAddress) ? "cultmesh://aetheria.local/eve/providers/aetheria.daemon" : cultMeshAddress, - RtsCultMeshHost = string.IsNullOrWhiteSpace(rtsCultMeshHost) ? "127.0.0.1" : rtsCultMeshHost, - RtsCultMeshAdvertiseHost = string.IsNullOrWhiteSpace(rtsCultMeshAdvertiseHost) - ? (string.IsNullOrWhiteSpace(rtsCultMeshHost) || rtsCultMeshHost == "0.0.0.0" || rtsCultMeshHost == "*" ? "127.0.0.1" : rtsCultMeshHost) - : rtsCultMeshAdvertiseHost, - RtsCultMeshPort = rtsCultMeshPort, + ClientCultMeshHost = string.IsNullOrWhiteSpace(clientCultMeshHost) ? "127.0.0.1" : clientCultMeshHost, + ClientCultMeshAdvertiseHost = string.IsNullOrWhiteSpace(clientCultMeshAdvertiseHost) + ? (string.IsNullOrWhiteSpace(clientCultMeshHost) || clientCultMeshHost == "0.0.0.0" || clientCultMeshHost == "*" ? "127.0.0.1" : clientCultMeshHost) + : clientCultMeshAdvertiseHost, + ClientCultMeshPort = clientCultMeshPort, AetheriaResourcesRoot = string.IsNullOrWhiteSpace(aetheriaResourcesRoot) ? Path.GetFullPath(Path.Combine(root, "Assets", "Resources")) : Path.GetFullPath(aetheriaResourcesRoot), diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index a74c52aee..54ee8cc65 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -16020,6 +16020,10 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) var daemonTickRunner = File.Exists(daemonTickRunnerPath) ? File.ReadAllText(daemonTickRunnerPath) : throw new InvalidOperationException("Cannot verify shared simulation authority; daemon tick runner is missing."); + var daemonProgramPath = Path.Combine(root, "Aetheria.State.Daemon", "Program.cs"); + var daemonProgram = File.Exists(daemonProgramPath) + ? File.ReadAllText(daemonProgramPath) + : throw new InvalidOperationException("Cannot verify shared simulation authority; daemon host is missing."); var requiredDaemonTickSymbols = new[] { @@ -16047,12 +16051,30 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeSnapshotDocuments.cs"); - var rtsSimulationPath = Path.Combine( + var oldRtsSimulationPath = Path.Combine( root, "Packages", "org.gamecult.aetheria.state", "Runtime", "AetheriaRuntimeRtsSimulation.cs"); + if (File.Exists(oldRtsSimulationPath)) + { + throw new InvalidOperationException( + "Aetheria simulation authority is still runtime-named. Delete AetheriaRuntimeRtsSimulation.cs and keep runtime-specific names at projection edges."); + } + + var daemonSimulationPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeDaemonSimulation.cs"); + var daemonSimulationSettingsPath = Path.Combine( + root, + "Packages", + "org.gamecult.aetheria.state", + "Runtime", + "AetheriaRuntimeDaemonSimulationSettings.cs"); var ymirProjectilePhysicsPath = Path.Combine( root, "Packages", @@ -16075,9 +16097,12 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) var snapshotDocuments = File.Exists(snapshotDocumentsPath) ? File.ReadAllText(snapshotDocumentsPath) : throw new InvalidOperationException("Cannot verify daemon projectile authority; runtime snapshot documents are missing."); - var rtsSimulation = File.Exists(rtsSimulationPath) - ? File.ReadAllText(rtsSimulationPath) - : throw new InvalidOperationException("Cannot verify daemon projectile authority; RTS simulation is missing."); + var daemonSimulation = File.Exists(daemonSimulationPath) + ? File.ReadAllText(daemonSimulationPath) + : throw new InvalidOperationException("Cannot verify daemon projectile authority; daemon simulation is missing."); + var daemonSimulationSettings = File.Exists(daemonSimulationSettingsPath) + ? File.ReadAllText(daemonSimulationSettingsPath) + : throw new InvalidOperationException("Cannot verify daemon simulation authority; daemon simulation settings are missing."); var ymirProjectilePhysics = File.Exists(ymirProjectilePhysicsPath) ? File.ReadAllText(ymirProjectilePhysicsPath) : throw new InvalidOperationException("Cannot verify daemon projectile authority; Ymir projectile physics boundary is missing."); @@ -16098,14 +16123,23 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) "public IReadOnlyList Projectiles", "public sealed class AetheriaRuntimeProjectileCommit" }, - [rtsSimulationPath] = new[] + [daemonSimulationPath] = new[] { - "StepCombat(zone, entities, deltaSeconds)", + "public static class AetheriaRuntimeDaemonSimulation", + "AetheriaRuntimeDaemonSimulationSettings settings", + "StepCombat(zone, entities, deltaSeconds, settings)", "EnsureDaemonWeaponState(", - "SpawnProjectile(zone, attacker, target)", + "SpawnProjectile(zone, attacker, target, settings)", "AetheriaRuntimeYmirProjectilePhysics.Step(zone, entities, deltaSeconds)", "Damage(target, hit.Projectile.Damage)" }, + [daemonSimulationSettingsPath] = new[] + { + "public readonly struct AetheriaRuntimeDaemonSimulationSettings", + "public static AetheriaRuntimeDaemonSimulationSettings AetheriaDefault", + "public double ProjectileSpeed { get; }", + "public double AttackRange { get; }" + }, [ymirProjectilePhysicsPath] = new[] { "public static class AetheriaRuntimeYmirProjectilePhysics", @@ -16137,7 +16171,8 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) .SelectMany(pair => { var text = pair.Key == snapshotDocumentsPath ? snapshotDocuments : - pair.Key == rtsSimulationPath ? rtsSimulation : + pair.Key == daemonSimulationPath ? daemonSimulation : + pair.Key == daemonSimulationSettingsPath ? daemonSimulationSettings : pair.Key == ymirProjectilePhysicsPath ? ymirProjectilePhysics : pair.Key == rtsDocumentsPath ? rtsDocuments : pair.Key == rtsViewportDocumentsPath ? rtsViewportDocuments : @@ -16154,13 +16189,35 @@ static void RequireUnityDoesNotCallSharedSimulationTicks(string root) string.Join("; ", missingProjectileAuthoritySymbols)); } - if (rtsSimulation.Contains("Damage(target, damage)", StringComparison.Ordinal) || - rtsSimulation.Contains("Damage(target, ResolveProjectileDamage(attacker)", StringComparison.Ordinal) || - rtsSimulation.Contains("private static void StepProjectiles(", StringComparison.Ordinal) || - rtsSimulation.Contains("private static bool ProjectileHits(", StringComparison.Ordinal)) + if (daemonSimulation.Contains("AetheriaRuntimeRtsSimulation", StringComparison.Ordinal) || + daemonSimulation.Contains("Damage(target, damage)", StringComparison.Ordinal) || + daemonSimulation.Contains("Damage(target, ResolveProjectileDamage(attacker)", StringComparison.Ordinal) || + daemonSimulation.Contains("private static void StepProjectiles(", StringComparison.Ordinal) || + daemonSimulation.Contains("private static bool ProjectileHits(", StringComparison.Ordinal) || + daemonSimulation.Contains("private const double PawnSpeed", StringComparison.Ordinal) || + daemonSimulation.Contains("private const double AttackRange", StringComparison.Ordinal) || + daemonSimulation.Contains("private const double ProjectileSpeed", StringComparison.Ordinal)) { throw new InvalidOperationException( - "Daemon RTS combat regressed to direct target damage or local projectile physics instead of Ymir-shaped projectile contact damage."); + "Daemon combat regressed to runtime-named simulation, hardcoded game feel, direct target damage, or local projectile physics instead of daemon settings plus Ymir-shaped projectile contact damage."); + } + + if (!daemonTickRunner.Contains("AetheriaRuntimeDaemonSimulation.Step(", StringComparison.Ordinal) || + !daemonTickRunner.Contains("public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; }", StringComparison.Ordinal) || + !daemonTickRunner.Contains("AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault", StringComparison.Ordinal) || + !daemonTickRunner.Contains("simulationSettings: options.SimulationSettings", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Daemon ticks must route Aetheria simulation through daemon-owned settings, not runtime-local constants."); + } + + if (daemonProgram.Contains("RtsCultMesh", StringComparison.Ordinal) || + daemonProgram.Contains("--rts-cultmesh", StringComparison.Ordinal) || + daemonProgram.Contains("RTS CultMesh endpoint", StringComparison.Ordinal) || + daemonProgram.Contains("aetheria-rts-rudp", StringComparison.Ordinal)) + { + throw new InvalidOperationException( + "Aetheria daemon transport must be client-neutral; runtime-branded CultMesh flags and transport ids belong outside daemon authority."); } if (daemonTickRunner.Contains("AetheriaRuntimeDaemonFrameStore.PublishFrame", StringComparison.Ordinal)) diff --git a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs index 14a101211..6aa42136c 100644 --- a/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs +++ b/Assets/Scripts/Tests/DaemonRuntimeDocumentTests.cs @@ -1925,10 +1925,11 @@ public class DaemonRuntimeDocumentTests } }; - AetheriaRuntimeRtsSimulation.Step( + AetheriaRuntimeDaemonSimulation.Step( run, new AetheriaRuntimeDaemonIntentState(), - 0.1); + 0.1, + AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault); var zone = FindZone(run, 0); Assert.IsNotEmpty(zone.Projectiles); @@ -1961,6 +1962,102 @@ public class DaemonRuntimeDocumentTests obj.IconAsset.AssetKey == "aetheria.asset.sprite.rts.projectile")); } + [Test] + public void DaemonSimulationSettingsDriveAetheriaCombatGameFeel() + { + var run = new AetheriaRuntimeRunCheckpointCommit + { + RunId = "simulation-settings-run", + CurrentZoneIndex = 0, + CurrentEntityKey = "zone.0.entity.0", + Zones = new[] + { + new AetheriaRuntimeZoneSnapshotCommit + { + ZoneIndex = 0, + SimulationTimeSeconds = 4, + Entities = new[] + { + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 0, + Name = "Vanguard", + Kind = "ship", + FactionKey = "player", + IsActive = true, + TargetEntityIndex = 1, + PositionX = 0, + PositionZ = 0 + }, + new AetheriaRuntimeEntitySnapshotCommit + { + EntityIndex = 1, + Name = "Raider", + Kind = "ship", + FactionKey = "raider", + IsActive = true, + PositionX = 90, + PositionZ = 0 + } + } + } + } + }; + var settings = new AetheriaRuntimeDaemonSimulationSettings( + pawnSpeed: 11, + raiderSpeed: 7, + attackRange: 120, + attackHoldRatio: 0.75, + pawnProjectileDamage: 33, + raiderProjectileDamage: 5, + weaponCooldownSeconds: 1.25, + projectileSpeed: 444, + projectileRadius: 9, + projectileLifetimeSeconds: 3.5, + projectileSpawnOffset: 21, + projectileHeatScale: 0.5, + heatDissipationPerSecond: 0, + stationSensorRange: 333, + entitySensorRange: 222, + playerStationHull: 900, + hostileStationHull: 300, + playerEntityHull: 88, + raiderEntityHull: 66, + stationShield: 44, + entityShield: 22); + + AetheriaRuntimeDaemonSimulation.Step( + run, + new AetheriaRuntimeDaemonIntentState(), + 0.1, + settings); + + var zone = FindZone(run, 0); + var projectile = zone.Projectiles.Single(projectile => projectile.SourceEntityIndex == 0); + Assert.AreEqual(21, projectile.PositionX, 0.0001); + Assert.AreEqual(444, projectile.VelocityX, 0.0001); + Assert.AreEqual(33, projectile.Damage, 0.0001); + Assert.AreEqual(9, projectile.Radius, 0.0001); + Assert.AreEqual(3.5, projectile.LifetimeSeconds, 0.0001); + var weapon = zone.Entities[0].WeaponStates.Single(); + Assert.AreEqual(1.25, weapon.CooldownProgress, 0.0001); + Assert.AreEqual(1.25, weapon.BurstInterval, 0.0001); + Assert.AreEqual(88, zone.Entities[0].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001); + Assert.AreEqual(66, zone.Entities[1].StatGrids.Single(grid => grid.Name == "hull").Values[0], 0.0001); + Assert.AreEqual(222, zone.Entities[0].Visibility, 0.0001); + + var frame = AetheriaRuntimeDaemonFrameDocument.Create( + run, + "daemon", + "session", + 5, + 4.1, + 0.1, + simulationSettings: settings); + Assert.AreEqual(444, frame.SimulationSettings.ProjectileSpeed, 0.0001); + Assert.AreEqual(120, frame.SimulationSettings.AttackRange, 0.0001); + } + [Test] public void SoaViewIndexRejectsInvalidRenderGroups() { diff --git a/GameCult.Aetheria.State.Unity.csproj b/GameCult.Aetheria.State.Unity.csproj index 2d6996550..d87907257 100644 --- a/GameCult.Aetheria.State.Unity.csproj +++ b/GameCult.Aetheria.State.Unity.csproj @@ -123,7 +123,8 @@ - + + diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs index fca1f2066..9bdc41dfe 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonDocuments.cs @@ -536,6 +536,10 @@ namespace GameCult.Aetheria.State.Verse public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; set; } = AetheriaRuntimeDaemonRenderSettings.AetheriaDefault; + [Key(22)] + public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; } = + AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault; + public static AetheriaRuntimeDaemonFrameDocument Create( AetheriaRuntimeRunCheckpointCommit run, string daemonId, @@ -545,7 +549,8 @@ namespace GameCult.Aetheria.State.Verse double fixedDeltaSeconds, bool isAuthoritative = true, string stateSource = "daemon", - AetheriaRuntimeDaemonRenderSettings? renderSettings = null) + AetheriaRuntimeDaemonRenderSettings? renderSettings = null, + AetheriaRuntimeDaemonSimulationSettings? simulationSettings = null) { return new AetheriaRuntimeDaemonFrameDocument { @@ -558,7 +563,8 @@ namespace GameCult.Aetheria.State.Verse IsAuthoritative = isAuthoritative, StateSource = string.IsNullOrWhiteSpace(stateSource) ? "daemon" : stateSource, Run = run ?? new AetheriaRuntimeRunCheckpointCommit(), - RenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault + RenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault, + SimulationSettings = simulationSettings ?? AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault }; } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsSimulation.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs similarity index 79% rename from Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsSimulation.cs rename to Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs index 6360874e1..426e3a021 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeRtsSimulation.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulation.cs @@ -6,25 +6,17 @@ using System.Linq; namespace GameCult.Aetheria.State.Verse { - public static class AetheriaRuntimeRtsSimulation + public static class AetheriaRuntimeDaemonSimulation { private const string Hull = "hull"; private const string Shield = "shield"; private const string Heat = "heat"; - private const double PawnSpeed = 84.0; - private const double RaiderSpeed = 68.0; - private const double AttackRange = 145.0; - private const double PawnProjectileDamage = 18.0; - private const double RaiderProjectileDamage = 12.0; - private const double WeaponCooldownSeconds = 0.55; - private const double ProjectileSpeed = 330.0; - private const double ProjectileRadius = 18.0; - private const double ProjectileLifetimeSeconds = 2.2; public static void Step( AetheriaRuntimeRunCheckpointCommit run, AetheriaRuntimeDaemonIntentState intents, - double deltaSeconds) + double deltaSeconds, + AetheriaRuntimeDaemonSimulationSettings settings) { if (run == null || deltaSeconds <= 0) return; @@ -40,20 +32,21 @@ namespace GameCult.Aetheria.State.Verse if (entities.Length == 0) continue; - EnsureStats(entities); - ApplyMovementIntent(run, entities, intents?.Movement); + EnsureStats(entities, settings); + ApplyMovementIntent(run, entities, intents?.Movement, settings); StepRaiderAi(entities); - StepTargetPursuit(entities); + StepTargetPursuit(entities, settings); StepMovement(entities, deltaSeconds); - StepCombat(zone, entities, deltaSeconds); - RefreshContacts(entities); + StepCombat(zone, entities, deltaSeconds, settings); + RefreshContacts(entities, settings); } } private static void ApplyMovementIntent( AetheriaRuntimeRunCheckpointCommit run, IReadOnlyList entities, - AetheriaRuntimeDaemonMovementIntent? movement) + AetheriaRuntimeDaemonMovementIntent? movement, + AetheriaRuntimeDaemonSimulationSettings settings) { if (movement == null || !TryParseEntityIndex(movement.ActorEntityKey, out var entityIndex)) return; @@ -64,7 +57,7 @@ namespace GameCult.Aetheria.State.Verse var magnitude = Clamp01(movement.Magnitude); var normalized = Normalize(movement.DirectionX, movement.DirectionY); - var speed = ResolveSpeed(entity); + var speed = ResolveSpeed(entity, settings); entity.VelocityX = normalized.X * speed * magnitude; entity.VelocityY = normalized.Y * speed * magnitude; if (magnitude <= 0.001) @@ -92,7 +85,9 @@ namespace GameCult.Aetheria.State.Verse } } - private static void StepTargetPursuit(IReadOnlyList entities) + private static void StepTargetPursuit( + IReadOnlyList entities, + AetheriaRuntimeDaemonSimulationSettings settings) { var byIndex = entities.ToDictionary(entity => entity.EntityIndex); foreach (var entity in entities) @@ -108,7 +103,7 @@ namespace GameCult.Aetheria.State.Verse var dx = target.PositionX - entity.PositionX; var dy = target.PositionZ - entity.PositionZ; var distance = Math.Sqrt(dx * dx + dy * dy); - if (distance <= AttackRange * 0.82) + if (distance <= settings.AttackRange * settings.AttackHoldRatio) { entity.VelocityX *= 0.72; entity.VelocityY *= 0.72; @@ -119,7 +114,7 @@ namespace GameCult.Aetheria.State.Verse if (string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)) { var direction = Normalize(dx, dy); - var speed = ResolveSpeed(entity); + var speed = ResolveSpeed(entity, settings); entity.VelocityX = direction.X * speed; entity.VelocityY = direction.Y * speed; Face(entity, direction.X, direction.Y); @@ -148,12 +143,13 @@ namespace GameCult.Aetheria.State.Verse private static void StepCombat( AetheriaRuntimeZoneSnapshotCommit zone, IReadOnlyList entities, - double deltaSeconds) + double deltaSeconds, + AetheriaRuntimeDaemonSimulationSettings settings) { var byIndex = entities.ToDictionary(entity => entity.EntityIndex); foreach (var attacker in entities) { - var weaponState = EnsureDaemonWeaponState(attacker); + var weaponState = EnsureDaemonWeaponState(attacker, settings); weaponState.Firing = false; weaponState.CooldownProgress = Math.Max(0, weaponState.CooldownProgress - deltaSeconds); weaponState.CoolingDown = weaponState.CooldownProgress > 0; @@ -167,7 +163,7 @@ namespace GameCult.Aetheria.State.Verse continue; } - if (DistanceSq(attacker, target) > AttackRange * AttackRange) + if (DistanceSq(attacker, target) > settings.AttackRange * settings.AttackRange) continue; Face(attacker, target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ); @@ -177,11 +173,11 @@ namespace GameCult.Aetheria.State.Verse if (weaponState.CooldownProgress > 0) continue; - SpawnProjectile(zone, attacker, target); + SpawnProjectile(zone, attacker, target, settings); weaponState.Firing = true; weaponState.CoolingDown = true; - weaponState.CooldownProgress = WeaponCooldownSeconds; - SetStat(attacker, Heat, Math.Min(100, GetStat(attacker, Heat) + ResolveProjectileDamage(attacker) * 0.18)); + weaponState.CooldownProgress = settings.WeaponCooldownSeconds; + SetStat(attacker, Heat, Math.Min(100, GetStat(attacker, Heat) + ResolveProjectileDamage(attacker, settings) * settings.ProjectileHeatScale)); } var projectileStep = AetheriaRuntimeYmirProjectilePhysics.Step(zone, entities, deltaSeconds); @@ -194,7 +190,7 @@ namespace GameCult.Aetheria.State.Verse foreach (var entity in entities) { - SetStat(entity, Heat, Math.Max(0, GetStat(entity, Heat) - 8.0 * deltaSeconds)); + SetStat(entity, Heat, Math.Max(0, GetStat(entity, Heat) - settings.HeatDissipationPerSecond * deltaSeconds)); entity.IsActive = IsAlive(entity); if (!entity.IsActive) { @@ -206,7 +202,8 @@ namespace GameCult.Aetheria.State.Verse } private static AetheriaRuntimeWeaponStateCommit EnsureDaemonWeaponState( - AetheriaRuntimeEntitySnapshotCommit entity) + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeDaemonSimulationSettings settings) { var states = (entity.WeaponStates ?? Array.Empty()).ToList(); var state = states.FirstOrDefault(candidate => @@ -224,7 +221,7 @@ namespace GameCult.Aetheria.State.Verse BehaviorKind = "ProjectileWeapon", Ammo = -1, BurstRemaining = 0, - BurstInterval = WeaponCooldownSeconds, + BurstInterval = settings.WeaponCooldownSeconds, LockTargetEntityIndex = -1 }; states.Add(state); @@ -235,7 +232,8 @@ namespace GameCult.Aetheria.State.Verse private static void SpawnProjectile( AetheriaRuntimeZoneSnapshotCommit zone, AetheriaRuntimeEntitySnapshotCommit attacker, - AetheriaRuntimeEntitySnapshotCommit target) + AetheriaRuntimeEntitySnapshotCommit target, + AetheriaRuntimeDaemonSimulationSettings settings) { var direction = Normalize(target.PositionX - attacker.PositionX, target.PositionZ - attacker.PositionZ); if (Math.Abs(direction.X) + Math.Abs(direction.Y) <= 0.0001) @@ -252,16 +250,16 @@ namespace GameCult.Aetheria.State.Verse SourceEntityIndex = attacker.EntityIndex, TargetEntityIndex = target.EntityIndex, FactionKey = attacker.FactionKey ?? "", - PositionX = attacker.PositionX + direction.X * 18.0, + PositionX = attacker.PositionX + direction.X * settings.ProjectileSpawnOffset, PositionY = attacker.PositionY, - PositionZ = attacker.PositionZ + direction.Y * 18.0, + PositionZ = attacker.PositionZ + direction.Y * settings.ProjectileSpawnOffset, DirectionX = direction.X, DirectionY = direction.Y, - VelocityX = direction.X * ProjectileSpeed, - VelocityY = direction.Y * ProjectileSpeed, - Damage = ResolveProjectileDamage(attacker), - Radius = ProjectileRadius, - LifetimeSeconds = ProjectileLifetimeSeconds, + VelocityX = direction.X * settings.ProjectileSpeed, + VelocityY = direction.Y * settings.ProjectileSpeed, + Damage = ResolveProjectileDamage(attacker, settings), + Radius = settings.ProjectileRadius, + LifetimeSeconds = settings.ProjectileLifetimeSeconds, Guided = true, Active = true, WeaponKind = IsPlayerOwned(attacker) ? "vanguard-bolt" : "raider-bolt" @@ -279,12 +277,16 @@ namespace GameCult.Aetheria.State.Verse return $"projectile:{zone.ZoneIndex}:{tick}:{attacker.EntityIndex}:{target.EntityIndex}:{ordinal}"; } - private static double ResolveProjectileDamage(AetheriaRuntimeEntitySnapshotCommit attacker) + private static double ResolveProjectileDamage( + AetheriaRuntimeEntitySnapshotCommit attacker, + AetheriaRuntimeDaemonSimulationSettings settings) { - return IsPlayerOwned(attacker) ? PawnProjectileDamage : RaiderProjectileDamage; + return IsPlayerOwned(attacker) ? settings.PawnProjectileDamage : settings.RaiderProjectileDamage; } - private static void RefreshContacts(IReadOnlyList entities) + private static void RefreshContacts( + IReadOnlyList entities, + AetheriaRuntimeDaemonSimulationSettings settings) { foreach (var observer in entities) { @@ -295,7 +297,7 @@ namespace GameCult.Aetheria.State.Verse continue; var distance = Math.Sqrt(DistanceSq(observer, target)); - var visible = distance <= ResolveSensorRange(observer); + var visible = distance <= ResolveSensorRange(observer, settings); if (!visible && !Hostile(observer, target)) continue; @@ -309,12 +311,14 @@ namespace GameCult.Aetheria.State.Verse } observer.Contacts = contacts; - observer.Visibility = observer.IsActive ? ResolveSensorRange(observer) : 0; + observer.Visibility = observer.IsActive ? ResolveSensorRange(observer, settings) : 0; observer.VisibilitySourceCount = observer.IsActive ? 1 : 0; } } - private static void EnsureStats(IReadOnlyList entities) + private static void EnsureStats( + IReadOnlyList entities, + AetheriaRuntimeDaemonSimulationSettings settings) { foreach (var entity in entities) { @@ -322,8 +326,8 @@ namespace GameCult.Aetheria.State.Verse { entity.StatGrids = new[] { - Stat(Hull, DefaultHull(entity)), - Stat(Shield, DefaultShield(entity)), + Stat(Hull, DefaultHull(entity, settings)), + Stat(Shield, DefaultShield(entity, settings)), Stat(Heat, 0) }; } @@ -366,33 +370,45 @@ namespace GameCult.Aetheria.State.Verse string.Equals(right.FactionKey, "raider", StringComparison.OrdinalIgnoreCase)); } - private static double ResolveSpeed(AetheriaRuntimeEntitySnapshotCommit entity) + private static double ResolveSpeed( + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeDaemonSimulationSettings settings) { if (string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase)) return 0; return string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase) - ? RaiderSpeed - : PawnSpeed; + ? settings.RaiderSpeed + : settings.PawnSpeed; } - private static double ResolveSensorRange(AetheriaRuntimeEntitySnapshotCommit entity) - { - return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) ? 720 : 520; - } - - private static double DefaultHull(AetheriaRuntimeEntitySnapshotCommit entity) + private static double ResolveSensorRange( + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeDaemonSimulationSettings settings) { return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) - ? (IsPlayerOwned(entity) ? 420 : 240) - : string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase) - ? 80 - : 120; + ? settings.StationSensorRange + : settings.EntitySensorRange; } - private static double DefaultShield(AetheriaRuntimeEntitySnapshotCommit entity) + private static double DefaultHull( + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeDaemonSimulationSettings settings) { - return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) ? 120 : 45; + return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) + ? (IsPlayerOwned(entity) ? settings.PlayerStationHull : settings.HostileStationHull) + : string.Equals(entity.FactionKey, "raider", StringComparison.OrdinalIgnoreCase) + ? settings.RaiderEntityHull + : settings.PlayerEntityHull; + } + + private static double DefaultShield( + AetheriaRuntimeEntitySnapshotCommit entity, + AetheriaRuntimeDaemonSimulationSettings settings) + { + return string.Equals(entity.Kind, "station", StringComparison.OrdinalIgnoreCase) + ? settings.StationShield + : settings.EntityShield; } private static double GetStat(AetheriaRuntimeEntitySnapshotCommit entity, string name) diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs new file mode 100644 index 000000000..a6dc8148d --- /dev/null +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSimulationSettings.cs @@ -0,0 +1,142 @@ +using MessagePack; + +#nullable enable + +namespace GameCult.Aetheria.State.Verse +{ + [MessagePackObject(true)] + public readonly struct AetheriaRuntimeDaemonSimulationSettings + { + public static AetheriaRuntimeDaemonSimulationSettings AetheriaDefault { get; } = + new AetheriaRuntimeDaemonSimulationSettings( + pawnSpeed: 84.0, + raiderSpeed: 68.0, + attackRange: 145.0, + attackHoldRatio: 0.82, + pawnProjectileDamage: 18.0, + raiderProjectileDamage: 12.0, + weaponCooldownSeconds: 0.55, + projectileSpeed: 330.0, + projectileRadius: 18.0, + projectileLifetimeSeconds: 2.2, + projectileSpawnOffset: 18.0, + projectileHeatScale: 0.18, + heatDissipationPerSecond: 8.0, + stationSensorRange: 720.0, + entitySensorRange: 520.0, + playerStationHull: 420.0, + hostileStationHull: 240.0, + playerEntityHull: 120.0, + raiderEntityHull: 80.0, + stationShield: 120.0, + entityShield: 45.0); + + public AetheriaRuntimeDaemonSimulationSettings( + double pawnSpeed, + double raiderSpeed, + double attackRange, + double attackHoldRatio, + double pawnProjectileDamage, + double raiderProjectileDamage, + double weaponCooldownSeconds, + double projectileSpeed, + double projectileRadius, + double projectileLifetimeSeconds, + double projectileSpawnOffset, + double projectileHeatScale, + double heatDissipationPerSecond, + double stationSensorRange, + double entitySensorRange, + double playerStationHull, + double hostileStationHull, + double playerEntityHull, + double raiderEntityHull, + double stationShield, + double entityShield) + { + PawnSpeed = PositiveOr(pawnSpeed, AetheriaDefaultRaw.PawnSpeed); + RaiderSpeed = PositiveOr(raiderSpeed, AetheriaDefaultRaw.RaiderSpeed); + AttackRange = PositiveOr(attackRange, AetheriaDefaultRaw.AttackRange); + AttackHoldRatio = Clamp01Or(attackHoldRatio, AetheriaDefaultRaw.AttackHoldRatio); + PawnProjectileDamage = PositiveOr(pawnProjectileDamage, AetheriaDefaultRaw.PawnProjectileDamage); + RaiderProjectileDamage = PositiveOr(raiderProjectileDamage, AetheriaDefaultRaw.RaiderProjectileDamage); + WeaponCooldownSeconds = PositiveOr(weaponCooldownSeconds, AetheriaDefaultRaw.WeaponCooldownSeconds); + ProjectileSpeed = PositiveOr(projectileSpeed, AetheriaDefaultRaw.ProjectileSpeed); + ProjectileRadius = PositiveOr(projectileRadius, AetheriaDefaultRaw.ProjectileRadius); + ProjectileLifetimeSeconds = PositiveOr(projectileLifetimeSeconds, AetheriaDefaultRaw.ProjectileLifetimeSeconds); + ProjectileSpawnOffset = PositiveOr(projectileSpawnOffset, AetheriaDefaultRaw.ProjectileSpawnOffset); + ProjectileHeatScale = NonNegativeOr(projectileHeatScale, AetheriaDefaultRaw.ProjectileHeatScale); + HeatDissipationPerSecond = NonNegativeOr(heatDissipationPerSecond, AetheriaDefaultRaw.HeatDissipationPerSecond); + StationSensorRange = PositiveOr(stationSensorRange, AetheriaDefaultRaw.StationSensorRange); + EntitySensorRange = PositiveOr(entitySensorRange, AetheriaDefaultRaw.EntitySensorRange); + PlayerStationHull = PositiveOr(playerStationHull, AetheriaDefaultRaw.PlayerStationHull); + HostileStationHull = PositiveOr(hostileStationHull, AetheriaDefaultRaw.HostileStationHull); + PlayerEntityHull = PositiveOr(playerEntityHull, AetheriaDefaultRaw.PlayerEntityHull); + RaiderEntityHull = PositiveOr(raiderEntityHull, AetheriaDefaultRaw.RaiderEntityHull); + StationShield = NonNegativeOr(stationShield, AetheriaDefaultRaw.StationShield); + EntityShield = NonNegativeOr(entityShield, AetheriaDefaultRaw.EntityShield); + } + + public double PawnSpeed { get; } + public double RaiderSpeed { get; } + public double AttackRange { get; } + public double AttackHoldRatio { get; } + public double PawnProjectileDamage { get; } + public double RaiderProjectileDamage { get; } + public double WeaponCooldownSeconds { get; } + public double ProjectileSpeed { get; } + public double ProjectileRadius { get; } + public double ProjectileLifetimeSeconds { get; } + public double ProjectileSpawnOffset { get; } + public double ProjectileHeatScale { get; } + public double HeatDissipationPerSecond { get; } + public double StationSensorRange { get; } + public double EntitySensorRange { get; } + public double PlayerStationHull { get; } + public double HostileStationHull { get; } + public double PlayerEntityHull { get; } + public double RaiderEntityHull { get; } + public double StationShield { get; } + public double EntityShield { get; } + + private static double PositiveOr(double value, double fallback) + { + return double.IsFinite(value) && value > 0 ? value : fallback; + } + + private static double NonNegativeOr(double value, double fallback) + { + return double.IsFinite(value) && value >= 0 ? value : fallback; + } + + private static double Clamp01Or(double value, double fallback) + { + return double.IsFinite(value) ? value < 0 ? 0 : value > 1 ? 1 : value : fallback; + } + + private static class AetheriaDefaultRaw + { + public const double PawnSpeed = 84.0; + public const double RaiderSpeed = 68.0; + public const double AttackRange = 145.0; + public const double AttackHoldRatio = 0.82; + public const double PawnProjectileDamage = 18.0; + public const double RaiderProjectileDamage = 12.0; + public const double WeaponCooldownSeconds = 0.55; + public const double ProjectileSpeed = 330.0; + public const double ProjectileRadius = 18.0; + public const double ProjectileLifetimeSeconds = 2.2; + public const double ProjectileSpawnOffset = 18.0; + public const double ProjectileHeatScale = 0.18; + public const double HeatDissipationPerSecond = 8.0; + public const double StationSensorRange = 720.0; + public const double EntitySensorRange = 520.0; + public const double PlayerStationHull = 420.0; + public const double HostileStationHull = 240.0; + public const double PlayerEntityHull = 120.0; + public const double RaiderEntityHull = 80.0; + public const double StationShield = 120.0; + public const double EntityShield = 45.0; + } + } +} diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs index 527f5d1dc..91e9cdd26 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonTickRunner.cs @@ -32,6 +32,8 @@ namespace GameCult.Aetheria.State.Verse public AetheriaRuntimeCatalogSnapshot? Catalog { get; set; } public AetheriaRuntimeDaemonRenderSettings RenderSettings { get; set; } = AetheriaRuntimeDaemonRenderSettings.AetheriaDefault; + public AetheriaRuntimeDaemonSimulationSettings SimulationSettings { get; set; } = + AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault; public bool BuildPublications { get; set; } = true; } @@ -126,10 +128,11 @@ namespace GameCult.Aetheria.State.Verse .ToArray(), operationResult.Intents); } - AetheriaRuntimeRtsSimulation.Step( + AetheriaRuntimeDaemonSimulation.Step( operationResult.Run, operationResult.Intents, - options.FixedDeltaSeconds); + options.FixedDeltaSeconds, + options.SimulationSettings); StampZoneSimulationTime(operationResult.Run, options.SimulationTimeSeconds); var frame = AetheriaRuntimeDaemonFrameDocument.Create( @@ -139,7 +142,8 @@ namespace GameCult.Aetheria.State.Verse options.FrameId, options.SimulationTimeSeconds, options.FixedDeltaSeconds, - renderSettings: options.RenderSettings); + renderSettings: options.RenderSettings, + simulationSettings: options.SimulationSettings); frame.AppliedCommandIds = operationResult.AppliedCommandIds; frame.RejectedCommandIds = operationResult.RejectedCommandIds; frame.AccountedCommandIds = accountedBeforeTick