Restore native target and interact controls
This commit is contained in:
parent
4f1d490915
commit
2fbb225389
4 changed files with 129 additions and 6 deletions
|
|
@ -183,6 +183,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
InputCapabilityPublishesExactBehaviorLevers,
|
||||
InputCapabilityPublishesConsumableActionBarState,
|
||||
InputCapabilityPublishesHeldWeaponGroupLever,
|
||||
InputCapabilityPublishesNativeTargetAndInteractControls,
|
||||
ReflectorUsesSharedStellarLightFieldWithoutAccumulating,
|
||||
SensorPingIsActorScopedEnergyGatedAndReconnectable,
|
||||
TurretControllerAcquiresAimsAndTriggersExactWeapons,
|
||||
|
|
@ -763,6 +764,88 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
"weapon release must clear the daemon-owned held state");
|
||||
}
|
||||
|
||||
private static void InputCapabilityPublishesNativeTargetAndInteractControls()
|
||||
{
|
||||
var actor = Entity(0, 0, "player");
|
||||
var near = Entity(1, 0, "enemy");
|
||||
near.PositionX = 4;
|
||||
var far = Entity(2, 0, "enemy");
|
||||
far.PositionX = 12;
|
||||
actor.Contacts =
|
||||
[
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{ TargetEntityIndex = near.EntityIndex, Visible = true, Hostile = true },
|
||||
new AetheriaRuntimeEntityContactCommit
|
||||
{ TargetEntityIndex = far.EntityIndex, Visible = true, Hostile = true }
|
||||
];
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "native-input-smoke",
|
||||
CurrentZoneIndex = 0,
|
||||
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [actor, near, far] }]
|
||||
};
|
||||
run.CurrentEntityKey = run.EntityRecordKey(0, actor.EntityIndex);
|
||||
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "native-input", 1, 0.1, 0.1);
|
||||
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
||||
var targeting = capability.Actions.Where(value => value.Category == "targeting").ToArray();
|
||||
Require(targeting.Select(value => value.ActionId).ToHashSet(StringComparer.Ordinal).SetEquals(
|
||||
["pilot.target-nearest", "pilot.target-previous", "pilot.target-next", "pilot.target-clear"]),
|
||||
"portable input must advertise every argumentless daemon target-selection lever");
|
||||
Require(targeting.Where(value => value.ActionId != "pilot.target-clear")
|
||||
.All(value => value.Availability == "available" &&
|
||||
value.Payload["visibleHostileCount"] == "2" &&
|
||||
value.Payload["currentTargetEntityIndex"] == "-1") &&
|
||||
targeting.Single(value => value.ActionId == "pilot.target-clear").Availability == "unavailable",
|
||||
"target controls must derive availability and current selection from daemon contact truth");
|
||||
|
||||
var keyboard = capability.DefaultProfiles.Single(value => value.DeviceClass == "keyboard-mouse");
|
||||
string Control(string actionId) => keyboard.Bindings.Single(value => value.ActionId == actionId).Gesture.Controls.Single();
|
||||
RequireEqual("keyboard.f", Control("pilot.interact"),
|
||||
"the generic default profile must preserve the fossil interaction key");
|
||||
RequireEqual("keyboard.t", Control("pilot.target-nearest"),
|
||||
"the generic default profile must preserve the fossil nearest-target key");
|
||||
RequireEqual("keyboard.y", Control("pilot.target-previous"),
|
||||
"the generic default profile must preserve the fossil previous-target key");
|
||||
RequireEqual("keyboard.u", Control("pilot.target-next"),
|
||||
"the generic default profile must preserve the fossil next-target key");
|
||||
Require(!keyboard.Bindings.Any(value => value.Gesture.Controls.Contains("keyboard.r", StringComparer.Ordinal)),
|
||||
"the advertised profile must not steal the fossil reticle-target key for docking");
|
||||
|
||||
AetheriaRuntimeDaemonCommandDocument Translate(AetheriaRuntimeInputActionDocument action)
|
||||
{
|
||||
var request = new EveSurfaceCommandRequest(
|
||||
capability.ProviderId,
|
||||
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
||||
CultMesh.OperationInvocation(action.Operation),
|
||||
CultMesh.OperationPayload(("entityId", run.CurrentEntityKey)),
|
||||
DateTimeOffset.UtcNow,
|
||||
"native-input-smoke");
|
||||
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
||||
request, frame, ".", "native-input-smoke", "native-input", out var command),
|
||||
$"advertised action {action.ActionId} must translate through the generic Eve command path");
|
||||
return command!;
|
||||
}
|
||||
|
||||
var nearestCommand = Translate(targeting.Single(value => value.ActionId == "pilot.target-nearest"));
|
||||
RequireEqual(AetheriaRuntimeDaemonCommandKinds.TargetNearest, nearestCommand.Kind,
|
||||
"nearest target action must preserve daemon command identity");
|
||||
var result = AetheriaRuntimeDaemonOperations.Execute(
|
||||
run, [nearestCommand], new AetheriaRuntimeDaemonOperationContext());
|
||||
RequireEqual(1, result.AppliedCommandIds.Count,
|
||||
"the advertised target action must reach the daemon owner");
|
||||
RequireEqual(near.EntityIndex, actor.TargetEntityIndex,
|
||||
"nearest selection must remain daemon policy rather than client reconstruction");
|
||||
var selected = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
||||
Require(selected.Actions.Single(value => value.ActionId == "pilot.target-clear").Availability == "available" &&
|
||||
selected.Actions.Where(value => value.Category == "targeting")
|
||||
.All(value => value.Payload["currentTargetEntityIndex"] == near.EntityIndex.ToString()),
|
||||
"all portable target levers must refresh from the newly selected daemon target");
|
||||
|
||||
var interact = capability.Actions.Single(value => value.ActionId == "pilot.interact");
|
||||
RequireEqual(AetheriaRuntimeDaemonCommandKinds.Interact, Translate(interact).Kind,
|
||||
"the fossil interaction binding must reach the shared wormhole-first/then-dock daemon command");
|
||||
}
|
||||
|
||||
private static void DockedRefitSurfacePublishesGenericCommands()
|
||||
{
|
||||
var hull = HullCatalogItem("surface-hull", 9, 7, 100);
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonCommandKinds.SetHeatsinksEnabled,
|
||||
AetheriaRuntimeDaemonCommandKinds.SetOverrideShutdown,
|
||||
AetheriaRuntimeDaemonCommandKinds.ToggleShieldEnabled,
|
||||
AetheriaRuntimeDaemonCommandKinds.Interact,
|
||||
AetheriaRuntimeDaemonCommandKinds.DockNearest,
|
||||
AetheriaRuntimeDaemonCommandKinds.Undock
|
||||
};
|
||||
|
|
@ -81,6 +82,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
AetheriaRuntimeDaemonCommandKinds.SetHeatsinksEnabled => client.SetHeatsinksEnabled(frame, true),
|
||||
AetheriaRuntimeDaemonCommandKinds.SetOverrideShutdown => client.SetOverrideShutdown(frame, true),
|
||||
AetheriaRuntimeDaemonCommandKinds.ToggleShieldEnabled => client.ToggleShieldEnabled(frame),
|
||||
AetheriaRuntimeDaemonCommandKinds.Interact => client.Interact(frame),
|
||||
AetheriaRuntimeDaemonCommandKinds.DockNearest => client.DockNearest(frame),
|
||||
AetheriaRuntimeDaemonCommandKinds.Undock => client.Undock(frame),
|
||||
_ => null
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var run = frame?.Run ?? new AetheriaRuntimeRunCheckpointCommit();
|
||||
var entity = run.Zones.SelectMany(zone => zone.Entities).FirstOrDefault(candidate =>
|
||||
string.Equals(run.EntityRecordKey(run.CurrentZoneIndex, candidate.EntityIndex), run.CurrentEntityKey, StringComparison.Ordinal));
|
||||
var actions = CoreActions().ToList();
|
||||
var actions = CoreActions(entity).ToList();
|
||||
if (includeSimulationClock)
|
||||
{
|
||||
actions.Add(Action("simulation.pause", "Pause", "SetSimulationRate", "simulation", "terminus-clock", ("scalarValue", "0")));
|
||||
|
|
@ -365,7 +365,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
private static string StableToken(string value) =>
|
||||
new string((value ?? "").Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '-').ToArray()).Trim('-');
|
||||
|
||||
private static IEnumerable<AetheriaRuntimeInputActionDocument> CoreActions()
|
||||
private static IEnumerable<AetheriaRuntimeInputActionDocument> CoreActions(
|
||||
AetheriaRuntimeEntitySnapshotCommit? entity)
|
||||
{
|
||||
var scoop = Action("pilot.scoop", "Scoop", "SetTractorPower", "ship", "pilot", ("scalarValue", "0"));
|
||||
scoop.InputValue = new AetheriaRuntimeInputValueDocument
|
||||
|
|
@ -374,9 +375,40 @@ namespace GameCult.Aetheria.State.Verse
|
|||
PayloadKey = "scalarValue"
|
||||
};
|
||||
yield return scoop;
|
||||
yield return Action("pilot.interact", "Interact", "Interact", "ship", "pilot");
|
||||
yield return Action("pilot.dock", "Dock", "DockNearest", "ship", "pilot");
|
||||
yield return Action("pilot.undock", "Undock", "Undock", "ship", "pilot");
|
||||
yield return Action("pilot.target-nearest", "Target Nearest", "TargetNearest", "targeting", "pilot");
|
||||
var visibleHostileCount = (entity?.Contacts ?? Array.Empty<AetheriaRuntimeEntityContactCommit>())
|
||||
.Count(contact => contact != null && contact.Visible && contact.Hostile);
|
||||
var currentTarget = entity?.TargetEntityIndex ?? -1;
|
||||
yield return TargetAction("pilot.target-nearest", "Target Nearest", "TargetNearest",
|
||||
visibleHostileCount > 0, currentTarget, visibleHostileCount);
|
||||
yield return TargetAction("pilot.target-previous", "Target Previous", "TargetPrevious",
|
||||
visibleHostileCount > 0, currentTarget, visibleHostileCount);
|
||||
yield return TargetAction("pilot.target-next", "Target Next", "TargetNext",
|
||||
visibleHostileCount > 0, currentTarget, visibleHostileCount);
|
||||
yield return TargetAction("pilot.target-clear", "Clear Target", "ClearTarget",
|
||||
currentTarget >= 0, currentTarget, visibleHostileCount);
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeInputActionDocument TargetAction(
|
||||
string id,
|
||||
string label,
|
||||
string operation,
|
||||
bool available,
|
||||
int currentTarget,
|
||||
int visibleHostileCount)
|
||||
{
|
||||
var action = Action(
|
||||
id,
|
||||
label,
|
||||
operation,
|
||||
"targeting",
|
||||
"pilot",
|
||||
("currentTargetEntityIndex", currentTarget.ToString(System.Globalization.CultureInfo.InvariantCulture)),
|
||||
("visibleHostileCount", visibleHostileCount.ToString(System.Globalization.CultureInfo.InvariantCulture)));
|
||||
action.Availability = available ? "available" : "unavailable";
|
||||
return action;
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeInputActionDocument Action(string id, string label, string operation, string category, string source, params (string Key, string Value)[] payload) =>
|
||||
|
|
@ -405,8 +437,14 @@ namespace GameCult.Aetheria.State.Verse
|
|||
? Binding("scoop.shift", "pilot.scoop", "direct", "keyboard.leftShift")
|
||||
: Binding("scoop.sequence", "pilot.scoop", "sequence", "gamepad.dpad.down", "gamepad.dpad.right"));
|
||||
bindings.Add(keyboard
|
||||
? Binding("dock.r", "pilot.dock", "direct", "keyboard.r")
|
||||
: Binding("dock.sequence", "pilot.dock", "sequence", "gamepad.dpad.down", "gamepad.dpad.up"));
|
||||
? Binding("interact.f", "pilot.interact", "direct", "keyboard.f")
|
||||
: Binding("interact.sequence", "pilot.interact", "sequence", "gamepad.dpad.down", "gamepad.dpad.up"));
|
||||
if (keyboard)
|
||||
{
|
||||
bindings.Add(Binding("target.nearest.t", "pilot.target-nearest", "direct", "keyboard.t"));
|
||||
bindings.Add(Binding("target.previous.y", "pilot.target-previous", "direct", "keyboard.y"));
|
||||
bindings.Add(Binding("target.next.u", "pilot.target-next", "direct", "keyboard.u"));
|
||||
}
|
||||
if (actions.Any(action => string.Equals(action.ActionId, "simulation.pause", StringComparison.Ordinal)))
|
||||
bindings.Add(keyboard
|
||||
? Binding("simulation.pause", "simulation.pause", "direct", "keyboard.pause")
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Wormhole travel | `ActionGameManager.EnterWormhole` plus `Ship` enter/exit state | daemon fixed-step transition plus Ymir exclusion | current-entity transition state and feedback chronology | provider-selected generic transition lowering | command acceptance no longer moves an entity immediately; player interact, explicit travel, and agent routing derive the same canonical adjacent-zone wormhole and proximity gate, then persist four-second `entering`, authoritative transfer, four-second `exiting`, and one-frame `completed` phases. The daemon owns smootherstep XZ motion, the fossil 1000-unit visual-depth lever, deterministic 20-unit exit direction, 50-unit exit radius, target-adjacency discovery, and exactly-once start/transfer/completion events. Flight and every world-physics implementation receive no transitioning body; Ymir resumes on the tick after completion. Smoke serializes and rehydrates mid-entry and proves agents traverse multi-edge routes through the same command boundary | generic provider transition effect/camera lowering and live multi-zone pilot witness |
|
||||
| Inventory/equipment | `Entity.ItemFits`, `TryEquip`, `TryUnequip`, `EquippedCargoBay.ItemFits`/`TryStore`, inventory panels + ServerShared | daemon atomic inventory transactions | typed inventory/equipment state, commands, exact receipts | generic UI | generated cargo and docking bays now occupy their typed collections instead of masquerading as ordinary equipment; docked equip/reposition/store validates the complete destination on cloned state before committing either side, uses authored hull interior and hardpoint geometry, forces hardpoint rotation, rejects occupied cells and deployed ships, preserves exact item instance fields and weapon-group membership, routes bay categories with aligned content/assignment arrays, and refuses to unequip nonempty/occupied bays. Cargo transfer now uses the same plan-first rule with typed bay interiors, rotated item footprints, deterministic free-cell search, exact stack maxima, and no partial source/destination mutation on rejection; exact reasons reach generic Eve receipts | live generic refit controls, reconnect after accepted and rejected moves, commodity multi-stack chronology, and full fossil inventory feedback parity |
|
||||
| Trade | trade UI/economy | daemon | stock, quote, purchase/sale commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; purchases ignore client opinions about price and destination kind. Docked sales identify only the source stack and quantity: the daemon derives station, instance-sensitive value, payout, deterministic typed stock placement, and commits cargo/credits atomically; generic Eve advertises available buy and sell actions. The generic Unity live witness receives an authoritative purchase receipt while paused | live sale receipt witness, quote expiry, contested stock, and full trade surface parity |
|
||||
| Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; consumable rows are catalog-filtered, expose daemon-owned cargo quantity, active-effect count, exact duration/remaining duration, and the fossil `remaining / duration` fill as `remaining-ratio.v1`. A depleted or non-stackable active consumable remains visible with unavailable command state until its effect ends, so a lowerer never queries Unity gameplay objects. Weapon groups advertise the fossil press/release lifecycle as generic `button-hold.v1` values routed to `SetWeaponGroupActive`; daemon entity state persists the active-group latch, press edges pulse instant/charged weapons once, constant weapons remain selected without client keepalives, release clears the latch, and the one-shot compatibility command is not the public input contract. Equipment actions derive from exact installed catalog payload/state pairs instead of blindly activating behavior zero: Switch and Trigger expose generic hold values, adjustable Thermotoggle exposes a scalar Kelvin target/current value, fixed/non-interactive rows are omitted, and offline rows retain identity as unavailable. Every value form translates through generic Eve requests into typed daemon commands under provider identity `aetheria` | generic scalar-control and action-bar presentation lowering |
|
||||
| Action bar/input | input shell and rebinding UI | daemon advertises actions; client owns bindings | input capability and action descriptors | universal binding UI | contracts wired; consumable rows are catalog-filtered, expose daemon-owned cargo quantity, active-effect count, exact duration/remaining duration, and the fossil `remaining / duration` fill as `remaining-ratio.v1`. A depleted or non-stackable active consumable remains visible with unavailable command state until its effect ends, so a lowerer never queries Unity gameplay objects. Weapon groups advertise the fossil press/release lifecycle as generic `button-hold.v1` values routed to `SetWeaponGroupActive`; daemon entity state persists the active-group latch, press edges pulse instant/charged weapons once, constant weapons remain selected without client keepalives, release clears the latch, and the one-shot compatibility command is not the public input contract. Equipment actions derive from exact installed catalog payload/state pairs instead of blindly activating behavior zero: Switch and Trigger expose generic hold values, adjustable Thermotoggle exposes a scalar Kelvin target/current value, fixed/non-interactive rows are omitted, and offline rows retain identity as unavailable. Target nearest/previous/next/clear actions expose daemon-owned current selection, visible-hostile count, and availability; the default profile preserves fossil `T/Y/U`, restores the shared wormhole-first/then-dock interaction to `F`, and no longer steals the fossil `R` reticle key for docking. Every value form translates through generic Eve requests into typed daemon commands under provider identity `aetheria` | generic view-direction input model for reticle targeting, scalar-control lowering, and action-bar presentation lowering |
|
||||
| Pilot camera | ARPG follow/dock cameras | Eve view semantics + runtime | camera rig and semantic view id | EveUnity camera rig | daemon dock state selects distinct contracts instead of collapsing both fossil cameras: undocked flight advertises the entity-forward `Third Person Rig` framing (distance 30, screen 0.64/0.19, FOV 60, lens 1-4096) and the shared `aim.presentation` convergence point as its look-at; Aetheria translates the fossil Cinemachine top-origin screen Y of `0.81` to Eve's bottom-origin viewport Y of `0.19`; EveUnity solves native pose while preserving both optical aim and advertised entity framing, without provider types; the retained dock projection remains separate | reproduce the dock camera's separate follow/look-at subjects, blend timeline, noise, and reconnect state |
|
||||
| Render channels | Unity layers/culling masks | provider surface policy + runtime asset variant | semantic channel exclusions | EveUnity native layer mapping | wired, exclusive, and lowered: the pilot surface excludes the semantic `map` channel; the Unity asset variant maps `map` to its authored native layer; EveUnity subtracts that layer from the configured camera mask; portable state contains no Unity layer or mask; provider conversion preserves the source icon shader's unlit lighting model; the released witness proves the pilot camera excludes map glyphs while the map camera retains readable white glyphs without a scene light | preserve the channel proof while world presentation and transport continue to evolve |
|
||||
| Minimap/tactical map | `ZoneRenderer`, map cameras | daemon world facts | explicit map projections | Unity and Electron | facts fragmented; generic lowerers absent | same state in both runtimes |
|
||||
|
|
|
|||
Loading…
Reference in a new issue