Preserve generated equipment rotation
This commit is contained in:
parent
f8460a3a05
commit
01f0d69b8d
15 changed files with 78 additions and 20 deletions
|
|
@ -1561,7 +1561,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
availableHull.Hardpoints =
|
||||
[
|
||||
new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0),
|
||||
new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "None", 0),
|
||||
new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "Clockwise", 0),
|
||||
new AetheriaRuntimeHardpoint("Sensors", 2, 0, 1, 1, 1, one, "", "None", 0)
|
||||
];
|
||||
var unavailableHull = Item("cheap-foreign-hull", AetheriaRuntimeItemCategories.Hull, "foreign", 1);
|
||||
|
|
@ -1637,6 +1637,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
first.Equipment.Any(value => value.ItemKey == "ship-sensor") &&
|
||||
first.Equipment.All(value => value.ItemKey != "station-sensor"),
|
||||
"generated ships must fit hardpoint equipment and reject station-sized sensor gear");
|
||||
RequireEqual("Clockwise", first.Equipment.Single(value => value.ItemKey == "cannon").Rotation,
|
||||
"generated equipment must preserve the placement rotation used by hardpoint fitting");
|
||||
Require(first.HullItemKey == second.HullItemKey &&
|
||||
first.Equipment.Select(value => value.ItemKey).SequenceEqual(second.Equipment.Select(value => value.ItemKey)),
|
||||
"same seed, map, faction and catalog must produce the same loadout");
|
||||
|
|
|
|||
|
|
@ -66,8 +66,9 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
continue;
|
||||
}
|
||||
|
||||
slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey));
|
||||
Reserve(occupied, item, hardpoint.PositionX, hardpoint.PositionY, ParseRotation(hardpoint.Rotation));
|
||||
var rotation = ParseRotation(hardpoint.Rotation);
|
||||
slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey, rotation));
|
||||
Reserve(occupied, item, hardpoint.PositionX, hardpoint.PositionY, rotation);
|
||||
previous.Add(item);
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +142,7 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
if (selected.Item == null || !selected.Fit.HasValue)
|
||||
throw new InvalidOperationException("No available mandatory equipment fits the remaining hull space.");
|
||||
var fit = selected.Fit.Value;
|
||||
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey));
|
||||
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation));
|
||||
Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation);
|
||||
return selected.Item;
|
||||
}
|
||||
|
|
@ -163,7 +164,7 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
if (selected.Item == null || !selected.Fit.HasValue)
|
||||
return null;
|
||||
var fit = selected.Fit.Value;
|
||||
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey));
|
||||
slots.Add(Slot(fit.X, fit.Y, selected.Item.ItemKey, fit.Rotation));
|
||||
Reserve(occupied, selected.Item, fit.X, fit.Y, fit.Rotation);
|
||||
return selected.Item;
|
||||
}
|
||||
|
|
@ -201,7 +202,7 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
var fit = FindFit(interiorCells, width, height, item, occupied);
|
||||
if (!fit.HasValue) continue;
|
||||
Reserve(occupied, item, fit.Value.X, fit.Value.Y, fit.Value.Rotation);
|
||||
packed.Add(CargoSlot(fit.Value.X, fit.Value.Y, item.ItemKey));
|
||||
packed.Add(CargoSlot(fit.Value.X, fit.Value.Y, item.ItemKey, fit.Value.Rotation));
|
||||
}
|
||||
return packed.ToArray();
|
||||
}
|
||||
|
|
@ -362,6 +363,14 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
_ => 0
|
||||
};
|
||||
|
||||
private static string RotationName(int rotation) => (((rotation % 4) + 4) % 4) switch
|
||||
{
|
||||
1 => "Clockwise",
|
||||
2 => "Half",
|
||||
3 => "CounterClockwise",
|
||||
_ => "None"
|
||||
};
|
||||
|
||||
private static bool HasBehavior(AetheriaRuntimeCatalogItem item, string kind) =>
|
||||
(item.BehaviorKinds ?? Array.Empty<string>()).Contains(kind, StringComparer.Ordinal);
|
||||
|
||||
|
|
@ -375,19 +384,21 @@ public sealed class AetheriaDaemonLoadoutGenerator
|
|||
(item.BehaviorKinds ?? Array.Empty<string>()).Any(kind =>
|
||||
kind.Contains("Weapon", StringComparison.Ordinal)));
|
||||
|
||||
private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey) => new()
|
||||
private static AetheriaEntityItemSlot Slot(int x, int y, string itemKey, int rotation = 0) => new()
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = x, Y = y }, ItemKey = itemKey,
|
||||
Quality = 1, Durability = 1, Quantity = 1, Enabled = true
|
||||
Quality = 1, Durability = 1, Quantity = 1, Enabled = true,
|
||||
Rotation = RotationName(rotation)
|
||||
};
|
||||
|
||||
private static AetheriaLoadoutItemSlot CargoSlot(int index, string itemKey) =>
|
||||
CargoSlot(index % 4, index / 4, itemKey);
|
||||
|
||||
private static AetheriaLoadoutItemSlot CargoSlot(int x, int y, string itemKey) => new()
|
||||
private static AetheriaLoadoutItemSlot CargoSlot(int x, int y, string itemKey, int rotation = 0) => new()
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = x, Y = y },
|
||||
Item = new AetheriaLoadoutItem { ItemKey = itemKey, Quality = 1, Durability = 1, Quantity = 1, Enabled = true }
|
||||
Item = new AetheriaLoadoutItem { ItemKey = itemKey, Quality = 1, Durability = 1, Quantity = 1, Enabled = true },
|
||||
Rotation = RotationName(rotation)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2916,6 +2916,7 @@ static AetheriaRuntimeLoadoutItemSlotCommit[] ToEntitySlotCommits(
|
|||
{
|
||||
X = slot.Position?.X ?? 0,
|
||||
Y = slot.Position?.Y ?? 0,
|
||||
Rotation = slot.Rotation ?? "None",
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = slot.ItemKey ?? "",
|
||||
|
|
@ -2996,6 +2997,7 @@ static AetheriaRuntimeLoadoutItemSlotCommit[] ToSlotCommits(
|
|||
{
|
||||
X = slot.Position?.X ?? 0,
|
||||
Y = slot.Position?.Y ?? 0,
|
||||
Rotation = slot.Rotation ?? "None",
|
||||
Item = ToLoadoutItemCommit(slot.Item)
|
||||
})
|
||||
.ToArray();
|
||||
|
|
|
|||
|
|
@ -248,6 +248,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
new AetheriaLoadoutItemSlot
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = 0, Y = 0 },
|
||||
Rotation = "Clockwise",
|
||||
Item = new AetheriaLoadoutItem
|
||||
{
|
||||
ItemKey = itemKey.ToString(),
|
||||
|
|
@ -275,6 +276,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
new AetheriaEntityItemSlot
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = 0, Y = 0 },
|
||||
Rotation = "CounterClockwise",
|
||||
ItemKey = itemKey.ToString(),
|
||||
Quality = 0.9,
|
||||
Durability = 0.8,
|
||||
|
|
@ -292,6 +294,7 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s
|
|||
new AetheriaLoadoutItemSlot
|
||||
{
|
||||
Position = new AetheriaGridCoord { X = 2, Y = 3 },
|
||||
Rotation = "Half",
|
||||
Item = new AetheriaLoadoutItem
|
||||
{
|
||||
ItemKey = itemKey.ToString(),
|
||||
|
|
@ -851,6 +854,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
|
|||
|
||||
if (loadout?.RootEntity.Hull.ItemKey != AetheriaCatalogKeys.ItemDefinitionFromLegacyId(itemLegacyId).ToString() ||
|
||||
loadout.RootEntity.Equipment.Length != 1 ||
|
||||
loadout.RootEntity.Equipment[0].Rotation != "Clockwise" ||
|
||||
loadout.RootEntity.Equipment[0].Item.Enabled ||
|
||||
!loadout.RootEntity.Equipment[0].Item.OverrideShutdown ||
|
||||
loadout.RootEntity.WeaponGroups.Length != 1)
|
||||
|
|
@ -890,10 +894,12 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether
|
|||
entitySnapshot.Equipment[0].Quality != 0.9 ||
|
||||
entitySnapshot.Equipment[0].Durability != 0.8 ||
|
||||
entitySnapshot.Equipment[0].Quantity != 1 ||
|
||||
entitySnapshot.Equipment[0].Rotation != "CounterClockwise" ||
|
||||
entitySnapshot.Equipment[0].Enabled ||
|
||||
!entitySnapshot.Equipment[0].OverrideShutdown ||
|
||||
entitySnapshot.CargoContents.Length != 1 ||
|
||||
entitySnapshot.CargoContents[0].Items[0].Item.Quantity != 7 ||
|
||||
entitySnapshot.CargoContents[0].Items[0].Rotation != "Half" ||
|
||||
entitySnapshot.DockingBayAssignments.Length != 1 ||
|
||||
entitySnapshot.DockingBayAssignments[0] != -1 ||
|
||||
entitySnapshot.Visibility != 12.75 ||
|
||||
|
|
|
|||
|
|
@ -1275,6 +1275,8 @@ static void RequireDaemonPlayableRunGenerationAuthority(string root)
|
|||
"HasBehavior(candidate, controllerKind)",
|
||||
"for (var draw = 0; draw < 16 && pool.Count > 0; draw++)",
|
||||
"FindFit(interiorCells, width, height, item, occupied)",
|
||||
"slots.Add(Slot(hardpoint.PositionX, hardpoint.PositionY, item.ItemKey, rotation))",
|
||||
"Rotation = RotationName(rotation)",
|
||||
"AetheriaLoadoutGenerationReceipt",
|
||||
"Selections = selectedKeys.Select"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ public static class AetheriaRuntimeStateMapper
|
|||
X = slot.X,
|
||||
Y = slot.Y
|
||||
},
|
||||
Rotation = slot.Rotation ?? "None",
|
||||
Item = ToLoadoutItem(slot.Item)
|
||||
})
|
||||
.ToArray();
|
||||
|
|
|
|||
|
|
@ -190,6 +190,9 @@ public sealed class AetheriaLoadoutItemSlot
|
|||
|
||||
[Key(1)]
|
||||
public AetheriaLoadoutItem Item { get; set; } = new();
|
||||
|
||||
[Key(2)]
|
||||
public string Rotation { get; set; } = "None";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -681,6 +684,9 @@ public sealed class AetheriaEntityItemSlot
|
|||
|
||||
[Key(7)]
|
||||
public double Temperature { get; set; }
|
||||
|
||||
[Key(8)]
|
||||
public string Rotation { get; set; } = "None";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
private static AetheriaRuntimeLoadoutItemSlotCommit Clone(AetheriaRuntimeLoadoutItemSlotCommit source, int quantity) => new()
|
||||
{
|
||||
X = source.X, Y = source.Y,
|
||||
X = source.X, Y = source.Y, Rotation = source.Rotation ?? "None",
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = source.Item.ItemKey, Quality = source.Item.Quality, Durability = source.Item.Durability,
|
||||
|
|
|
|||
|
|
@ -845,16 +845,22 @@ namespace GameCult.Aetheria.State.Verse
|
|||
[MessagePackObject(true)]
|
||||
public sealed class AetheriaRuntimeLoadoutItemSlotSnapshot
|
||||
{
|
||||
public AetheriaRuntimeLoadoutItemSlotSnapshot(int x, int y, AetheriaRuntimeLoadoutItemSnapshot item)
|
||||
public AetheriaRuntimeLoadoutItemSlotSnapshot(
|
||||
int x,
|
||||
int y,
|
||||
AetheriaRuntimeLoadoutItemSnapshot item,
|
||||
string rotation = "None")
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Item = item;
|
||||
Rotation = rotation ?? "None";
|
||||
}
|
||||
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
public AetheriaRuntimeLoadoutItemSnapshot Item { get; set; }
|
||||
public string Rotation { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject(true)]
|
||||
|
|
@ -1017,7 +1023,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
int quantity,
|
||||
bool enabled,
|
||||
bool overrideShutdown,
|
||||
double temperature = 0)
|
||||
double temperature = 0,
|
||||
string rotation = "None")
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
|
|
@ -1028,6 +1035,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Enabled = enabled;
|
||||
OverrideShutdown = overrideShutdown;
|
||||
Temperature = temperature;
|
||||
Rotation = rotation ?? "None";
|
||||
}
|
||||
|
||||
public int X { get; set; }
|
||||
|
|
@ -1039,6 +1047,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public bool Enabled { get; set; }
|
||||
public bool OverrideShutdown { get; set; }
|
||||
public double Temperature { get; set; }
|
||||
public string Rotation { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject(true)]
|
||||
|
|
|
|||
|
|
@ -954,7 +954,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var enabled = ReadFieldBool(ref reader, slotFields, 5, true);
|
||||
var overrideShutdown = ReadFieldBool(ref reader, slotFields, 6);
|
||||
var temperature = ReadFieldDouble(ref reader, slotFields, 7);
|
||||
SkipRemaining(ref reader, slotFields, 8);
|
||||
var rotation = ReadFieldString(ref reader, slotFields, 8);
|
||||
if (string.IsNullOrWhiteSpace(rotation)) rotation = "None";
|
||||
SkipRemaining(ref reader, slotFields, 9);
|
||||
slots[slot] = new AetheriaRuntimeEntityItemSlotSnapshot(
|
||||
position.X,
|
||||
position.Y,
|
||||
|
|
@ -964,7 +966,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
quantity <= 0 ? 1 : quantity,
|
||||
enabled,
|
||||
overrideShutdown,
|
||||
temperature);
|
||||
temperature,
|
||||
rotation);
|
||||
}
|
||||
|
||||
return slots;
|
||||
|
|
@ -1532,8 +1535,10 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var slotFields = reader.ReadArrayHeader();
|
||||
var position = ReadFieldGridCoord(ref reader, slotFields, 0);
|
||||
var item = ReadFieldLoadoutItem(ref reader, slotFields, 1);
|
||||
SkipRemaining(ref reader, slotFields, 2);
|
||||
return new AetheriaRuntimeLoadoutItemSlotSnapshot(position.X, position.Y, item);
|
||||
var rotation = ReadFieldString(ref reader, slotFields, 2);
|
||||
if (string.IsNullOrWhiteSpace(rotation)) rotation = "None";
|
||||
SkipRemaining(ref reader, slotFields, 3);
|
||||
return new AetheriaRuntimeLoadoutItemSlotSnapshot(position.X, position.Y, item, rotation);
|
||||
}
|
||||
|
||||
private static GridCoord ReadFieldGridCoord(ref MessagePackReader reader, int fields, int index)
|
||||
|
|
|
|||
|
|
@ -1983,6 +1983,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
X = slot?.X ?? 0,
|
||||
Y = slot?.Y ?? 0,
|
||||
Rotation = slot?.Rotation ?? "None",
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit
|
||||
{
|
||||
ItemKey = item?.ItemKey ?? "",
|
||||
|
|
|
|||
|
|
@ -1031,7 +1031,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
slot.Item.Quantity,
|
||||
slot.Item.Enabled,
|
||||
slot.Item.OverrideShutdown,
|
||||
slot.Item.Temperature))
|
||||
slot.Item.Temperature,
|
||||
slot.Rotation ?? "None"))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
|
@ -1059,7 +1060,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
slot.Item.Quantity,
|
||||
slot.Item.Enabled,
|
||||
slot.Item.OverrideShutdown,
|
||||
slot.Item.Temperature)))
|
||||
slot.Item.Temperature),
|
||||
slot.Rotation ?? "None"))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
[Key(2)]
|
||||
public AetheriaRuntimeLoadoutItemCommit Item { get; set; } = new AetheriaRuntimeLoadoutItemCommit();
|
||||
|
||||
[Key(3)]
|
||||
public string Rotation { get; set; } = "None";
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
|
|
@ -420,6 +423,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
X = slot.X,
|
||||
Y = slot.Y,
|
||||
Rotation = slot.Rotation ?? "None",
|
||||
Item = CloneItem(slot.Item)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1246,6 +1246,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
|
|||
before hydrating row projections. No live caller can enumerate
|
||||
legacy catalog entries or request catalog-shaped single entries through
|
||||
`ItemManager`.
|
||||
Equipment placement rotation is durable semantic state, not an occupancy-loop
|
||||
temporary. The daemon generator emits the same quarter-turn used to reserve a
|
||||
hardpoint, free-space fit, or cargo fit; loadout documents, runtime commits,
|
||||
catalog and viewport snapshots, state mapping, and clone/transfer paths retain
|
||||
it. This is the required input for daemon-owned thruster direction and torque;
|
||||
Unity may visualize the resulting actuator state but cannot reconstruct a
|
||||
missing rotation from prefab transforms.
|
||||
- Inputs: Unity gameplay code, legacy catalog files, typed state documents, and
|
||||
CultMesh server state.
|
||||
- Outputs: `Aetheria.State` emits `.cc` state and CultMesh documents. Runtime
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
|---|---|---|---|---|---|---|
|
||||
| Run/session state | `ServerShared`, `ActionGameManager` | Aetheria daemon/CultCache | provider/session advertisement | all clients | owner-exists; wired; projected; not proven | restart and multi-client convergence |
|
||||
| Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | owner-exists; wired; projected; not proven | loadout round trip and reconnect |
|
||||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role; generation fills the largest compatible footprint before applying economic weighting, so small gear remains a valid large-mount fallback without displacing available station-scale gear; cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; ordinary ships begin without generated cargo, and the deleted scenario-string path can no longer silently invent non-catalog item identities; station cargo slots and receipts preserve the same canonical keys; zone generation derives one continuing deterministic RNG stream per availability corporation, so cross-faction calls cannot perturb another faction; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil |
|
||||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role; the exact placement rotation used by hardpoint/free-space/cargo fitting now survives generated state, durable loadouts, runtime commits, viewport snapshots, cloning, and reconnect instead of disappearing after occupancy reservation; generation fills the largest compatible footprint before applying economic weighting, so small gear remains a valid large-mount fallback without displacing available station-scale gear; cargo/capacitor plus station docking equipment must fit remaining hull space; stations draw up to sixteen faction-available equipment rows and pack largest-first into the selected cargo bay's actual interior shape; ordinary ships begin without generated cargo, and the deleted scenario-string path can no longer silently invent non-catalog item identities; station cargo slots and receipts preserve the same canonical keys; zone generation derives one continuing deterministic RNG stream per availability corporation, so cross-faction calls cannot perturb another faction; every generated entity persists an immutable source/selection receipt that reaches Eve as `loadout.item` semantics; Terminus explicitly maps combat-role factions to catalog availability corporations | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil |
|
||||
| Flight intent | `Ship`, `Thruster`, `AetherDrive`, input paths | daemon intent + Ymir physics | analog movement/look/tractor | EveUnity input driver | movement intents feed Ymir-owned body integration; authored thrust allocation, torque, energy and heat remain simplified | prove original drive/equipment behavior ordering and runtime interpolation |
|
||||
| Collision | Unity physics and entity handlers | Ymir | collision/result facts | visual interpolation only | Ymir.Core world stepping wired for entity bodies, radial fields and contacts; missing-owner negative proof passes | publish authoritative collision events and prove Unity cannot write transforms back |
|
||||
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | simplified owner wired/projected; every authored instant lock behavior now evaluates on every daemon step independently of trigger/burst selection, so an idle weapon cannot retain a stale completed lock; acquisition and decay derive from facing, elapsed time, and observer-local contact confidence instead of granting an immediate perfect lock; target changes and invalid targets clear the daemon state; `weapon.lock.started`, `weapon.lock.acquired`, and `weapon.lock.lost` publish stable one-time chronology with target, weapon identity, reason, and prior/current progress through the generic feedback stream; the provider cockpit and generic combat reticle consume the same continuous selected-target lock without client smoothing or reconstruction | angular loss fixture, visibility/nearest/explicit targeting cases, provider audio/effect roles, and authored reticle parity |
|
||||
|
|
|
|||
Loading…
Reference in a new issue