feat(terminus): restore single-player pilot loop
This commit is contained in:
parent
97f86d5365
commit
bfb5d9d1d5
11 changed files with 186 additions and 9 deletions
|
|
@ -5,7 +5,7 @@ using GameCult.Caching;
|
|||
|
||||
internal static class AetheriaDaemonZoneGenerator
|
||||
{
|
||||
public const string RunId = "local-starbridge";
|
||||
public const string RunId = "local-terminus";
|
||||
public const uint GenerationSeed = 0xA37E_2026u;
|
||||
|
||||
public static async Task WritePlayableRunAsync(
|
||||
|
|
@ -25,7 +25,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
.ReadAsync()
|
||||
.ConfigureAwait(false) ?? new AetheriaPlayerSettings();
|
||||
settings.ActiveRunKey = runKey.ToString();
|
||||
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Starbridge Commander" : settings.PlayerName;
|
||||
settings.PlayerName = string.IsNullOrWhiteSpace(settings.PlayerName) ? "Terminus Pilot" : settings.PlayerName;
|
||||
settings.TutorialPassed = true;
|
||||
settings.LastUpdatedAtUtc = now;
|
||||
await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
|
||||
|
|
@ -41,7 +41,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
DiscoveredZoneIndices = [0],
|
||||
ZoneKeys = [zoneKey.ToString()],
|
||||
GenerationSeed = GenerationSeed,
|
||||
CurrentEntityKey = entityKeys[0],
|
||||
CurrentEntityKey = entityKeys[1],
|
||||
UpdatedAtUtc = now
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
|
|
@ -218,7 +218,7 @@ internal static class AetheriaDaemonZoneGenerator
|
|||
HullItemKey = loadout.HullItemKey,
|
||||
IsActive = true,
|
||||
HeatsinksEnabled = true,
|
||||
TractorPower = 1,
|
||||
TractorPower = 0,
|
||||
Visibility = visibility,
|
||||
VisibilitySourceCount = 1,
|
||||
TargetEntityKey = target,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
|||
await EnsureTradeValuePolicyAsync(node, startedAtUtc).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
await EnsureStarbridgeSessionDocumentsAsync(node, options).ConfigureAwait(false);
|
||||
await EnsureTerminusGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
var verseHost = await EnsureVerseHostSettingsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
await EnsureVerseAuthorityPolicyAsync(node, options).ConfigureAwait(false);
|
||||
discoveryHost.Update(verseHost);
|
||||
|
|
@ -119,6 +119,8 @@ static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
|
|||
{
|
||||
await AcceptCoreEveInvocationsAsync(node, options, currentFrame).ConfigureAwait(false);
|
||||
await AcceptEveCommandsAsync(node, options).ConfigureAwait(false);
|
||||
if (await ApplyRequestedTerminusSessionAsync(node, options).ConfigureAwait(false))
|
||||
currentFrame = null;
|
||||
|
||||
var fixedDeltaSeconds = currentFrame?.FixedDeltaSeconds > 0
|
||||
? currentFrame.FixedDeltaSeconds
|
||||
|
|
@ -2211,7 +2213,7 @@ static async Task EnsurePlayableRunDocumentsAsync(
|
|||
if (!string.IsNullOrWhiteSpace(settings?.ActiveRunKey))
|
||||
{
|
||||
var existingRun = await ReadRuntimeRunCheckpointAsync(node, options.RenderSettings).ConfigureAwait(false);
|
||||
if (HasPlayableRun(existingRun) && HasStarbridgeScenario(existingRun))
|
||||
if (HasPlayableRun(existingRun) && HasTerminusRun(existingRun))
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -2222,6 +2224,63 @@ static async Task EnsurePlayableRunDocumentsAsync(
|
|||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
static async Task EnsureTerminusGameSessionAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options,
|
||||
string now)
|
||||
{
|
||||
var existing = await node.MutableDocument<AetheriaGameSessionState>(AetheriaStateNode.GameSessionStateKey)
|
||||
.ReadAsync().ConfigureAwait(false);
|
||||
if (existing != null &&
|
||||
string.Equals(existing.Mode, AetheriaGameSessionState.TerminusMode, StringComparison.Ordinal) &&
|
||||
!string.IsNullOrWhiteSpace(existing.ControlledEntityKey))
|
||||
return;
|
||||
|
||||
await node.MutableDocument<AetheriaGameSessionState>(AetheriaStateNode.GameSessionStateKey)
|
||||
.ReplaceAsync(new AetheriaGameSessionState
|
||||
{
|
||||
Mode = AetheriaGameSessionState.TerminusMode,
|
||||
SessionId = options.SessionId,
|
||||
RunId = AetheriaDaemonZoneGenerator.RunId,
|
||||
ControlledEntityKey = AetheriaDaemonZoneGenerator.EntityKey(0, 1),
|
||||
EntrySurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
||||
UpdatedAtUtc = now
|
||||
}).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
static async Task<bool> ApplyRequestedTerminusSessionAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options)
|
||||
{
|
||||
var menu = await node.MutableDocument<AetheriaMainMenuState>(AetheriaStateNode.MainMenuStateKey)
|
||||
.ReadAsync().ConfigureAwait(false);
|
||||
if (menu == null ||
|
||||
!string.Equals(menu.LastCommand, AetheriaRuntimeMainMenuCommands.NewGame, StringComparison.Ordinal) ||
|
||||
string.IsNullOrWhiteSpace(menu.LastCommandId))
|
||||
return false;
|
||||
|
||||
var session = await node.MutableDocument<AetheriaGameSessionState>(AetheriaStateNode.GameSessionStateKey)
|
||||
.ReadAsync().ConfigureAwait(false) ?? new AetheriaGameSessionState();
|
||||
if (string.Equals(session.LastStartCommandId, menu.LastCommandId, StringComparison.Ordinal))
|
||||
return false;
|
||||
|
||||
var now = DateTimeOffset.UtcNow.ToString("O");
|
||||
await AetheriaDaemonZoneGenerator.WritePlayableRunAsync(node, node.RuntimeCatalog().Latest(), now)
|
||||
.ConfigureAwait(false);
|
||||
session.Mode = AetheriaGameSessionState.TerminusMode;
|
||||
session.SessionId = options.SessionId;
|
||||
session.RunId = AetheriaDaemonZoneGenerator.RunId;
|
||||
session.ControlledEntityKey = AetheriaDaemonZoneGenerator.EntityKey(0, 1);
|
||||
session.EntrySurfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId;
|
||||
session.LastStartCommandId = menu.LastCommandId;
|
||||
session.UpdatedAtUtc = now;
|
||||
await node.MutableDocument<AetheriaGameSessionState>(AetheriaStateNode.GameSessionStateKey)
|
||||
.ReplaceAsync(session).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
static async Task EnsureStarbridgeSessionDocumentsAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options)
|
||||
|
|
@ -2674,7 +2733,7 @@ static bool HasPlayableRun(AetheriaRuntimeRunCheckpointCommit? run)
|
|||
.Any(zone => (zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()).Count > 0);
|
||||
}
|
||||
|
||||
static bool HasStarbridgeScenario(AetheriaRuntimeRunCheckpointCommit? run)
|
||||
static bool HasTerminusRun(AetheriaRuntimeRunCheckpointCommit? run)
|
||||
{
|
||||
var zones = run?.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>();
|
||||
var entities = zones.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()).ToArray();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ public static class AetheriaDocumentRegistry
|
|||
typeof(AetheriaEntitySnapshot),
|
||||
typeof(AetheriaVerseHostSettings),
|
||||
typeof(AetheriaMainMenuState),
|
||||
typeof(AetheriaGameSessionState),
|
||||
typeof(AetheriaEveCommandAcceptanceStatus),
|
||||
typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument),
|
||||
typeof(AetheriaRuntimeDaemonHealthDocument),
|
||||
|
|
|
|||
|
|
@ -262,6 +262,9 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
|
|||
public static CultRecordKey MainMenuStateKey { get; } =
|
||||
new("global:aetheria.main_menu_state.v1");
|
||||
|
||||
public static CultRecordKey GameSessionStateKey { get; } =
|
||||
new("global:aetheria.game_session.v1");
|
||||
|
||||
public static CultRecordKey TradeValuePolicyKey { get; } =
|
||||
new(AetheriaTradeValuePolicy.RecordKey);
|
||||
|
||||
|
|
|
|||
20
Aetheria.State/Documents/AetheriaGameSessionState.cs
Normal file
20
Aetheria.State/Documents/AetheriaGameSessionState.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using GameCult.Caching;
|
||||
using MessagePack;
|
||||
|
||||
namespace Aetheria.State.Documents;
|
||||
|
||||
[CultDocument("aetheria.game_session", "aetheria.game_session.v1")]
|
||||
[CultGlobal]
|
||||
[MessagePackObject]
|
||||
public sealed class AetheriaGameSessionState
|
||||
{
|
||||
public const string TerminusMode = "terminus";
|
||||
|
||||
[Key(0)] public string Mode { get; set; } = TerminusMode;
|
||||
[Key(1)] public string SessionId { get; set; } = "";
|
||||
[Key(2)] public string RunId { get; set; } = "";
|
||||
[Key(3)] public string ControlledEntityKey { get; set; } = "";
|
||||
[Key(4)] public string EntrySurfaceId { get; set; } = "aetheria.pilot";
|
||||
[Key(5)] public string LastStartCommandId { get; set; } = "";
|
||||
[Key(6)] public string UpdatedAtUtc { get; set; } = "";
|
||||
}
|
||||
|
|
@ -2159,6 +2159,64 @@ public class DaemonRuntimeDocumentTests
|
|||
Assert.AreEqual(120, frame.SimulationSettings.AttackRange, 0.0001);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TerminusPilotTractorScoopsCargoFromTargetedWreck()
|
||||
{
|
||||
var loot = new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "raider-salvage" }
|
||||
};
|
||||
var pilot = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 1,
|
||||
Kind = "ship",
|
||||
FactionKey = "player",
|
||||
IsActive = true,
|
||||
TargetEntityIndex = 6,
|
||||
TractorPower = 1,
|
||||
CargoContents = new[] { new AetheriaRuntimeCargoBayLoadoutCommit() }
|
||||
};
|
||||
var wreck = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 6,
|
||||
Kind = "ship",
|
||||
FactionKey = "raider",
|
||||
IsActive = false,
|
||||
PositionX = 10,
|
||||
CargoContents = new[]
|
||||
{
|
||||
new AetheriaRuntimeCargoBayLoadoutCommit { Items = new[] { loot } }
|
||||
},
|
||||
StatGrids = new[]
|
||||
{
|
||||
new AetheriaRuntimeEntityStatGridCommit { Name = "hull", Values = new[] { 0.0 } }
|
||||
}
|
||||
};
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "local-terminus",
|
||||
CurrentZoneIndex = 0,
|
||||
CurrentEntityKey = "global:aetheria.run_state.local-terminus.zone.0.entity.1.v1",
|
||||
Zones = new[]
|
||||
{
|
||||
new AetheriaRuntimeZoneSnapshotCommit
|
||||
{
|
||||
ZoneIndex = 0,
|
||||
Entities = new[] { pilot, wreck }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AetheriaRuntimeDaemonSimulation.Step(
|
||||
run,
|
||||
new AetheriaRuntimeDaemonIntentState(),
|
||||
0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
|
||||
|
||||
Assert.IsTrue(pilot.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "raider-salvage"));
|
||||
Assert.IsEmpty(wreck.CargoContents);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SoaViewIndexRejectsInvalidRenderGroups()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
CommandButton("aetheria.daemon.game.commands.move", "Move", AetheriaRuntimeDaemonCommandKinds.SetMoveVector),
|
||||
CommandButton("aetheria.daemon.game.commands.target", "Target Nearest", AetheriaRuntimeDaemonCommandKinds.TargetNearest),
|
||||
CommandButton("aetheria.daemon.game.commands.fire", "Fire", AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup),
|
||||
CommandButton("aetheria.daemon.game.commands.tractor", "Scoop", AetheriaRuntimeDaemonCommandKinds.SetTractorPower),
|
||||
CommandButton("aetheria.daemon.game.commands.dock", "Dock", AetheriaRuntimeDaemonCommandKinds.DockNearest),
|
||||
CommandButton("aetheria.daemon.game.commands.undock", "Undock", AetheriaRuntimeDaemonCommandKinds.Undock),
|
||||
CommandButton("aetheria.daemon.game.commands.ping", "Sensor Ping", AetheriaRuntimeDaemonCommandKinds.SensorPing)))
|
||||
};
|
||||
|
||||
|
|
@ -343,6 +346,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
["focusCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.TargetNearest),
|
||||
["targetCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.SetTarget),
|
||||
["actionCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup),
|
||||
["tractorCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.SetTractorPower),
|
||||
["dockCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.DockNearest),
|
||||
["undockCommand"] = CommandName(AetheriaRuntimeDaemonCommandKinds.Undock),
|
||||
["zoneIndex"] = zone.ZoneIndex.ToString(CultureInfo.InvariantCulture),
|
||||
["runId"] = run.RunId ?? ""
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,10 +38,39 @@ namespace GameCult.Aetheria.State.Verse
|
|||
StepTargetPursuit(entities, settings);
|
||||
StepMovement(entities, deltaSeconds);
|
||||
StepCombat(run, zone, entities, intents, deltaSeconds, settings);
|
||||
StepTractorSalvage(run, zone, entities, settings);
|
||||
RefreshContacts(entities, settings);
|
||||
}
|
||||
}
|
||||
|
||||
private static void StepTractorSalvage(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings)
|
||||
{
|
||||
if (!TryParseEntityIndex(run.CurrentEntityKey, out var actorIndex))
|
||||
return;
|
||||
var actor = entities.FirstOrDefault(entity => entity.EntityIndex == actorIndex && IsAlive(entity));
|
||||
if (actor == null || actor.TractorPower <= 0 || actor.TargetEntityIndex < 0)
|
||||
return;
|
||||
var wreck = entities.FirstOrDefault(entity => entity.EntityIndex == actor.TargetEntityIndex && !IsAlive(entity));
|
||||
if (wreck == null || DistanceSq(actor, wreck) > settings.AttackRange * settings.AttackRange)
|
||||
return;
|
||||
|
||||
var loot = (wreck.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
||||
.SelectMany(bay => bay?.Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
||||
.ToArray();
|
||||
if (loot.Length == 0)
|
||||
return;
|
||||
var bays = (actor.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToList();
|
||||
if (bays.Count == 0)
|
||||
bays.Add(new AetheriaRuntimeCargoBayLoadoutCommit());
|
||||
bays[0].Items = (bays[0].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).Concat(loot).ToArray();
|
||||
actor.CargoContents = bays.ToArray();
|
||||
wreck.CargoContents = Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
||||
}
|
||||
|
||||
private static void ApplyMovementIntent(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
|
|
|
|||
|
|
@ -1567,7 +1567,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var zone = zones.FirstOrDefault(candidate => candidate.ZoneIndex == run.CurrentZoneIndex) ??
|
||||
zones.FirstOrDefault() ??
|
||||
new AetheriaRuntimeZoneSnapshotCommit();
|
||||
var runId = string.IsNullOrWhiteSpace(run.RunId) ? "local-starbridge" : run.RunId;
|
||||
var runId = string.IsNullOrWhiteSpace(run.RunId) ? "local-terminus" : run.RunId;
|
||||
return new DocumentContext(run, zone, runId);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -709,7 +709,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.Where(entity => entity != null && entity.IsActive)
|
||||
.Select(entity =>
|
||||
{
|
||||
var entityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run?.RunId ?? "local-starbridge", currentZone.ZoneIndex, entity.EntityIndex);
|
||||
var entityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(run?.RunId ?? "local-terminus", currentZone.ZoneIndex, entity.EntityIndex);
|
||||
return new AetheriaMainMenuGravityObject(
|
||||
entityKey,
|
||||
entity.Kind,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@
|
|||
"aetheria.runtime.daemon.game_surface.v1",
|
||||
"aetheria.runtime.daemon.editor_surface.v1",
|
||||
"aetheria.runtime.daemon.command.v1",
|
||||
"aetheria.game_session.v1",
|
||||
"gamecult.eve.surface.v1",
|
||||
"gamecult.eve.command.v1",
|
||||
"gamecult.eve.command_invocation.v1",
|
||||
|
|
|
|||
Loading…
Reference in a new issue