Port historical asteroid mining to daemon
This commit is contained in:
parent
21c469565a
commit
922b0b06ac
7 changed files with 225 additions and 7 deletions
|
|
@ -21,6 +21,45 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
RejectedHaulTransferDoesNotAdvanceTask();
|
||||
AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands();
|
||||
TickReconcilesAndEvaluatesCatalogBehaviors();
|
||||
AgentMinesAsteroidThroughEquippedBehavior();
|
||||
}
|
||||
|
||||
private static void AgentMinesAsteroidThroughEquippedBehavior()
|
||||
{
|
||||
var miner = Entity(0, 0, "workers");
|
||||
miner.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Mine];
|
||||
miner.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "mining-tool", Enabled = true, Quality = 1, Durability = 1 } }];
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
[CatalogItem("mining-tool", new AetheriaRuntimeBehaviorPayload(0, "MiningTool", 0,
|
||||
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(1000)), new AetheriaRuntimeBehaviorField(2, PerformanceStat(1000000000)), new AetheriaRuntimeBehaviorField(3, PerformanceStat(2)), new AetheriaRuntimeBehaviorField(4, PerformanceStat(1000))]))],
|
||||
[], []);
|
||||
var asteroid = new AetheriaRuntimeAsteroidCommit { Distance = 0, Size = 6 };
|
||||
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
||||
{
|
||||
ZoneIndex = 0,
|
||||
Entities = [miner],
|
||||
Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "belt", Kind = "asteroid_belt", Asteroids = [asteroid], Resources = [new AetheriaRuntimeBodyResourceCommit { ItemKey = "iron", Amount = 1 }] }]
|
||||
};
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
GenerationSeed = 7,
|
||||
CurrentZoneIndex = 0,
|
||||
CurrentEntityKey = "zone.0.entity.0",
|
||||
Zones = [zone],
|
||||
AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "mine-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Mine, ZoneIndex = 0, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetBodyKeys = ["belt"] }]
|
||||
};
|
||||
|
||||
var result = AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-mining-smoke.cc"), run,
|
||||
new AetheriaRuntimeDaemonTickOptions { FrameId = 1, FixedDeltaSeconds = 1, SimulationTimeSeconds = 1, Catalog = catalog, BuildPublications = false });
|
||||
|
||||
Require(run.AgentTasks[0].Phase == "mining", "mining task must activate its equipped tool");
|
||||
Require(result.OperationResult.Intents.Behaviors.Count == 1,
|
||||
$"mining behavior command must become daemon intent (applied={string.Join(',', result.Frame.AppliedCommandIds)}, rejected={string.Join(',', result.Frame.RejectedCommandIds)})");
|
||||
var committedMiner = run.Zones[0].Entities.Single(entity => entity.EntityIndex == 0);
|
||||
var committedAsteroid = run.Zones[0].Bodies[0].Asteroids[0];
|
||||
Require(committedAsteroid.RespawnTimer > 0, $"mining damage must deplete the asteroid and start historical respawn (damage={committedAsteroid.Damage}, respawn={committedAsteroid.RespawnTimer})");
|
||||
Require(committedMiner.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "iron" && slot.Item.Quantity > 0),
|
||||
"historical mining yield must enter daemon-owned cargo");
|
||||
}
|
||||
|
||||
private static void TickReconcilesAndEvaluatesCatalogBehaviors()
|
||||
|
|
|
|||
|
|
@ -82,14 +82,16 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
public static IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> AssignAndPlan(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
long frameId)
|
||||
long frameId,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null,
|
||||
double simulationTimeSeconds = 0)
|
||||
{
|
||||
if (run == null)
|
||||
return Array.Empty<AetheriaRuntimeDaemonCommandDocument>();
|
||||
|
||||
ReleaseInvalidAssignments(run);
|
||||
AssignQueuedTasks(run, frameId);
|
||||
return PlanAssignedTasks(run, frameId);
|
||||
return PlanAssignedTasks(run, frameId, catalog, simulationTimeSeconds);
|
||||
}
|
||||
|
||||
public static void Reconcile(
|
||||
|
|
@ -164,7 +166,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> PlanAssignedTasks(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
long frameId)
|
||||
long frameId,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double simulationTimeSeconds)
|
||||
{
|
||||
var commands = new List<AetheriaRuntimeDaemonCommandDocument>();
|
||||
foreach (var task in (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
|
||||
|
|
@ -181,6 +185,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
commands.AddRange(PlanHaul(run, zone!, task, agent, frameId));
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Mine, StringComparison.Ordinal))
|
||||
{
|
||||
commands.AddRange(PlanMining(zone!, task, agent, frameId, catalog, simulationTimeSeconds));
|
||||
continue;
|
||||
}
|
||||
if (string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Defend, StringComparison.Ordinal))
|
||||
{
|
||||
commands.AddRange(PlanPatrol(zone!, task, agent, frameId));
|
||||
|
|
@ -223,6 +232,53 @@ namespace GameCult.Aetheria.State.Verse
|
|||
return commands;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> PlanMining(
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeAgentTaskCommit task,
|
||||
AetheriaRuntimeEntitySnapshotCommit agent,
|
||||
long frameId,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
double simulationTimeSeconds)
|
||||
{
|
||||
var tool = AetheriaRuntimeEquippedBehaviorQueries.Find(agent, catalog, "MiningTool").FirstOrDefault();
|
||||
var bodyKey = (task.TargetBodyKeys ?? Array.Empty<string>()).FirstOrDefault() ?? "";
|
||||
var body = (zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null && string.Equals(candidate.BodyKey, bodyKey, StringComparison.Ordinal));
|
||||
if (tool == null || body == null)
|
||||
{
|
||||
Fail(task, agent);
|
||||
return Array.Empty<AetheriaRuntimeDaemonCommandDocument>();
|
||||
}
|
||||
|
||||
var asteroid = AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(zone, bodyKey, simulationTimeSeconds)
|
||||
.Where(pose => pose.AsteroidIndex >= 0 && pose.AsteroidIndex < body.Asteroids.Count && body.Asteroids[pose.AsteroidIndex].RespawnTimer <= 0)
|
||||
.OrderBy(pose => Math.Pow(pose.PositionX - agent.PositionX, 2) + Math.Pow(pose.PositionZ - agent.PositionZ, 2))
|
||||
.FirstOrDefault();
|
||||
if (asteroid.BodyKey == null)
|
||||
return new[] { Movement(task, agent, frameId, 0, 0, 0) };
|
||||
|
||||
var dx = asteroid.PositionX - agent.PositionX;
|
||||
var dz = asteroid.PositionZ - agent.PositionZ;
|
||||
var distance = Math.Sqrt(dx * dx + dz * dz);
|
||||
var range = Math.Max(0.01, tool.EvaluateStat(4));
|
||||
if (distance > range)
|
||||
return new[] { Movement(task, agent, frameId, dx, dz, 1) };
|
||||
|
||||
task.Phase = "mining";
|
||||
return new[]
|
||||
{
|
||||
Movement(task, agent, frameId, 0, 0, 0),
|
||||
Command(task, agent, frameId, AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive, "mine", command =>
|
||||
{
|
||||
command.EquipmentIndex = tool.EquipmentIndex;
|
||||
command.BehaviorIndex = tool.BehaviorIndex;
|
||||
command.ScalarValue = 1;
|
||||
command.TextValue = bodyKey;
|
||||
command.PositionX = asteroid.AsteroidIndex;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> PlanHaul(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public int EquipmentIndex { get; set; } = -1;
|
||||
public int BehaviorIndex { get; set; } = -1;
|
||||
public bool Active { get; set; }
|
||||
public string TargetBodyKey { get; set; } = "";
|
||||
public int TargetAsteroidIndex { get; set; } = -1;
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeDaemonConsumableIntent
|
||||
|
|
|
|||
|
|
@ -1095,7 +1095,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
ActorEntityKey = actor,
|
||||
EquipmentIndex = command.EquipmentIndex,
|
||||
BehaviorIndex = command.BehaviorIndex,
|
||||
Active = command.ScalarValue > 0.5
|
||||
Active = command.ScalarValue > 0.5,
|
||||
TargetBodyKey = command.TextValue ?? "",
|
||||
TargetAsteroidIndex = (int)command.PositionX
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonIntentState intents,
|
||||
double deltaSeconds,
|
||||
AetheriaRuntimeDaemonSimulationSettings settings,
|
||||
IAetheriaRuntimeProjectilePhysics projectilePhysics)
|
||||
IAetheriaRuntimeProjectilePhysics projectilePhysics,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog = null,
|
||||
long frameId = 0,
|
||||
double simulationTimeSeconds = 0)
|
||||
{
|
||||
if (run == null || deltaSeconds <= 0)
|
||||
return;
|
||||
|
|
@ -44,6 +47,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
StepTargetPursuit(entities, settings);
|
||||
StepMovement(entities, deltaSeconds);
|
||||
StepCombat(run, zone, entities, intents, deltaSeconds, settings, projectilePhysics);
|
||||
AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
|
||||
StepTractorSalvage(run, zone, entities, settings);
|
||||
RefreshContacts(entities, settings);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,11 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.ToArray(),
|
||||
operationResult.Intents);
|
||||
}
|
||||
var agentCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan(operationResult.Run, options.FrameId);
|
||||
var agentCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan(
|
||||
operationResult.Run,
|
||||
options.FrameId,
|
||||
options.Catalog,
|
||||
options.SimulationTimeSeconds);
|
||||
if (agentCommands.Count > 0)
|
||||
{
|
||||
var agentResult = AetheriaRuntimeDaemonOperations.Execute(
|
||||
|
|
@ -157,7 +161,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
operationResult.Intents,
|
||||
options.FixedDeltaSeconds,
|
||||
options.SimulationSettings,
|
||||
options.ProjectilePhysics);
|
||||
options.ProjectilePhysics,
|
||||
options.Catalog,
|
||||
options.FrameId,
|
||||
options.SimulationTimeSeconds);
|
||||
StampZoneSimulationTime(operationResult.Run, options.SimulationTimeSeconds);
|
||||
|
||||
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimeMiningSimulation
|
||||
{
|
||||
public const double MiningDifficulty = 500;
|
||||
|
||||
public static void Step(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
AetheriaRuntimeDaemonIntentState intents,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId,
|
||||
double simulationTimeSeconds,
|
||||
double deltaSeconds)
|
||||
{
|
||||
foreach (var body in zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
||||
foreach (var asteroid in body?.Asteroids ?? Array.Empty<AetheriaRuntimeAsteroidCommit>())
|
||||
asteroid.RespawnTimer = Math.Max(0, asteroid.RespawnTimer - deltaSeconds);
|
||||
|
||||
foreach (var intent in intents?.Behaviors ?? Enumerable.Empty<AetheriaRuntimeDaemonBehaviorIntent>())
|
||||
{
|
||||
if (!intent.Active || !TryEntityIndex(intent.ActorEntityKey, out var minerIndex))
|
||||
continue;
|
||||
var miner = entities.FirstOrDefault(entity => entity.EntityIndex == minerIndex && entity.IsActive);
|
||||
var tool = AetheriaRuntimeEquippedBehaviorQueries.Find(miner, catalog, "MiningTool")
|
||||
.FirstOrDefault(candidate => candidate.EquipmentIndex == intent.EquipmentIndex && candidate.BehaviorIndex == intent.BehaviorIndex);
|
||||
var body = (zone.Bodies ?? Array.Empty<AetheriaRuntimeBodySnapshotCommit>())
|
||||
.FirstOrDefault(candidate => candidate != null && string.Equals(candidate.BodyKey, intent.TargetBodyKey, StringComparison.Ordinal));
|
||||
if (miner == null || tool == null || body == null || intent.TargetAsteroidIndex < 0 || intent.TargetAsteroidIndex >= body.Asteroids.Count)
|
||||
continue;
|
||||
var asteroid = body.Asteroids[intent.TargetAsteroidIndex];
|
||||
if (asteroid.RespawnTimer > 0)
|
||||
continue;
|
||||
var pose = AetheriaRuntimeDaemonRenderQueries.QueryAsteroidInstancePoses(zone, body.BodyKey, simulationTimeSeconds)
|
||||
.FirstOrDefault(candidate => candidate.AsteroidIndex == intent.TargetAsteroidIndex);
|
||||
var range = Math.Max(0, tool.EvaluateStat(4));
|
||||
if (Math.Pow(pose.PositionX - miner.PositionX, 2) + Math.Pow(pose.PositionZ - miner.PositionZ, 2) > range * range)
|
||||
continue;
|
||||
|
||||
var damage = Math.Max(0, tool.EvaluateStat(1)) * deltaSeconds;
|
||||
var efficiency = Math.Max(0, tool.EvaluateStat(2));
|
||||
var penetration = Math.Max(0.001, tool.EvaluateStat(3));
|
||||
asteroid.Damage += damage;
|
||||
var accumulators = (asteroid.MiningAccumulators ?? Array.Empty<AetheriaRuntimeAsteroidMiningAccumulatorCommit>()).ToList();
|
||||
var accumulator = accumulators.FirstOrDefault(value => value.MinerEntityIndex == minerIndex);
|
||||
if (accumulator == null)
|
||||
{
|
||||
accumulator = new AetheriaRuntimeAsteroidMiningAccumulatorCommit { MinerEntityIndex = minerIndex };
|
||||
accumulators.Add(accumulator);
|
||||
}
|
||||
accumulator.Amount += damage;
|
||||
asteroid.MiningAccumulators = accumulators;
|
||||
|
||||
var resources = (body.Resources ?? Array.Empty<AetheriaRuntimeBodyResourceCommit>()).Where(resource => resource.Amount > 0).ToArray();
|
||||
if (resources.Length > 0)
|
||||
{
|
||||
var resource = resources.OrderByDescending(value => Math.Pow(value.Amount, 1.0 / penetration) * Roll(run.GenerationSeed, frameId, minerIndex, intent.TargetAsteroidIndex, value.ItemKey)).First();
|
||||
if (efficiency * Roll(run.GenerationSeed, frameId, minerIndex, intent.TargetAsteroidIndex, "yield") * accumulator.Amount * resources.Length / MiningDifficulty > 1)
|
||||
{
|
||||
accumulator.Amount = 0;
|
||||
AddCargo(miner, resource.ItemKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (asteroid.Damage >= Hitpoints(asteroid.Size))
|
||||
{
|
||||
asteroid.Damage = 0;
|
||||
asteroid.MiningAccumulators = Array.Empty<AetheriaRuntimeAsteroidMiningAccumulatorCommit>();
|
||||
asteroid.RespawnTimer = RespawnTime(asteroid.Size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCargo(AetheriaRuntimeEntitySnapshotCommit entity, string itemKey)
|
||||
{
|
||||
var bays = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToList();
|
||||
if (bays.Count == 0) bays.Add(new AetheriaRuntimeCargoBayLoadoutCommit());
|
||||
var slots = (bays[0].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
|
||||
var existing = slots.FirstOrDefault(slot => string.Equals(slot?.Item?.ItemKey, itemKey, StringComparison.Ordinal));
|
||||
if (existing != null) existing.Item.Quantity++;
|
||||
else slots.Add(new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = itemKey, Quantity = 1 } });
|
||||
bays[0].Items = slots;
|
||||
entity.CargoContents = bays;
|
||||
}
|
||||
|
||||
private static double Hitpoints(double size) => 10 + 190 * Math.Pow(Clamp((size - 3) / 3), 2);
|
||||
private static double RespawnTime(double size) => 10 + 90 * Math.Pow(Clamp((size - 3) / 3), 1.5);
|
||||
private static double Clamp(double value) => Math.Max(0, Math.Min(1, value));
|
||||
private static double Roll(uint seed, long frame, int miner, int asteroid, string salt)
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
uint hash = seed ^ (uint)frame * 16777619u ^ (uint)miner * 2246822519u ^ (uint)asteroid * 3266489917u;
|
||||
foreach (var c in salt ?? "") hash = (hash ^ c) * 16777619u;
|
||||
hash ^= hash >> 16; hash *= 2246822519u; hash ^= hash >> 13;
|
||||
return (hash + 1.0) / (uint.MaxValue + 2.0);
|
||||
}
|
||||
}
|
||||
private static bool TryEntityIndex(string key, out int value) => int.TryParse((key ?? "").Split('.').LastOrDefault(), out value);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue