import { randomUUID } from "node:crypto"; import type { Action } from "../contracts/action"; import type { ValidationResult } from "../contracts/validation"; import type { Entity } from "../contracts/entity"; import type { WorldState } from "../contracts/world"; function cloneWorldState(worldState: WorldState): WorldState { const entities: Record = {}; for (const [id, entity] of Object.entries(worldState.entities)) { entities[id] = { ...entity, attributes: { ...entity.attributes }, }; } return { ...worldState, entities, metadata: { ...worldState.metadata }, }; } function slugify(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") || "character"; } function createCharacterId(worldState: WorldState, baseName: string): string { const baseId = `character_${slugify(baseName)}`; if (!worldState.entities[baseId]) { return baseId; } let suffix = 2; while (worldState.entities[`${baseId}_${suffix}`]) { suffix += 1; } return `${baseId}_${suffix}`; } function getActionCharacterName(action: Action): string | undefined { const displayName = action.metadata?.displayName; if (typeof displayName === "string" && displayName.trim()) { return displayName.trim(); } const characterName = action.metadata?.characterName; if (typeof characterName === "string" && characterName.trim()) { return characterName .trim() .split(/\s+/) .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" "); } return undefined; } export function applyActions( actions: Action[], results: ValidationResult[], worldState: WorldState ): WorldState { const nextState = cloneWorldState(worldState); for (const result of results) { if (!result.success) { continue; } const action = actions[result.actionIndex]; if (!action) { continue; } const actor = nextState.entities[action.actorId]; const target = action.targetId ? nextState.entities[action.targetId] : undefined; switch (action.type) { case "move": if (actor && action.targetId) { actor.attributes.location = action.targetId; } break; case "take": if (actor && target) { target.attributes.location = `inventory:${actor.id}`; if (target.id === "key_1") { actor.attributes.has_key_1 = true; } } break; case "open": if (target) { target.attributes.open = true; } break; case "introduce": if (actor && target) { target.attributes.location = actor.attributes.location; target.attributes.in_scene = true; target.attributes.last_introduced_by = actor.id; } else if (actor) { const characterName = getActionCharacterName(action); if (!characterName) { break; } const characterId = createCharacterId(nextState, characterName); nextState.entities[characterId] = { id: characterId, name: characterName, type: "character", attributes: { location: actor.attributes.location, is_social: true, in_scene: true, created_by_action: "introduce", last_introduced_by: actor.id, }, }; } break; case "describe": if (target) { const trait = action.metadata?.trait; if (typeof trait === "string" && trait.trim()) { const traits = Array.isArray(target.attributes.traits) ? target.attributes.traits : []; target.attributes.traits = [...traits, trait.trim()]; } } break; case "inspect": default: break; } } nextState.id = randomUUID(); nextState.createdAt = Date.now(); return nextState; }