Lower interactive worlds in the browser

This commit is contained in:
Meta Vens 2026-07-10 07:01:44 +01:00
commit 7a9493b67d
6 changed files with 429 additions and 1 deletions

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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>,

View file

@ -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;

View file

@ -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: {