Lower interactive worlds in the browser
This commit is contained in:
parent
9660ecb126
commit
7a9493b67d
6 changed files with 429 additions and 1 deletions
|
|
@ -328,6 +328,14 @@ Recently cut:
|
|||
next ordered snapshot request buffered forever. CultNet TS unit tests, the
|
||||
CultMesh TS suite, Aetheria's local Stage 7C verifier, and its full Electron
|
||||
verifier all pass with the repaired protocol.
|
||||
- The browser lowerer now gives `world.scene3d` / `world.entity3d` a generic
|
||||
tactical projection. It normalizes provider-authored X/Z poses into a stable
|
||||
viewport, distinguishes controlled and faction markers, and maps target,
|
||||
movement, focus, and action input into provider-routed Eve command intents.
|
||||
It does not advance poses or simulate combat locally. Aetheria's Electron
|
||||
witness requires the Eve-owned world scene, provider entity markers, and one
|
||||
controlled marker in the rendered DOM, so surrounding HUD text cannot stand
|
||||
in for interactive-world lowering proof.
|
||||
|
||||
- The clean EveUnity PlayMode client now consumes Aetheria's independently
|
||||
running daemon without importing Aetheria code. Aetheria builds its authored
|
||||
|
|
|
|||
12
packages/eve-browser-lowering/dist/index.d.ts
vendored
12
packages/eve-browser-lowering/dist/index.d.ts
vendored
|
|
@ -118,8 +118,20 @@ export interface EveResolvedDocument {
|
|||
schemaId?: string;
|
||||
surface?: EveSurfaceDocument["surface"];
|
||||
}
|
||||
export interface EveProjectedWorldEntity {
|
||||
entityId: string;
|
||||
faction: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
controlled: boolean;
|
||||
xPercent: number;
|
||||
yPercent: number;
|
||||
source: EveSurfaceComponent;
|
||||
}
|
||||
export declare function renderEveSurface(surface: EveSurfaceDocument, host: HTMLElement, options?: EveBrowserLoweringOptions): HTMLElement;
|
||||
export declare function renderEveComponent(node: EveSurfaceComponent, options?: EveBrowserLoweringOptions): HTMLElement;
|
||||
export declare function projectWorldScene(node: EveSurfaceComponent): EveProjectedWorldEntity[];
|
||||
export declare function createWorldActionIntent(command: string, action: Record<string, unknown>, options?: EveBrowserLoweringOptions): EveCommandIntent;
|
||||
export declare function applyEveSurfaceStyles(styles: EveSurfaceStyles | undefined, body?: HTMLElement): void;
|
||||
export declare function createEveCommandIntent(commandId: string, props?: Record<string, unknown>, options?: EveBrowserLoweringOptions): EveCommandIntent;
|
||||
export declare function emptyState(message: string): HTMLElement;
|
||||
|
|
|
|||
128
packages/eve-browser-lowering/dist/index.js
vendored
128
packages/eve-browser-lowering/dist/index.js
vendored
|
|
@ -201,6 +201,9 @@ export function renderEveComponent(node, options = currentOptions) {
|
|||
if (kind === "field.surface2d" || kind === "gravity.surface") {
|
||||
return renderGravitySurface(node, props, layout, style, options);
|
||||
}
|
||||
if (kind === "world.scene3d") {
|
||||
return renderWorldScene(node, props, layout, style, options);
|
||||
}
|
||||
if (kind === "image.background") {
|
||||
const view = el("div", "cultui-background");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
|
|
@ -667,6 +670,131 @@ function renderGravitySurface(node, props, layout, style, options) {
|
|||
window.setTimeout(scheduleDraw, 1000);
|
||||
return canvas;
|
||||
}
|
||||
export function projectWorldScene(node) {
|
||||
const entities = (node.children || []).filter(child => child.kind === "world.entity3d");
|
||||
const positions = entities.map(entity => vector3Prop(objectProps(entity.props).position, [0, 0, 0]));
|
||||
const xs = positions.map(position => position[0]);
|
||||
const zs = positions.map(position => position[2]);
|
||||
const minX = Math.min(...xs, -1);
|
||||
const maxX = Math.max(...xs, 1);
|
||||
const minZ = Math.min(...zs, -1);
|
||||
const maxZ = Math.max(...zs, 1);
|
||||
const spanX = Math.max(1, maxX - minX);
|
||||
const spanZ = Math.max(1, maxZ - minZ);
|
||||
const padding = 6;
|
||||
return entities.map((entity, index) => {
|
||||
const props = objectProps(entity.props);
|
||||
return {
|
||||
entityId: firstString(props.entityId, entity.id),
|
||||
faction: stringProp(props.faction, "neutral"),
|
||||
kind: stringProp(props.entityKind, "entity"),
|
||||
label: firstString(props.label, props.entityId, entity.id, "Entity"),
|
||||
controlled: boolProp(props.controllable),
|
||||
xPercent: padding + ((positions[index][0] - minX) / spanX) * (100 - padding * 2),
|
||||
yPercent: padding + (1 - ((positions[index][2] - minZ) / spanZ)) * (100 - padding * 2),
|
||||
source: entity,
|
||||
};
|
||||
});
|
||||
}
|
||||
function renderWorldScene(node, props, layout, style, options) {
|
||||
const scene = el("section", "cultui-world-scene");
|
||||
assignId(scene, node);
|
||||
applyGeneratedLayout(scene, layout, style);
|
||||
scene.tabIndex = 0;
|
||||
scene.setAttribute("aria-label", stringProp(props.label, "Interactive world"));
|
||||
const entities = projectWorldScene(node);
|
||||
const controlled = entities.find(entity => entity.controlled);
|
||||
const heading = el("div", "cultui-world-heading");
|
||||
heading.append(el("strong", "", stringProp(props.label, "World")));
|
||||
heading.append(el("span", "", `${entities.length} entities`));
|
||||
scene.append(heading);
|
||||
const plane = el("div", "cultui-world-plane");
|
||||
for (const entity of entities) {
|
||||
const marker = el("button", `cultui-world-entity faction-${cssIdentifier(entity.faction)} kind-${cssIdentifier(entity.kind)}`);
|
||||
marker.type = "button";
|
||||
marker.style.left = `${entity.xPercent}%`;
|
||||
marker.style.top = `${entity.yPercent}%`;
|
||||
marker.dataset.entityId = entity.entityId;
|
||||
marker.dataset.controlled = entity.controlled ? "true" : "false";
|
||||
marker.title = entity.label;
|
||||
marker.setAttribute("aria-label", `${entity.label}, ${entity.kind}, ${entity.faction}`);
|
||||
marker.append(el("span", "cultui-world-entity-glyph", worldEntityGlyph(entity.kind)));
|
||||
marker.append(el("span", "cultui-world-entity-label", entity.label));
|
||||
const entityProps = objectProps(entity.source.props);
|
||||
const targetCommand = firstString(entityProps.targetCommand, props.targetCommand, entityProps.focusCommand, props.focusCommand);
|
||||
if (targetCommand) {
|
||||
marker.addEventListener("click", () => emitWorldCommand(targetCommand, {
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
entityId: entity.entityId,
|
||||
targetEntityId: entity.entityId,
|
||||
targetEntityKey: entity.entityId,
|
||||
}, entity.source, options));
|
||||
}
|
||||
plane.append(marker);
|
||||
}
|
||||
scene.append(plane);
|
||||
scene.addEventListener("keydown", event => {
|
||||
const direction = worldDirectionForKey(event.key);
|
||||
if (direction) {
|
||||
event.preventDefault();
|
||||
const command = firstString(props.movementCommand, objectProps(controlled?.source.props).moveCommand);
|
||||
if (command)
|
||||
emitWorldCommand(command, {
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
actorEntityKey: controlled?.entityId || firstString(props.playerEntityId),
|
||||
directionX: direction[0],
|
||||
directionY: direction[1],
|
||||
scalar: 1,
|
||||
}, node, options);
|
||||
return;
|
||||
}
|
||||
const command = event.key === " "
|
||||
? firstString(props.actionCommand, objectProps(controlled?.source.props).actionCommand)
|
||||
: event.key.toLowerCase() === "f"
|
||||
? firstString(props.focusCommand, objectProps(controlled?.source.props).focusCommand)
|
||||
: "";
|
||||
if (command) {
|
||||
event.preventDefault();
|
||||
emitWorldCommand(command, {
|
||||
actionId: event.key === " " ? "0" : "focus",
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
actorEntityKey: controlled?.entityId || firstString(props.playerEntityId),
|
||||
}, node, options);
|
||||
}
|
||||
});
|
||||
return scene;
|
||||
}
|
||||
function emitWorldCommand(command, action, node, options) {
|
||||
void options.commandSink?.(createWorldActionIntent(command, action, options), node);
|
||||
}
|
||||
export function createWorldActionIntent(command, action, options = currentOptions) {
|
||||
return createEveCommandIntent(command, { action }, options);
|
||||
}
|
||||
function worldDirectionForKey(key) {
|
||||
switch (key.toLowerCase()) {
|
||||
case "w":
|
||||
case "arrowup": return [0, 1];
|
||||
case "s":
|
||||
case "arrowdown": return [0, -1];
|
||||
case "a":
|
||||
case "arrowleft": return [-1, 0];
|
||||
case "d":
|
||||
case "arrowright": return [1, 0];
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
function worldEntityGlyph(kind) {
|
||||
if (kind.includes("station"))
|
||||
return "S";
|
||||
if (kind.includes("projectile"))
|
||||
return ".";
|
||||
if (kind.includes("ship"))
|
||||
return "^";
|
||||
return "+";
|
||||
}
|
||||
function cssIdentifier(value) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
||||
}
|
||||
async function resolveGravitySurfaceDocuments(node, props, options, state) {
|
||||
if (!options.documentResolver || state.loading)
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -185,6 +185,17 @@ export interface EveResolvedDocument {
|
|||
surface?: EveSurfaceDocument["surface"];
|
||||
}
|
||||
|
||||
export interface EveProjectedWorldEntity {
|
||||
entityId: string;
|
||||
faction: string;
|
||||
kind: string;
|
||||
label: string;
|
||||
controlled: boolean;
|
||||
xPercent: number;
|
||||
yPercent: number;
|
||||
source: EveSurfaceComponent;
|
||||
}
|
||||
|
||||
const defaultStyleTokens: Record<string, string> = {
|
||||
"--bg": "#020909",
|
||||
"--bg-top": "#020909",
|
||||
|
|
@ -329,6 +340,10 @@ export function renderEveComponent(
|
|||
return renderGravitySurface(node, props, layout, style, options);
|
||||
}
|
||||
|
||||
if (kind === "world.scene3d") {
|
||||
return renderWorldScene(node, props, layout, style, options);
|
||||
}
|
||||
|
||||
if (kind === "image.background") {
|
||||
const view = el("div", "cultui-background");
|
||||
const src = resolveAssetUrl(firstString(props.src, props.assetUri, props.assetRef, ""));
|
||||
|
|
@ -815,6 +830,148 @@ function renderGravitySurface(
|
|||
return canvas;
|
||||
}
|
||||
|
||||
export function projectWorldScene(node: EveSurfaceComponent): EveProjectedWorldEntity[] {
|
||||
const entities = (node.children || []).filter(child => child.kind === "world.entity3d");
|
||||
const positions = entities.map(entity => vector3Prop(objectProps(entity.props).position, [0, 0, 0]));
|
||||
const xs = positions.map(position => position[0]);
|
||||
const zs = positions.map(position => position[2]);
|
||||
const minX = Math.min(...xs, -1);
|
||||
const maxX = Math.max(...xs, 1);
|
||||
const minZ = Math.min(...zs, -1);
|
||||
const maxZ = Math.max(...zs, 1);
|
||||
const spanX = Math.max(1, maxX - minX);
|
||||
const spanZ = Math.max(1, maxZ - minZ);
|
||||
const padding = 6;
|
||||
return entities.map((entity, index) => {
|
||||
const props = objectProps(entity.props);
|
||||
return {
|
||||
entityId: firstString(props.entityId, entity.id),
|
||||
faction: stringProp(props.faction, "neutral"),
|
||||
kind: stringProp(props.entityKind, "entity"),
|
||||
label: firstString(props.label, props.entityId, entity.id, "Entity"),
|
||||
controlled: boolProp(props.controllable),
|
||||
xPercent: padding + ((positions[index][0] - minX) / spanX) * (100 - padding * 2),
|
||||
yPercent: padding + (1 - ((positions[index][2] - minZ) / spanZ)) * (100 - padding * 2),
|
||||
source: entity,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function renderWorldScene(
|
||||
node: EveSurfaceComponent,
|
||||
props: Record<string, unknown>,
|
||||
layout: Record<string, unknown>,
|
||||
style: Record<string, unknown>,
|
||||
options: EveBrowserLoweringOptions,
|
||||
): HTMLElement {
|
||||
const scene = el("section", "cultui-world-scene");
|
||||
assignId(scene, node);
|
||||
applyGeneratedLayout(scene, layout, style);
|
||||
scene.tabIndex = 0;
|
||||
scene.setAttribute("aria-label", stringProp(props.label, "Interactive world"));
|
||||
const entities = projectWorldScene(node);
|
||||
const controlled = entities.find(entity => entity.controlled);
|
||||
|
||||
const heading = el("div", "cultui-world-heading");
|
||||
heading.append(el("strong", "", stringProp(props.label, "World")));
|
||||
heading.append(el("span", "", `${entities.length} entities`));
|
||||
scene.append(heading);
|
||||
|
||||
const plane = el("div", "cultui-world-plane");
|
||||
for (const entity of entities) {
|
||||
const marker = el("button", `cultui-world-entity faction-${cssIdentifier(entity.faction)} kind-${cssIdentifier(entity.kind)}`) as HTMLButtonElement;
|
||||
marker.type = "button";
|
||||
marker.style.left = `${entity.xPercent}%`;
|
||||
marker.style.top = `${entity.yPercent}%`;
|
||||
marker.dataset.entityId = entity.entityId;
|
||||
marker.dataset.controlled = entity.controlled ? "true" : "false";
|
||||
marker.title = entity.label;
|
||||
marker.setAttribute("aria-label", `${entity.label}, ${entity.kind}, ${entity.faction}`);
|
||||
marker.append(el("span", "cultui-world-entity-glyph", worldEntityGlyph(entity.kind)));
|
||||
marker.append(el("span", "cultui-world-entity-label", entity.label));
|
||||
const entityProps = objectProps(entity.source.props);
|
||||
const targetCommand = firstString(entityProps.targetCommand, props.targetCommand, entityProps.focusCommand, props.focusCommand);
|
||||
if (targetCommand) {
|
||||
marker.addEventListener("click", () => emitWorldCommand(targetCommand, {
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
entityId: entity.entityId,
|
||||
targetEntityId: entity.entityId,
|
||||
targetEntityKey: entity.entityId,
|
||||
}, entity.source, options));
|
||||
}
|
||||
plane.append(marker);
|
||||
}
|
||||
scene.append(plane);
|
||||
|
||||
scene.addEventListener("keydown", event => {
|
||||
const direction = worldDirectionForKey(event.key);
|
||||
if (direction) {
|
||||
event.preventDefault();
|
||||
const command = firstString(props.movementCommand, objectProps(controlled?.source.props).moveCommand);
|
||||
if (command) emitWorldCommand(command, {
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
actorEntityKey: controlled?.entityId || firstString(props.playerEntityId),
|
||||
directionX: direction[0],
|
||||
directionY: direction[1],
|
||||
scalar: 1,
|
||||
}, node, options);
|
||||
return;
|
||||
}
|
||||
const command = event.key === " "
|
||||
? firstString(props.actionCommand, objectProps(controlled?.source.props).actionCommand)
|
||||
: event.key.toLowerCase() === "f"
|
||||
? firstString(props.focusCommand, objectProps(controlled?.source.props).focusCommand)
|
||||
: "";
|
||||
if (command) {
|
||||
event.preventDefault();
|
||||
emitWorldCommand(command, {
|
||||
actionId: event.key === " " ? "0" : "focus",
|
||||
actorEntityId: controlled?.entityId || firstString(props.playerEntityId),
|
||||
actorEntityKey: controlled?.entityId || firstString(props.playerEntityId),
|
||||
}, node, options);
|
||||
}
|
||||
});
|
||||
return scene;
|
||||
}
|
||||
|
||||
function emitWorldCommand(
|
||||
command: string,
|
||||
action: Record<string, unknown>,
|
||||
node: EveSurfaceComponent,
|
||||
options: EveBrowserLoweringOptions,
|
||||
): void {
|
||||
void options.commandSink?.(createWorldActionIntent(command, action, options), node);
|
||||
}
|
||||
|
||||
export function createWorldActionIntent(
|
||||
command: string,
|
||||
action: Record<string, unknown>,
|
||||
options: EveBrowserLoweringOptions = currentOptions,
|
||||
): EveCommandIntent {
|
||||
return createEveCommandIntent(command, { action }, options);
|
||||
}
|
||||
|
||||
function worldDirectionForKey(key: string): [number, number] | undefined {
|
||||
switch (key.toLowerCase()) {
|
||||
case "w": case "arrowup": return [0, 1];
|
||||
case "s": case "arrowdown": return [0, -1];
|
||||
case "a": case "arrowleft": return [-1, 0];
|
||||
case "d": case "arrowright": return [1, 0];
|
||||
default: return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function worldEntityGlyph(kind: string): string {
|
||||
if (kind.includes("station")) return "S";
|
||||
if (kind.includes("projectile")) return ".";
|
||||
if (kind.includes("ship")) return "^";
|
||||
return "+";
|
||||
}
|
||||
|
||||
function cssIdentifier(value: string): string {
|
||||
return value.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
||||
}
|
||||
|
||||
async function resolveGravitySurfaceDocuments(
|
||||
node: EveSurfaceComponent,
|
||||
props: Record<string, unknown>,
|
||||
|
|
|
|||
|
|
@ -814,6 +814,85 @@ body[data-pixel-art="true"] .cultui-background {
|
|||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.cultui-world-scene {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
min-height: min(72vh, 760px);
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
background: rgba(2, 8, 12, 0.5);
|
||||
}
|
||||
|
||||
.cultui-world-scene:focus-visible {
|
||||
box-shadow: inset 0 0 0 2px var(--accent);
|
||||
}
|
||||
|
||||
.cultui-world-heading {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: baseline;
|
||||
color: var(--text);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cultui-world-heading span {
|
||||
color: var(--quiet);
|
||||
}
|
||||
|
||||
.cultui-world-plane {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(var(--pixel-grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--pixel-grid) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
}
|
||||
|
||||
.cultui-world-entity {
|
||||
position: absolute;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(224, 238, 240, 0.65);
|
||||
border-radius: 50%;
|
||||
color: #f1f5f4;
|
||||
background: rgba(24, 34, 38, 0.92);
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cultui-world-entity.faction-player { border-color: #54d9c7; color: #9ff8e8; }
|
||||
.cultui-world-entity.faction-raider,
|
||||
.cultui-world-entity.faction-hostile { border-color: #ff695f; color: #ffaaa3; }
|
||||
.cultui-world-entity[data-controlled="true"] { box-shadow: 0 0 0 3px rgba(255, 190, 72, 0.4); }
|
||||
|
||||
.cultui-world-entity-glyph {
|
||||
align-self: center;
|
||||
font: 700 16px/1 var(--font-mono);
|
||||
}
|
||||
|
||||
.cultui-world-entity-label {
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
width: max-content;
|
||||
max-width: 132px;
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
font: 600 11px/1.2 var(--font-body);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cultui-swatch {
|
||||
width: 36px;
|
||||
min-height: 36px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { createEveCommandIntent, selectAdvertisedSurface } from "../dist/index.js";
|
||||
import { createEveCommandIntent, createWorldActionIntent, projectWorldScene, selectAdvertisedSurface } from "../dist/index.js";
|
||||
|
||||
test("selects the requested surface from provider authority", () => {
|
||||
const provider = {
|
||||
|
|
@ -15,6 +15,50 @@ test("selects the requested surface from provider authority", () => {
|
|||
assert.throws(() => selectAdvertisedSurface(provider, "example.missing"), /does not advertise surface/);
|
||||
});
|
||||
|
||||
test("world interactions emit provider-routed command intents without local authority", () => {
|
||||
const intent = createWorldActionIntent("world.move", {
|
||||
actorEntityId: "entity.player",
|
||||
directionX: 1,
|
||||
directionY: 0,
|
||||
scalar: 1,
|
||||
}, {
|
||||
activeSurfaceId: "example.world",
|
||||
clientId: "browser.world-test",
|
||||
provider: {
|
||||
providerId: "example",
|
||||
surfaces: [{
|
||||
surfaceId: "example.world",
|
||||
worldInteraction: {
|
||||
commandBoundary: "example.commands",
|
||||
receiptSchema: "example.receipt.v1",
|
||||
},
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(intent.command, "world.move");
|
||||
assert.equal(intent.commandBoundary, "example.commands");
|
||||
assert.equal(intent.receiptSchema, "example.receipt.v1");
|
||||
assert.equal(intent.payload.actorEntityId, "entity.player");
|
||||
assert.equal(intent.payload.directionX, 1);
|
||||
});
|
||||
|
||||
test("projects provider world entities into a normalized tactical plane", () => {
|
||||
const entities = projectWorldScene({
|
||||
kind: "world.scene3d",
|
||||
children: [
|
||||
{ id: "player", kind: "world.entity3d", props: { position: "-10, 0, -20", faction: "player", controllable: "true" } },
|
||||
{ id: "target", kind: "world.entity3d", props: { position: "30, 0, 40", faction: "hostile" } },
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(entities.length, 2);
|
||||
assert.equal(entities[0].controlled, true);
|
||||
assert.equal(entities[1].faction, "hostile");
|
||||
assert.ok(entities.every(entity => entity.xPercent >= 6 && entity.xPercent <= 94));
|
||||
assert.ok(entities.every(entity => entity.yPercent >= 6 && entity.yPercent <= 94));
|
||||
});
|
||||
|
||||
test("command intents use the active surface advertisement boundary", () => {
|
||||
const intent = createEveCommandIntent("aetheria.daemon.editor.inspect", {
|
||||
action: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue