refactor: remove legacy types.ts file and update frontend to use new contracts feat: add applyActions function to manage action application and world state mutation chore: remove empty .gitkeep file from sqlite data directory refactor: update frontend App component to align with new API contracts and improve UX docs: revise project.md to reflect updated architecture and system requirements docs: update thoughts.md with current status, architecture decisions, and remaining checks
126 lines
3.7 KiB
TypeScript
126 lines
3.7 KiB
TypeScript
import type { Action } from "./contracts/action";
|
|
import type { Entity } from "./contracts/entity";
|
|
import type { ValidationResult } from "./contracts/validation";
|
|
import type { WorldState } from "./contracts/world";
|
|
|
|
function getEntity(worldState: WorldState, entityId: string | undefined): Entity | undefined {
|
|
if (!entityId) {
|
|
return undefined;
|
|
}
|
|
return worldState.entities[entityId];
|
|
}
|
|
|
|
function hasKey(actor: Entity, requiredKeyId: string): boolean {
|
|
return actor.attributes[`has_${requiredKeyId}`] === true;
|
|
}
|
|
|
|
export function validateActions(actions: Action[], worldState: WorldState): ValidationResult[] {
|
|
return actions.map((action, actionIndex): ValidationResult => {
|
|
const actor = getEntity(worldState, action.actorId);
|
|
if (!actor) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "actor_not_found",
|
|
message: `Actor '${action.actorId}' does not exist.`,
|
|
};
|
|
}
|
|
|
|
switch (action.type) {
|
|
case "inspect":
|
|
return { actionIndex, success: true };
|
|
|
|
case "take": {
|
|
const target = getEntity(worldState, action.targetId);
|
|
if (!target) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "target_not_found",
|
|
message: `Target '${action.targetId ?? "(missing)"}' does not exist.`,
|
|
};
|
|
}
|
|
|
|
const actorLocation = String(actor.attributes.location ?? "");
|
|
const targetLocation = String(target.attributes.location ?? "");
|
|
if (actorLocation !== targetLocation) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "not_in_same_location",
|
|
message: `Target '${target.id}' is not in the same location as '${actor.id}'.`,
|
|
};
|
|
}
|
|
|
|
if (target.attributes.takeable !== true) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "not_takeable",
|
|
message: `Target '${target.id}' cannot be taken.`,
|
|
};
|
|
}
|
|
|
|
return { actionIndex, success: true };
|
|
}
|
|
|
|
case "open": {
|
|
const target = getEntity(worldState, action.targetId);
|
|
if (!target) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "target_not_found",
|
|
message: `Target '${action.targetId ?? "(missing)"}' does not exist.`,
|
|
};
|
|
}
|
|
|
|
if (target.attributes.openable !== true) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "not_openable",
|
|
message: `Target '${target.id}' is not openable.`,
|
|
};
|
|
}
|
|
|
|
if (target.attributes.locked === true) {
|
|
const requiredKey = String(target.attributes.requiredKey ?? "key_1");
|
|
if (!hasKey(actor, requiredKey)) {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "locked_requires_key",
|
|
message: `Target '${target.id}' is locked and requires '${requiredKey}'.`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { actionIndex, success: true };
|
|
}
|
|
|
|
case "move": {
|
|
const target = getEntity(worldState, action.targetId);
|
|
if (!target || target.type !== "room") {
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "target_not_found",
|
|
message: `Move target '${action.targetId ?? "(missing)"}' is not a valid room.`,
|
|
};
|
|
}
|
|
|
|
return { actionIndex, success: true };
|
|
}
|
|
|
|
default:
|
|
return {
|
|
actionIndex,
|
|
success: false,
|
|
reason: "unknown_action",
|
|
message: `Action type '${action.type}' is not supported.`,
|
|
};
|
|
}
|
|
});
|
|
}
|