308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
import type { SceneRulebook } from "./contracts/rulebook";
|
|
|
|
export const DEFAULT_RULEBOOK_ID = "rulebook_default";
|
|
|
|
/**
|
|
* Builds the default SceneRulebook, encoding all validation logic that was
|
|
* previously hardcoded in the truth engine as editable, data-driven rules.
|
|
*/
|
|
export function createDefaultRulebook(worldId: string): SceneRulebook {
|
|
const now = Date.now();
|
|
return {
|
|
id: DEFAULT_RULEBOOK_ID,
|
|
worldId,
|
|
version: 1,
|
|
name: "Default Rulebook",
|
|
description: "Built-in scene rules for the CharacterGarden engine. Edit freely — the engine re-evaluates on every turn.",
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
rules: [
|
|
{
|
|
actionType: "inspect",
|
|
enabled: true,
|
|
checks: [],
|
|
},
|
|
|
|
{
|
|
actionType: "take",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "take_target_exists_or_actor_can_create",
|
|
description: "Target must exist, or actor must be authorized to create it when createIfMissing is true",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "entityExists", role: "target" },
|
|
{
|
|
op: "and",
|
|
conditions: [
|
|
{ op: "actionMetadataEq", key: "createIfMissing", value: true },
|
|
{ op: "actorIdIn", allowedIds: ["player"] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
failReason: "target_not_found",
|
|
failMessage: "Target '{target.id}' does not exist, and actor '{actor.id}' is not allowed to create missing items.",
|
|
},
|
|
{
|
|
id: "take_same_location",
|
|
description: "If target exists, actor and target must be in the same location",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "sameLocation", roleA: "actor", roleB: "target" },
|
|
],
|
|
},
|
|
failReason: "not_in_same_location",
|
|
failMessage: "Target '{target.id}' is not in the same location as '{actor.id}'.",
|
|
},
|
|
{
|
|
id: "take_takeable",
|
|
description: "If target exists, it must have takeable attribute set to true",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "eq", role: "target", attribute: "takeable", value: true },
|
|
],
|
|
},
|
|
failReason: "not_takeable",
|
|
failMessage: "Target '{target.id}' cannot be taken.",
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
actionType: "open",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "open_target_exists",
|
|
description: "Target entity must exist in the world",
|
|
condition: { op: "entityExists", role: "target" },
|
|
failReason: "target_not_found",
|
|
failMessage: "Target '{target.id}' does not exist.",
|
|
},
|
|
{
|
|
id: "open_openable",
|
|
description: "Target must have openable attribute set to true",
|
|
condition: { op: "eq", role: "target", attribute: "openable", value: true },
|
|
failReason: "not_openable",
|
|
failMessage: "Target '{target.id}' is not openable.",
|
|
},
|
|
{
|
|
id: "open_lock_check",
|
|
description: "If target is locked, actor must possess the required key (has_<requiredKey> attribute)",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{
|
|
op: "not",
|
|
condition: { op: "eq", role: "target", attribute: "locked", value: true },
|
|
},
|
|
{
|
|
op: "attributeRef",
|
|
checkRole: "actor",
|
|
prefix: "has_",
|
|
refRole: "target",
|
|
refAttribute: "requiredKey",
|
|
},
|
|
],
|
|
},
|
|
failReason: "locked_requires_key",
|
|
failMessage: "Target '{target.id}' is locked and requires a key.",
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
actionType: "move",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "move_target_is_room",
|
|
description: "Target must be an existing entity of type 'room'",
|
|
condition: {
|
|
op: "and",
|
|
conditions: [
|
|
{ op: "entityExists", role: "target" },
|
|
{ op: "entityType", role: "target", requiredType: "room" },
|
|
],
|
|
},
|
|
failReason: "target_not_found",
|
|
failMessage: "Move target '{target.id}' is not a valid room.",
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
actionType: "introduce",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "introduce_actor_authorized",
|
|
description: "Only approved characters can introduce/create characters in-scene",
|
|
condition: {
|
|
op: "actorIdIn",
|
|
allowedIds: ["player"],
|
|
},
|
|
failReason: "actor_not_authorized",
|
|
failMessage: "Actor '{actor.id}' is not allowed to introduce new characters.",
|
|
},
|
|
{
|
|
id: "introduce_actor_in_room",
|
|
description: "Actor must be located in a valid room entity",
|
|
condition: {
|
|
op: "and",
|
|
conditions: [
|
|
{ op: "entityExists", role: "actorRoom" },
|
|
{ op: "entityType", role: "actorRoom", requiredType: "room" },
|
|
],
|
|
},
|
|
failReason: "room_not_found",
|
|
failMessage: "Actor '{actor.id}' is not currently in a valid room.",
|
|
},
|
|
{
|
|
id: "introduce_room_joinable",
|
|
description: "Actor's room must allow new arrivals (is_joinable: true)",
|
|
condition: { op: "eq", role: "actorRoom", attribute: "is_joinable", value: true },
|
|
failReason: "room_not_joinable",
|
|
failMessage: "Room is not available for new arrivals.",
|
|
},
|
|
{
|
|
id: "introduce_target_is_character",
|
|
description: "If target entity exists, it must be of type 'character'",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "entityType", role: "target", requiredType: "character" },
|
|
],
|
|
},
|
|
failReason: "target_not_character",
|
|
failMessage: "Target '{target.id}' is not a character and cannot join the scene.",
|
|
},
|
|
{
|
|
id: "introduce_target_social",
|
|
description: "If target exists, it must be socially available (is_social: true)",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "eq", role: "target", attribute: "is_social", value: true },
|
|
],
|
|
},
|
|
failReason: "target_not_social",
|
|
failMessage: "Target '{target.id}' is not socially available to join the scene.",
|
|
},
|
|
{
|
|
id: "introduce_not_already_present",
|
|
description: "If target exists, it must not already be in the same room as the actor",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{
|
|
op: "not",
|
|
condition: { op: "sameLocation", roleA: "actor", roleB: "target" },
|
|
},
|
|
],
|
|
},
|
|
failReason: "already_in_scene",
|
|
failMessage: "Target '{target.id}' is already present in the scene.",
|
|
},
|
|
{
|
|
id: "introduce_no_name_duplicate",
|
|
description: "When introducing a new character by name, no character with that name may already be in the room",
|
|
condition: {
|
|
op: "metaValueNotInRoom",
|
|
metaKey: "characterName",
|
|
entityType: "character",
|
|
},
|
|
failReason: "already_in_scene",
|
|
failMessage: "A character with this name is already present in the scene.",
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
actionType: "describe",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "describe_target_exists",
|
|
description: "Target must exist in the world or be created earlier in this turn",
|
|
condition: { op: "entityExistsOrWillBeCreated", role: "target" },
|
|
failReason: "target_not_found",
|
|
failMessage: "Target '{target.id}' does not exist.",
|
|
},
|
|
{
|
|
id: "describe_target_is_character",
|
|
description: "If target exists, it must be of type 'character'",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "entityType", role: "target", requiredType: "character" },
|
|
],
|
|
},
|
|
failReason: "target_not_character",
|
|
failMessage: "Target '{target.id}' is not a character and cannot be described.",
|
|
},
|
|
{
|
|
id: "describe_same_location",
|
|
description: "If target exists, actor and target must be in the same location",
|
|
condition: {
|
|
op: "or",
|
|
conditions: [
|
|
{ op: "not", condition: { op: "entityExists", role: "target" } },
|
|
{ op: "sameLocation", roleA: "actor", roleB: "target" },
|
|
],
|
|
},
|
|
failReason: "not_in_same_location",
|
|
failMessage: "Target '{target.id}' is not in the same location as '{actor.id}'.",
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
actionType: "transfer",
|
|
enabled: true,
|
|
checks: [
|
|
{
|
|
id: "transfer_recipient_exists",
|
|
description: "Recipient must exist in the world",
|
|
condition: { op: "entityExists", role: "target" },
|
|
failReason: "target_not_found",
|
|
failMessage: "Recipient '{target.id}' does not exist.",
|
|
},
|
|
{
|
|
id: "transfer_recipient_character",
|
|
description: "Recipient must be a character",
|
|
condition: { op: "entityType", role: "target", requiredType: "character" },
|
|
failReason: "target_not_character",
|
|
failMessage: "Recipient '{target.id}' is not a character.",
|
|
},
|
|
{
|
|
id: "transfer_same_location",
|
|
description: "Actor and recipient must be in the same location",
|
|
condition: { op: "sameLocation", roleA: "actor", roleB: "target" },
|
|
failReason: "not_in_same_location",
|
|
failMessage: "Recipient '{target.id}' is not in the same location as '{actor.id}'.",
|
|
},
|
|
{
|
|
id: "transfer_actor_holds_item",
|
|
description: "Actor must currently hold the specified item in inventory",
|
|
condition: { op: "itemInInventory", itemMetadataKey: "itemId", holderRole: "actor" },
|
|
failReason: "item_not_in_inventory",
|
|
failMessage: "Actor '{actor.id}' is not holding the requested item.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|