import type { GameManifest, RulesetDefinition } from '@ai-rpg-engine/core'; import type { ValidationError, ValidationResult } from './validate.js'; import type { EntityBlueprint, ZoneDefinition, DialogueDefinition, QuestDefinition, AbilityDefinition, StatusDefinition, ConditionSpec, HazardSpec, ItemDefinition } from './schemas.js'; export type ContentPack = { entities?: EntityBlueprint[]; zones?: ZoneDefinition[]; dialogues?: DialogueDefinition[]; quests?: QuestDefinition[]; /** Optional ability definitions — used by validateGameContent to build a verb/status web */ abilities?: AbilityDefinition[]; /** Optional status definitions — used by validateGameContent to resolve status references */ statuses?: StatusDefinition[]; /** Optional verb definitions — used by validateGameContent to resolve ability verbs */ verbs?: { id: string; }[]; /** * Optional chargen archetype definitions (character-creation's build-catalog * `archetypes[]`) — used by validateGameContent to check `startingInventory` kits * against the item registry (F-703048a5). Minimal shape mirrors * `ArchetypeDefinition` (same pattern as `BuildCatalogShape` in build-catalog.ts); * content-schema sits BELOW character-creation in the dependency graph, so * importing the real type would invert the layering. A real `ArchetypeDefinition[]` * satisfies this shape as-is — no reshaping needed to wire it in. */ archetypes?: { id: string; startingInventory?: string[]; }[]; /** * Optional chargen background definitions (character-creation's build-catalog * `backgrounds[]`) — used by validateGameContent to check `startingInventory` kits * against the item registry (F-703048a5). Minimal shape mirrors * `BackgroundDefinition`; see `archetypes` above for the layering rationale. */ backgrounds?: { id: string; startingInventory?: string[]; }[]; /** * Optional bespoke item-use-effect definitions (e.g. inventory-core's * `ItemEffect[]`) — used by validateGameContent to check each `itemId` against * the item registry (F-703048a5). Minimal shape mirrors `ItemEffect` (the `use` * function field, if present, is simply ignored here). */ itemUseEffects?: { itemId: string; }[]; /** * Pack format version. Emitted by world-forge's `exportToEngine` since the * lane existed and, until C1, an UNDECLARED key — so it landed in the same * silent-pass bucket as a typo (C0 REPORT §3.1: a nonsense key produced a * byte-identical load report to real content). Declared here so the key * allowlist can reject genuine unknowns without rejecting a real emitted key. */ schemaVersion?: string; /** * District topology. Real `DistrictDefinition` data in the shape district-core * understands, which arrived at a key `ContentPack` did not declare — one of * C0's "cheap wire gaps" (REPORT §3.1). Routed into a booted world by * `applyContentPack`'s districts channel: district-core reads its definitions * from world state (`district-core.ts:212`), so a post-boot write lands. * * Minimal structural shape, mirroring the `archetypes`/`backgrounds` pattern * above — content-schema sits BELOW @ai-rpg-engine/modules, so importing the * real `DistrictDefinition` would invert the layering. A real * `DistrictDefinition[]` satisfies this as-is. */ districts?: { id: string; name: string; zoneIds: string[]; tags: string[]; controllingFaction?: string; baseMetrics?: Record; }[]; /** * Character-creation catalog. Authored in world-forge and exported — an * authoring win previously cancelled by a wire gap (REPORT §4: `archetypes` * and `backgrounds` ARE authored and exported, they just landed under a key * the engine did not declare). * * SESSION-SCOPED: consumed by character creation before a session runs, not by * any world reader. Read it with `extractSessionContent`, not * `applyContentPack`. Shape left open — `validateBuildCatalog` in this package * is the checker. */ buildCatalog?: Record; /** * Progression trees. SESSION-SCOPED for a structural reason measured in C1: * progression-core closure-captures its tree `Map` at construction * (`progression-core.ts:70-72`) and never reads trees from world state, so a * post-boot write cannot reach it. Read it with `extractSessionContent` and * pass it to `createProgressionCore({ trees })`. */ progressionTrees?: unknown[]; /** * WHERE the pack's entities stand (C3/P1). * * C0's sharpest single drop: `EntityBlueprint` has no location field, so an * exported pack knew every NPC and where none of them stood (REPORT §2 — * "the single most consequential drop in the lane"), and C1's intake seam * reported it as an advisory on every single ingestion because there was * nothing better to do about it. * * Placement is its own record rather than a field on the blueprint, and that * is deliberate: a blueprint is a TEMPLATE. `encounter-spawn` already clones * templates and overrides `zoneId` per instance, so a location on the * template would be a lie for every cloned participant. One template, N * placements. */ placements?: EntityPlacementRecord[]; /** * Deterministic per-zone spawn sets (C3/P1) — the charter's Pillar 2 * "deterministic per-zone spawn sets with cleared/respawn state". * * Emitted by world-forge since the lane existed and, until now, an UNDECLARED * pass-through with zero engine hits (C0 REPORT §3.1). Routed into a booted * world by an injected channel that REGISTERS into `encounter-spawn`'s * existing content registry — the engine already has a complete spawn system * (rolls, one-live-encounter-per-zone ledger, `encounter.spawned` event), and * C3 extends it rather than standing a second one beside it. * * Minimal structural shape, mirroring the `districts` pattern above: * content-schema sits BELOW @ai-rpg-engine/modules, so importing the real * `EncounterAnchor` would invert the layering. */ encounterAnchors?: EncounterAnchorRecord[]; /** * TYPED environmental hazards (C3/P3) — the vocabulary that lets hazard data * MEAN something. * * C0 §3.2's sharpest measurement: hazard STRINGS carry no engine semantics * ("their meaning is JavaScript the pack ships"), so a data-only export was * inert by construction, and C0 §9 called closing this "the highest-value single * item, because it closes a structural hole rather than a wire hole." * * Zones bind to these by id through `ZoneDefinition.hazardRefs`. Shape mirrored * structurally (the `districts` pattern) because the interpreter lives in * @ai-rpg-engine/modules, above this package. */ hazardDefinitions?: HazardSpec[]; /** * Optional item catalog entries. Derived into the item registry by * {@link validateGameContent} so a JSON pack that names items in inventory, * chargen kits, or quest rewards is not green on a dangling id. * * Promoted from `{ id: string }` to {@link ItemDefinition} (structural * subset of `@ai-rpg-engine/equipment`'s ItemDefinition — that package is * the canonical runtime item schema; a real equipment ItemDefinition is * assignable here). `id` remains the only required field so a JSON pack * that only names ids still loads. */ items?: ItemDefinition[]; /** * Authored giveItem: place a catalog item onto an entity's inventory at * intake. `itemId` binds to `items[].id`; `entityId` binds to `entities[].id`. * Zone containers stay forbidden (`zone.items` is ANDON'd). */ itemPlacements?: ItemPlacementRecord[]; /** * Per-entity runtime AI overlay, keyed by EntityBlueprint.id. When present, * applyContentPack writes EntityState.ai from this record (or from * ApplyContentPackOptions.profiles matching aiProfile) instead of dropping * the name. */ entityAi?: Record; /** * Optional RulesetDefinition the rest of the pack is written against. * Overlay-only packs omit this and reuse the host ruleset. When present, * loadContent runs validateRulesetDefinition then validateAbilityPack / * validateStatusPackAgainstRuleset against it. */ ruleset?: RulesetDefinition; /** * Optional listing identity (PackMetadata-shaped). Overlay-only packs omit * this — packEntryFromJsonFile then stubs id/name from the filename. * When present, the JSON path prefers it over the filename stub. * Not sim-affecting (listing, not world writes). */ meta?: PackListingMeta; /** * Optional GameManifest. Overlay-only packs omit this. When present, * packEntryFromJsonFile prefers it over a stub built from filename + meta. * Not sim-affecting. */ manifest?: GameManifest; /** * Optional RuleProfile registry keyed by {@link EntityBlueprint.ruleProfileId}. * Overlay packs omit this — apply then keeps copy-the-string on the id, * unchanged. When present, applyContentPack clones it and MERGES it onto * WorldState.ruleProfiles (never replaces — an overlay pack must layer * over rule profiles a host already registered; F-9930b9b6). statMapping * only (formulaOverrides reserved — closures cannot round-trip). * Not sim-affecting. */ ruleProfiles?: Record; /** * Optional faction registry keyed by {@link EntityBlueprint.faction} and * `districts[].controllingFaction` (F-d54f4d67, F-749aba8e). * `EntityBlueprint.faction` was always copied verbatim onto EntityState.faction * at intake, but until now no pack key could ship the record that id resolves * against — the pointer landed, the registry did not. Overlay packs omit this * — apply then keeps copy-the-string on the id, unchanged. When present, * applyContentPack clones it and MERGES it onto WorldState.factions (never * replaces — an overlay pack must layer over factions a host already * registered). `districts[].controllingFaction` is walked against the * post-merge registry whenever the pointer exists, not only when this key * is present. Not sim-affecting — do not add to SIM_AFFECTING_KEYS * (world-forge hasher pin, same as ruleProfiles / entityAi / meta / manifest). */ factions?: Record; }; /** * Per-archetype combat mapping a JSON pack can author. Structural copy of * core's RuleProfile — content-schema sits beside core so the real type is * assignable. formulaOverrides is reserved (closures cannot round-trip). */ export type PackRuleProfile = { statMapping: { attack: string; precision: string; resolve: string; }; }; /** * Per-faction registry entry a JSON pack can author, keyed by * {@link EntityBlueprint.faction} and `districts[].controllingFaction` * (F-d54f4d67, F-749aba8e). Structural copy of core's FactionState — * content-schema sits beside core so the real type is assignable, but this * stays a separate named type (mirrors PackRuleProfile) so the pack-authoring * contract does not drift with FactionState's own optional `data` field, * which is not authorable here. */ export type PackFactionRecord = { id: string; name: string; reputation: number; disposition: string; }; /** * FactionMembership row a TS pack can author (F-749aba8e). Structural * superset of modules' FactionMembership — content-schema sits below * modules so the real type is not imported. Optional name/reputation/ * disposition are the content opt-in to WorldState.factions; defaults * at seed are name=factionId, reputation=0, disposition='neutral'. * `seedWorldFactionsFromMembership` MERGES those onto the registry from * this same roster (no third list). */ export type AuthoredFactionMembership = { factionId: string; entityIds: string[]; cohesion?: number; name?: string; reputation?: number; disposition?: string; }; /** * Listing identity a JSON ContentPack may author. Structural subset of * pack-registry's PackMetadata — content-schema sits below that package, * so the real type is not imported. A real PackMetadata is assignable. */ export type PackListingMeta = { id: string; name: string; tagline?: string; genres?: string[]; difficulty?: string; tones?: string[]; tags?: string[]; engineVersion?: string; version?: string; description?: string; narratorTone?: string; }; /** Runtime AI overlay a JSON pack can author (structural AIState). */ export type EntityAiState = { profileId: string; goals?: string[]; fears?: string[]; alertLevel?: number; knowledge?: Record; }; /** Authored giveItem — place `itemId` onto `entityId`'s inventory at intake. */ export type ItemPlacementRecord = { itemId: string; entityId: string; }; /** * One entity, placed in one zone, optionally gated on a compiled condition. * * `spawnCondition` is a {@link ConditionSpec}, not a grammar string: world-forge * COMPILES its SpawnCondition grammar at export (RG-C1 Lane 2's ink pattern — a * rich authoring grammar compiling to a closed, engine-owned instruction * format). The engine never parses author syntax. */ export type EntityPlacementRecord = { /** An `EntityBlueprint.id` in this pack. Unresolvable ⇒ refused by name. */ entityId: string; /** A `ZoneDefinition.id` in this pack. Unresolvable ⇒ refused by name. */ zoneId: string; /** Absent ⇒ always placed. */ spawnCondition?: ConditionSpec; }; /** A per-zone encounter table entry. See {@link ContentPack.encounterAnchors}. */ export type EncounterAnchorRecord = { id: string; zoneId: string; /** Closed set — an unmapped value is REFUSED, never defaulted. */ encounterType: 'ambush' | 'patrol' | 'horde' | 'duel' | string; enemyIds: string[]; /** Per-anchor spawn chance in [0, 1]. */ probability: number; /** Rounds a zone stays quiet after this anchor fires. */ cooldownTurns: number; tags: string[]; }; /** * Result of a cross-reference pass. * * `errors` set `ok` to false (genuinely broken references). `advisories` never affect * `ok` — they are likely-mistake signals the author should look at (mirrors the * `validateAbilityPack` / `validateStatusDefinitionPack` warning pattern). */ export type RefsResult = ValidationResult & { advisories: ValidationError[]; }; export declare function validateRefs(pack: ContentPack): RefsResult; /** * Optional registries that define the ids entities/abilities reference. Each is optional: * when a registry is absent AND the pack itself does not define that category, the * corresponding cross-check is skipped (warn-and-degrade — we only flag what we can verify). */ export type GameContentRegistries = { /** Known status ids (e.g. from a StatusDefinition pack) */ statusIds?: string[]; /** Known verb ids (e.g. from a ruleset's verbs) */ verbIds?: string[]; /** Known ability ids */ abilityIds?: string[]; /** Known item ids (inventory / equipment) */ itemIds?: string[]; }; /** * Whole-game cross-validator (CA-05). * * Runs the structural `validateRefs` pass, then — when the relevant registry is available * (supplied explicitly OR derivable from the pack itself) — ties entity- and ability-level * references to the ids that actually define them: * * - entity.startingStatuses → status registry * - entity.inventory / entity.equipment → item registry * - archetype.startingInventory / background.startingInventory → item registry * (chargen build-catalog kits) * - itemUseEffect.itemId → item registry (bespoke item-use-effect definitions) * - quest.rewards[type="item"].params.itemId → item registry * - ability.verb → verb registry * - ability `apply-status` effects (params.statusId) → status registry * * A misspelled status/verb/item id is reported as an ERROR here, instead of failing * silently at runtime. Categories with no available registry are skipped (not invented as * errors). One-way neighbor advisories from `validateRefs` flow through unchanged. * * F-703048a5: the item-registry check originally covered only entity inventory/equipment, * so the same "typo'd itemId ships silently" bug kept recurring on three other * itemId-shaped surfaces — e.g. a fantasy-starter archetype shipping * `startingInventory: ['torch']` with no matching catalog entry anywhere in that starter. * All four surfaces now share this one structural check and the same finding shape. */ export declare function validateGameContent(pack: ContentPack, registries?: GameContentRegistries): RefsResult; //# sourceMappingURL=refs.d.ts.map