import { type SharedState, worldPositionOf } from '../../core/graph'; import { namedValue } from '../../core/graph/values'; import type { SceneAsset } from '../../core/model/Asset'; import { findAsset, type Project } from '../../core/model/Project'; import type { PropertyBag } from '../../core/model/Properties'; import { cameraFrame } from '../../core/properties/camera'; import type { Entity } from '../../runtime/Entity'; import type { World } from '../../runtime/World'; import { cameraOfItems, cameraOfWorld } from '../../viewport/activeCamera'; import { collectHolderItems } from '../../viewport/items'; /** The running game as `get_runtime` reads it (#429). */ export interface RuntimeFacts { world: World; /** The copy of the project the run plays: edits made while it runs land here. */ project: Project; shared: SharedState; heldKeys: ReadonlySet; /** Paused from the toolbar; the game's own pause is the world's. */ paused: boolean; } /** How many console lines an answer carries unless the agent asks for more. */ export const CONSOLE_LINES = 30; export const NO_CAMERA_HINT = 'No camera is switched on, so the game shows nothing: place a camera asset in the scene.'; /** * What `get_view` warns of: a game that would show nothing. Playing, the run's camera counts; before * that, the scene Play would start (#429, the agent's first game had none and nothing said so). */ export function cameraHint( project: Project, scene: SceneAsset | undefined, world: World | null, ): string | null { if (world) return cameraOfWorld(world) ? null : NO_CAMERA_HINT; if (!scene || cameraOfItems(collectHolderItems(project, scene, 0).items)) return null; return `The scene Play starts ("${scene.name}") has no camera switched on, so the game would show nothing: place a camera asset in it.`; } /** * Where everything is and what it holds, in world units. Every object is listed briefly; the ones * the agent names come with all their properties. */ export function runtimeView( facts: RuntimeFacts, options: { objects?: readonly string[]; console?: number } = {}, ): object { const { world, project } = facts; const named = options.objects ?? []; const picked = named.length > 0 ? pickEntities(world, named) : world.entities; const camera = cameraOfWorld(world); const lines = options.console ?? CONSOLE_LINES; return { scene: { id: world.sceneId, name: findAsset(project, world.sceneId)?.name ?? '' }, time: round(world.time), paused: facts.paused ? 'toolbar' : world.paused ? 'by the game' : false, camera: camera ? { id: camera.id, x: round(camera.x), y: round(camera.y), ...cameraFrame(camera.props) } : null, ...(camera ? {} : { hint: NO_CAMERA_HINT }), keys: [...facts.heldKeys], mouse: { ...world.pointer, x: round(world.pointer.x), y: round(world.pointer.y) }, objects: picked.map((entity) => entityRow(entity, project, named.length > 0)), variables: sharedRows(facts.shared, project), console: lines > 0 ? world.log.slice(-lines) : [], consoleLines: world.log.length, }; } /** The entities named by object id or name, in the order asked; one that is not there is an error. */ function pickEntities(world: World, names: readonly string[]): Entity[] { return names.map((name) => { const entity = world.findByObjectId(name) ?? world.findByName(name); if (!entity) throw new Error(`There is no object ${name} in the running game`); return entity; }); } function entityRow(entity: Entity, project: Project, full: boolean): object { const { x, y } = worldPositionOf(entity); const rotation = Number(entity.props.rotation ?? 0); const asset = findAsset(project, entity.assetId); const row: Record = { id: entity.objectId, name: entity.name, type: entity.type, asset: asset?.name ?? entity.assetId, x: round(x), y: round(y), }; if (rotation !== 0) row.rotation = round(rotation); if (entity.parent) row.parent = entity.parent.objectId; if (entity.props.enabled === false) row.enabled = false; // A behavior's own variables are what the agent most often wants to see move; its switches are not. const declared = asset?.type === 'behavior' ? asset.variables.map((def) => def.key) : []; if (declared.length > 0) row.variables = named(entity.props, declared); if (full) row.props = named(entity.props); return row; } /** The values the Variables assets share, by asset name. */ function sharedRows(shared: SharedState, project: Project): Record { const rows: Record = {}; for (const [id, bag] of Object.entries(shared.variables)) rows[findAsset(project, id)?.name ?? id] = named(bag); return rows; } function named(bag: PropertyBag, keys: readonly string[] = Object.keys(bag)): Record { const out: Record = {}; for (const key of keys) out[key] = namedValue(bag[key]); return out; } function round(value: number): number { return Math.round(value * 100) / 100; }