@ai-rpg-engine/starter-template 3.3.1 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,14 +12,7 @@ tests anywhere — no engine monorepo required.
12
12
 
13
13
  ## Getting started
14
14
 
15
- If you are reading this inside `templates/starter`, scaffold your own copy
16
- first (it names everything for you):
17
-
18
- ```bash
19
- npx --package=@ai-rpg-engine/cli ai-rpg-engine create-starter my-game --out=./my-game
20
- ```
21
-
22
- Then, from the project directory:
15
+ From the project directory:
23
16
 
24
17
  ```bash
25
18
  npm install # pulls @ai-rpg-engine/* plus typescript + vitest
@@ -32,8 +25,8 @@ npx vitest run # run the pack's tests
32
25
  | File | What to edit |
33
26
  |------|--------------|
34
27
  | `package.json` | name and description |
28
+ | `src/content.ts` | `packMeta`, the `ContentPack` (`entities` / `zones` / `placements`), plus session catalogs (`buildCatalog`, `itemCatalog`, `districts`, `progressionTree`, `statusDefinitions`). Keep each enemy's `aiProfile` paired with a profile in setup. `src/content.json` is the same world for `validate` / sidecar `--content`. |
35
29
  | `src/ruleset.ts` | your stats, resources, verbs |
36
- | `src/content.ts` | entities and zones — keep each enemy's `ai.profileId` paired with a profile in setup |
37
30
  | `src/setup.ts` | wire your modules alongside `buildCombatStack`; the intent profiles list lives here |
38
31
  | `src/starter.test.ts` | grows with your content — register dialogues/abilities in the integrity lists |
39
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-rpg-engine/starter-template",
3
- "version": "3.3.1",
3
+ "version": "3.8.1",
4
4
  "description": "Starter template for ai-rpg-engine — scaffold a standalone game project from it",
5
5
  "type": "module",
6
6
  "engines": {
@@ -20,7 +20,10 @@
20
20
  "dependencies": {
21
21
  "@ai-rpg-engine/core": "^3.0.0",
22
22
  "@ai-rpg-engine/content-schema": "^3.0.0",
23
- "@ai-rpg-engine/modules": "^3.0.0"
23
+ "@ai-rpg-engine/modules": "^3.0.0",
24
+ "@ai-rpg-engine/pack-registry": "^3.0.0",
25
+ "@ai-rpg-engine/character-creation": "^3.0.0",
26
+ "@ai-rpg-engine/equipment": "^3.0.0"
24
27
  },
25
28
  "devDependencies": {
26
29
  "typescript": "^5.7.0",
@@ -0,0 +1,179 @@
1
+ {
2
+ "meta": {
3
+ "id": "my-game",
4
+ "name": "My Game",
5
+ "tagline": "A one-line pitch for your game.",
6
+ "genres": ["fantasy"],
7
+ "difficulty": "beginner",
8
+ "tones": ["heroic"],
9
+ "tags": ["starter"],
10
+ "engineVersion": ">=3.8.0 <4.0.0",
11
+ "version": "0.1.0",
12
+ "description": "A brief description of your game. One to three sentences that appear as the listing subtitle.",
13
+ "narratorTone": "A short narrator-voice note."
14
+ },
15
+ "manifest": {
16
+ "id": "my-game",
17
+ "title": "My Game",
18
+ "version": "0.1.0",
19
+ "engineVersion": ">=3.8.0 <4.0.0",
20
+ "ruleset": "my-game",
21
+ "modules": ["traversal-core", "status-core", "combat-core"],
22
+ "contentPacks": ["my-game"]
23
+ },
24
+ "entities": [
25
+ {
26
+ "id": "player",
27
+ "type": "player",
28
+ "name": "Hero",
29
+ "tags": ["player"],
30
+ "baseStats": { "power": 5, "speed": 4, "grit": 3 },
31
+ "baseResources": { "hp": 25, "stamina": 10, "tension": 0 }
32
+ },
33
+ {
34
+ "id": "grunt",
35
+ "type": "npc",
36
+ "name": "Grunt",
37
+ "tags": ["enemy", "hostile"],
38
+ "baseStats": { "power": 3, "speed": 3, "grit": 2 },
39
+ "baseResources": { "hp": 12 },
40
+ "aiProfile": "aggressive"
41
+ }
42
+ ],
43
+ "zones": [
44
+ {
45
+ "id": "start",
46
+ "name": "Starting Area",
47
+ "tags": ["safe"],
48
+ "neighbors": ["danger-zone"]
49
+ },
50
+ {
51
+ "id": "danger-zone",
52
+ "name": "The Danger Zone",
53
+ "tags": ["hostile"],
54
+ "neighbors": ["start"]
55
+ }
56
+ ],
57
+ "placements": [
58
+ { "entityId": "player", "zoneId": "start" },
59
+ { "entityId": "grunt", "zoneId": "danger-zone" }
60
+ ],
61
+ "districts": [
62
+ {
63
+ "id": "my-game-grounds",
64
+ "name": "My Game Grounds",
65
+ "zoneIds": ["start", "danger-zone"],
66
+ "tags": ["starter"],
67
+ "controllingFaction": "my-game"
68
+ }
69
+ ],
70
+ "buildCatalog": {
71
+ "packId": "my-game",
72
+ "statBudget": 3,
73
+ "maxTraits": 2,
74
+ "requiredFlaws": 0,
75
+ "archetypes": [
76
+ {
77
+ "id": "wanderer",
78
+ "name": "Wanderer",
79
+ "description": "A wanderer in My Game.",
80
+ "statPriorities": { "power": 5, "speed": 4, "grit": 3 },
81
+ "startingTags": ["starter"],
82
+ "progressionTreeId": "my-game-mastery"
83
+ }
84
+ ],
85
+ "backgrounds": [
86
+ {
87
+ "id": "local",
88
+ "name": "Local",
89
+ "description": "Grew up around My Game.",
90
+ "statModifiers": {},
91
+ "startingTags": []
92
+ }
93
+ ],
94
+ "traits": [],
95
+ "disciplines": [],
96
+ "crossTitles": [],
97
+ "entanglements": []
98
+ },
99
+ "progressionTrees": [
100
+ {
101
+ "id": "my-game-mastery",
102
+ "name": "My Game Mastery",
103
+ "currency": "xp",
104
+ "nodes": [
105
+ {
106
+ "id": "hardened",
107
+ "name": "Hardened",
108
+ "cost": 10,
109
+ "effects": [{ "type": "resource-boost", "params": { "resource": "hp", "amount": 5 } }]
110
+ }
111
+ ]
112
+ }
113
+ ],
114
+ "statuses": [],
115
+ "dialogues": [],
116
+ "quests": [],
117
+ "abilities": [],
118
+ "items": [
119
+ {
120
+ "id": "worn-blade",
121
+ "name": "Worn Blade",
122
+ "description": "A simple blade for My Game.",
123
+ "slot": "weapon",
124
+ "rarity": "common"
125
+ }
126
+ ],
127
+ "entityAi": {
128
+ "grunt": {
129
+ "profileId": "aggressive",
130
+ "goals": ["guard-zone"],
131
+ "fears": [],
132
+ "alertLevel": 0,
133
+ "knowledge": {}
134
+ }
135
+ },
136
+ "ruleset": {
137
+ "id": "my-game",
138
+ "name": "My Game",
139
+ "version": "0.1.0",
140
+ "stats": [
141
+ { "id": "power", "name": "Power", "min": 1, "max": 20, "default": 5 },
142
+ { "id": "speed", "name": "Speed", "min": 1, "max": 20, "default": 5 },
143
+ { "id": "grit", "name": "Grit", "min": 1, "max": 20, "default": 5 }
144
+ ],
145
+ "resources": [
146
+ { "id": "hp", "name": "HP", "min": 0, "max": 100, "default": 25 },
147
+ { "id": "stamina", "name": "Stamina", "min": 0, "max": 50, "default": 10, "regenRate": 1 },
148
+ { "id": "tension", "name": "Tension", "min": 0, "max": 100, "default": 0 }
149
+ ],
150
+ "verbs": [
151
+ { "id": "move", "name": "Move", "description": "Travel to an adjacent zone" },
152
+ { "id": "inspect", "name": "Inspect", "description": "Examine surroundings" },
153
+ { "id": "attack", "name": "Attack", "tags": ["combat"], "description": "Strike a target" },
154
+ { "id": "guard", "name": "Guard", "tags": ["combat", "defensive"], "description": "Reduce incoming damage" },
155
+ { "id": "brace", "name": "Brace", "tags": ["combat", "defensive"], "description": "Stabilize and resist status effects" },
156
+ { "id": "disengage", "name": "Disengage", "tags": ["combat", "movement"], "description": "Break from combat" },
157
+ { "id": "reposition", "name": "Reposition", "tags": ["combat", "movement"], "description": "Shift engagement state" },
158
+ { "id": "use", "name": "Use", "description": "Use an item" },
159
+ { "id": "speak", "name": "Speak", "tags": ["dialogue"], "description": "Talk to an NPC" },
160
+ { "id": "choose", "name": "Choose", "tags": ["dialogue"], "description": "Select a dialogue option" },
161
+ { "id": "use-ability", "name": "Use Ability", "tags": ["ability"], "description": "Activate a special ability" }
162
+ ],
163
+ "formulas": [
164
+ { "id": "hit-chance", "name": "Hit Chance", "description": "Standard: 50 + speed*5 - target.speed*3", "inputs": ["attacker.speed", "target.speed"], "output": "number (0-100)" },
165
+ { "id": "damage", "name": "Damage", "description": "Standard: attacker.power", "inputs": ["attacker.power"], "output": "number" }
166
+ ],
167
+ "defaultModules": [
168
+ "traversal-core",
169
+ "combat-core",
170
+ "cognition-core",
171
+ "engagement-core",
172
+ "combat-tactics",
173
+ "combat-intent",
174
+ "combat-recovery",
175
+ "combat-state-narration"
176
+ ],
177
+ "progressionModels": []
178
+ }
179
+ }
package/src/content.ts CHANGED
@@ -1,14 +1,27 @@
1
- // Starter content — entities, zones, and manifest
1
+ // Starter content — a loadable ContentPack plus session catalogs
2
2
  //
3
- // Replace this with your game's content.
3
+ // Replace this with your game's content. create-starter rewrites "my-game" /
4
+ // "My Game" placeholders. Keep enemies' aiProfile paired with a profile in
5
+ // setup.ts (runtime AI is overlaid after applyContentPack).
4
6
 
5
- import type { EntityState, ZoneState, GameManifest } from '@ai-rpg-engine/core';
7
+ import type { GameManifest, EntityState } from '@ai-rpg-engine/core';
8
+ import type {
9
+ ContentPack,
10
+ EntityBlueprint,
11
+ ZoneDefinition,
12
+ ProgressionTreeDefinition,
13
+ StatusDefinition,
14
+ } from '@ai-rpg-engine/content-schema';
15
+ import type { PackMetadata } from '@ai-rpg-engine/pack-registry';
16
+ import type { BuildCatalog } from '@ai-rpg-engine/character-creation';
17
+ import type { ItemCatalog } from '@ai-rpg-engine/equipment';
18
+ import { myRuleset } from './ruleset.js';
6
19
 
7
20
  export const manifest: GameManifest = {
8
21
  id: 'my-game',
9
22
  title: 'My Game',
10
23
  version: '0.1.0',
11
- engineVersion: '0.1.0',
24
+ engineVersion: '>=3.8.0 <4.0.0',
12
25
  ruleset: 'my-game',
13
26
  modules: ['traversal-core', 'status-core', 'combat-core'],
14
27
  contentPacks: ['my-game'],
@@ -16,76 +29,179 @@ export const manifest: GameManifest = {
16
29
 
17
30
  // ═══════════════════════════════════════════════════════════════════
18
31
  // PACK METADATA
19
- // This is a minimal subset to keep the template dependency-light. To list
20
- // your pack in the CLI pack selector, export the full PackMetadata shape
21
- // from @ai-rpg-engine/pack-registry (adds: tagline, genres, difficulty,
22
- // tones, tags, engineVersion, narratorTone) plus a BuildCatalog from
23
- // @ai-rpg-engine/character-creation — see any packages/starter-* content.ts
24
- // for a complete example.
32
+ // Full PackMetadata shape so a copied pack can appear in getPackSummaries()
33
+ // / the CLI pack selector without a second trip to the docs.
25
34
  // ═══════════════════════════════════════════════════════════════════
26
35
 
27
- export const packMeta = {
36
+ export const packMeta: PackMetadata = {
28
37
  id: 'my-game',
29
38
  name: 'My Game',
30
- description: 'A brief description of your game',
39
+ tagline: 'A one-line pitch for your game.',
40
+ genres: ['fantasy'],
41
+ difficulty: 'beginner',
42
+ tones: ['heroic'],
43
+ tags: ['starter'],
44
+ engineVersion: '>=3.8.0 <4.0.0',
31
45
  version: '0.1.0',
46
+ description: 'A brief description of your game. One to three sentences that appear as the listing subtitle.',
47
+ narratorTone: 'A short narrator-voice note.',
32
48
  };
33
49
 
34
50
  // ═══════════════════════════════════════════════════════════════════
35
- // PLAYER
51
+ // ENTITY BLUEPRINTS (author data — not runtime EntityState)
36
52
  // ═══════════════════════════════════════════════════════════════════
37
53
 
38
- export const player: EntityState = {
54
+ export const player: EntityBlueprint = {
39
55
  id: 'player',
40
- blueprintId: 'player',
41
56
  type: 'player',
42
57
  name: 'Hero',
43
58
  tags: ['player'],
44
- stats: { power: 5, speed: 4, grit: 3 },
45
- resources: { hp: 25, stamina: 10, tension: 0 },
46
- statuses: [],
47
- inventory: [],
48
- zoneId: 'start',
59
+ baseStats: { power: 5, speed: 4, grit: 3 },
60
+ baseResources: { hp: 25, stamina: 10, tension: 0 },
49
61
  };
50
62
 
51
- // ═══════════════════════════════════════════════════════════════════
52
- // ENEMIES
53
- // ═══════════════════════════════════════════════════════════════════
54
-
55
- export const enemy: EntityState = {
63
+ export const enemy: EntityBlueprint = {
56
64
  id: 'grunt',
57
- blueprintId: 'grunt',
58
65
  type: 'npc',
59
66
  name: 'Grunt',
60
67
  tags: ['enemy', 'hostile'],
61
- stats: { power: 3, speed: 3, grit: 2 },
62
- resources: { hp: 12 },
63
- statuses: [],
64
- zoneId: 'danger-zone',
65
- // ai.profileId picks this enemy's combat brain. It must match a profile
68
+ baseStats: { power: 3, speed: 3, grit: 2 },
69
+ baseResources: { hp: 12 },
70
+ // aiProfile picks this enemy's combat brain. It must match a profile
66
71
  // provided in setup.ts (`cognition: { profiles: [...] }`) — without that
67
72
  // pairing the enemy never selects an intent and just stands there.
68
73
  // Built-ins: 'aggressive' (attack on sight) and 'cautious' (observe first).
69
- ai: { profileId: 'aggressive', goals: ['guard-zone'], fears: [], alertLevel: 0, knowledge: {} },
74
+ // applyContentPack resolves this name against ContentPack.entityAi and
75
+ // ApplyContentPackOptions.profiles (setup.ts).
76
+ aiProfile: 'aggressive',
77
+ };
78
+
79
+ // Per-entity AI overlay. applyContentPack writes this onto EntityState.ai
80
+ // when the entity's id is present here (F-035ac806).
81
+ export const entityAi: Record<string, NonNullable<EntityState['ai']>> = {
82
+ grunt: { profileId: 'aggressive', goals: ['guard-zone'], fears: [], alertLevel: 0, knowledge: {} },
70
83
  };
71
84
 
72
85
  // ═══════════════════════════════════════════════════════════════════
73
- // ZONES
86
+ // ZONES (ZoneDefinition — no roomId)
74
87
  // ═══════════════════════════════════════════════════════════════════
75
88
 
76
- export const zones: ZoneState[] = [
89
+ export const zones: ZoneDefinition[] = [
77
90
  {
78
91
  id: 'start',
79
- roomId: 'room-1',
80
92
  name: 'Starting Area',
81
93
  tags: ['safe'],
82
94
  neighbors: ['danger-zone'],
83
95
  },
84
96
  {
85
97
  id: 'danger-zone',
86
- roomId: 'room-2',
87
98
  name: 'The Danger Zone',
88
99
  tags: ['hostile'],
89
100
  neighbors: ['start'],
90
101
  },
91
102
  ];
103
+
104
+ export const placements: ContentPack['placements'] = [
105
+ { entityId: 'player', zoneId: 'start' },
106
+ { entityId: 'grunt', zoneId: 'danger-zone' },
107
+ ];
108
+
109
+ // ═══════════════════════════════════════════════════════════════════
110
+ // SESSION CATALOGS (chargen / items / progression / statuses / districts)
111
+ // Stubs so a create-starter project can join the pack selector.
112
+ // ═══════════════════════════════════════════════════════════════════
113
+
114
+ export const buildCatalog: BuildCatalog = {
115
+ packId: 'my-game',
116
+ statBudget: 3,
117
+ maxTraits: 2,
118
+ requiredFlaws: 0,
119
+ archetypes: [
120
+ {
121
+ id: 'wanderer',
122
+ name: 'Wanderer',
123
+ description: 'A wanderer in My Game.',
124
+ statPriorities: { power: 5, speed: 4, grit: 3 },
125
+ startingTags: ['starter'],
126
+ progressionTreeId: 'my-game-mastery',
127
+ },
128
+ ],
129
+ backgrounds: [
130
+ {
131
+ id: 'local',
132
+ name: 'Local',
133
+ description: 'Grew up around My Game.',
134
+ statModifiers: {},
135
+ startingTags: [],
136
+ },
137
+ ],
138
+ traits: [],
139
+ disciplines: [],
140
+ crossTitles: [],
141
+ entanglements: [],
142
+ };
143
+
144
+ export const itemCatalog: ItemCatalog = {
145
+ items: [
146
+ {
147
+ id: 'worn-blade',
148
+ name: 'Worn Blade',
149
+ description: 'A simple blade for My Game.',
150
+ slot: 'weapon',
151
+ rarity: 'common',
152
+ },
153
+ ],
154
+ };
155
+
156
+ export const districts = [
157
+ {
158
+ id: 'my-game-grounds',
159
+ name: 'My Game Grounds',
160
+ zoneIds: ['start', 'danger-zone'],
161
+ tags: ['starter'],
162
+ controllingFaction: 'my-game',
163
+ },
164
+ ];
165
+
166
+ export const progressionTree: ProgressionTreeDefinition = {
167
+ id: 'my-game-mastery',
168
+ name: 'My Game Mastery',
169
+ currency: 'xp',
170
+ nodes: [
171
+ {
172
+ id: 'hardened',
173
+ name: 'Hardened',
174
+ cost: 10,
175
+ effects: [{ type: 'resource-boost', params: { resource: 'hp', amount: 5 } }],
176
+ },
177
+ ],
178
+ };
179
+
180
+ export const statusDefinitions: StatusDefinition[] = [];
181
+
182
+ // ═══════════════════════════════════════════════════════════════════
183
+ // CONTENT PACK (the authoring pipeline consumes this)
184
+ // ═══════════════════════════════════════════════════════════════════
185
+
186
+ export const pack: ContentPack = {
187
+ meta: packMeta,
188
+ manifest,
189
+ entities: [player, enemy],
190
+ zones,
191
+ placements,
192
+ districts,
193
+ buildCatalog: buildCatalog as ContentPack['buildCatalog'],
194
+ progressionTrees: [progressionTree],
195
+ statuses: statusDefinitions,
196
+ dialogues: [],
197
+ quests: [],
198
+ abilities: [],
199
+ items: itemCatalog.items as ContentPack['items'],
200
+ entityAi,
201
+ ruleset: myRuleset,
202
+ };
203
+
204
+ /** Identity today — reserved for stripping any runtime-only fields later. */
205
+ export function toContentPack(): ContentPack {
206
+ return pack;
207
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,15 @@
1
- // @ai-rpg-engine/starter-YOURNAME
1
+ // @ai-rpg-engine/starter-template
2
2
 
3
3
  export { createGame, myIntentProfiles } from './setup.js';
4
- export { manifest, packMeta } from './content.js';
4
+ export {
5
+ manifest,
6
+ packMeta,
7
+ pack,
8
+ toContentPack,
9
+ buildCatalog,
10
+ itemCatalog,
11
+ districts,
12
+ progressionTree,
13
+ statusDefinitions,
14
+ } from './content.js';
5
15
  export { myRuleset } from './ruleset.js';
package/src/setup.ts CHANGED
@@ -17,7 +17,8 @@ import {
17
17
  aggressiveProfile,
18
18
  } from '@ai-rpg-engine/modules';
19
19
  import type { IntentProfile } from '@ai-rpg-engine/modules';
20
- import { manifest, player, enemy, zones } from './content.js';
20
+ import { applyContentPack } from '@ai-rpg-engine/content-schema';
21
+ import { manifest, pack } from './content.js';
21
22
  import { myRuleset } from './ruleset.js';
22
23
 
23
24
  // ═══════════════════════════════════════════════════════════════════
@@ -48,7 +49,7 @@ function createTensionPressure(): EngineModule {
48
49
 
49
50
  // ═══════════════════════════════════════════════════════════════════
50
51
  // INTENT PROFILES — required for enemies to act
51
- // Every entity in content.ts that declares an `ai.profileId` must find a
52
+ // Every entity in content.ts that declares an `aiProfile` must find a
52
53
  // matching profile in this list: cognition builds its profile map from
53
54
  // `cognition.profiles`, and with an empty map no enemy ever selects an
54
55
  // intent — they stand still forever. Built-ins from @ai-rpg-engine/modules:
@@ -94,7 +95,7 @@ export function createGame(seed?: number): Engine {
94
95
  // recovery — safe zone recovery:
95
96
  recovery: { safeZoneTags: ['safe'] },
96
97
  // cognition — wires the intent profiles above into the enemy AI.
97
- // Every ai.profileId declared in content.ts must resolve here.
98
+ // Every entityAi profileId (content.ts aiProfile) must resolve here.
98
99
  cognition: { profiles: myIntentProfiles },
99
100
  });
100
101
 
@@ -148,16 +149,20 @@ export function createGame(seed?: number): Engine {
148
149
  ],
149
150
  });
150
151
 
151
- // Register zones
152
- for (const zone of zones) {
153
- engine.store.addZone(zone);
152
+ // Author data → runtime. JSON hosts: extractSessionContent(pack) →
153
+ // construct modules (buildWorldStack({ districts: session.districts }))
154
+ // applyContentPack({ profiles, channels: createStandardChannels() }).
155
+ // Import createStandardChannels from @ai-rpg-engine/modules — do not
156
+ // auto-inject. This factory still wires named catalogs at construction;
157
+ // apply copies EntityBlueprint relations/custom/resistances/faction/
158
+ // ruleProfileId and resolves aiProfile against cognition profiles +
159
+ // pack.entityAi.
160
+ const applied = applyContentPack(engine, pack, { profiles: myIntentProfiles });
161
+ if (!applied.ok) {
162
+ const detail = applied.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
163
+ throw new Error(`applyContentPack failed:\n${detail}`);
154
164
  }
155
165
 
156
- // Register entities — the store detaches (structuredClone) at ingestion,
157
- // so module-level constants are safe to pass directly.
158
- engine.store.addEntity(player);
159
- engine.store.addEntity(enemy);
160
-
161
166
  // Set player context
162
167
  engine.store.state.playerId = 'player';
163
168
  engine.store.state.locationId = 'start';
@@ -1,12 +1,21 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { describe, it, expect } from 'vitest';
4
- import { validateRulesetDefinition, validateGameContent, validateAbilityPack } from '@ai-rpg-engine/content-schema';
5
- import type { ContentPack, AbilityDefinition, DialogueDefinition } from '@ai-rpg-engine/content-schema';
4
+ import { validateRulesetDefinition, validateGameContent, validateAbilityPack, loadContent, loadContentFromFile } from '@ai-rpg-engine/content-schema';
5
+ import type { AbilityDefinition, DialogueDefinition } from '@ai-rpg-engine/content-schema';
6
6
  import { selectIntent } from '@ai-rpg-engine/modules';
7
7
  import type { CognitionState } from '@ai-rpg-engine/modules';
8
8
  import { myRuleset } from './ruleset.js';
9
9
  import { createGame, myIntentProfiles } from './setup.js';
10
+ import {
11
+ pack,
12
+ toContentPack,
13
+ buildCatalog,
14
+ itemCatalog,
15
+ districts,
16
+ progressionTree,
17
+ statusDefinitions,
18
+ } from './content.js';
10
19
 
11
20
  describe('starter template', () => {
12
21
  it('ruleset validates against schema', () => {
@@ -73,18 +82,37 @@ describe('starter template — cross-reference integrity', () => {
73
82
  // Register your AbilityDefinitions here as you write them.
74
83
  const myAbilities: AbilityDefinition[] = [];
75
84
 
76
- function builtContentPack(): ContentPack {
77
- const engine = createGame(1);
85
+ function authoredPack() {
78
86
  return {
79
- zones: Object.values(engine.store.state.zones) as unknown as ContentPack['zones'],
80
- entities: Object.values(engine.store.state.entities) as unknown as ContentPack['entities'],
81
- dialogues: myDialogues,
82
- abilities: myAbilities,
87
+ ...toContentPack(),
88
+ dialogues: [...(toContentPack().dialogues ?? []), ...myDialogues],
89
+ abilities: [...(toContentPack().abilities ?? []), ...myAbilities],
83
90
  };
84
91
  }
85
92
 
93
+ it('F-407729dc: authored pack is loadContent-valid (not a cast of live store state)', () => {
94
+ const r = loadContent(pack);
95
+ expect(r.ok).toBe(true);
96
+ expect(r.errors).toEqual([]);
97
+ expect(loadContent(toContentPack()).ok).toBe(true);
98
+ });
99
+
100
+ it('F-407729dc: src/content.json is a loadable fixture', () => {
101
+ const file = fileURLToPath(new URL('./content.json', import.meta.url));
102
+ const r = loadContentFromFile(file);
103
+ expect(r.ok).toBe(true);
104
+ expect(r.errors).toEqual([]);
105
+ });
106
+
107
+ it('F-82b17cb3: src/content.json equals toContentPack() (full pack, not a subset)', () => {
108
+ const file = fileURLToPath(new URL('./content.json', import.meta.url));
109
+ const fromFile = loadContentFromFile(file);
110
+ expect(fromFile.ok).toBe(true);
111
+ expect(JSON.parse(JSON.stringify(fromFile.pack))).toEqual(JSON.parse(JSON.stringify(toContentPack())));
112
+ });
113
+
86
114
  it('zones, entities, dialogues, and abilities have no dangling references or duplicate ids', () => {
87
- const result = validateGameContent(builtContentPack());
115
+ const result = validateGameContent(authoredPack());
88
116
  expect(result.errors).toEqual([]);
89
117
  expect(result.ok).toBe(true);
90
118
  });
@@ -92,7 +120,7 @@ describe('starter template — cross-reference integrity', () => {
92
120
  it('has no one-way zone passages (neighbor symmetry advisory)', () => {
93
121
  // A zone listing a neighbor that doesn't list it back is legal but
94
122
  // almost always a mistake — the player can walk in and never back out.
95
- const result = validateGameContent(builtContentPack());
123
+ const result = validateGameContent(authoredPack());
96
124
  expect(result.advisories).toEqual([]);
97
125
  });
98
126
 
@@ -215,6 +243,43 @@ describe('starter template — standalone scaffold', () => {
215
243
  expect(readme.toLowerCase()).not.toContain('copy this directory to `packages/');
216
244
  expect(readme).not.toMatch(/cp -r templates\/starter/);
217
245
  });
246
+
247
+ it('F-3dcac63f: first-run README is the game title, not a scaffold-yourself instruction', () => {
248
+ const readme = read('../README.md');
249
+ expect(readme).toMatch(/^# My Game$/m);
250
+ expect(readme).not.toMatch(/scaffold your own copy/i);
251
+ expect(readme).not.toContain('templates/starter');
252
+ expect(readme).toContain('standalone');
253
+ expect(readme).toContain('Getting started');
254
+ });
255
+
256
+ it('F-3dcac63f: packMeta carries the catalog fields getPackSummaries() lists on', () => {
257
+ const src = read('./content.ts');
258
+ for (const field of ['tagline', 'genres', 'difficulty', 'tones', 'tags', 'description', 'version', 'narratorTone']) {
259
+ expect(src, `packMeta must declare ${field}`).toMatch(new RegExp(`\\b${field}\\s*:`));
260
+ }
261
+ const index = read('./index.ts');
262
+ expect(index).toContain('@ai-rpg-engine/starter-template');
263
+ expect(index).not.toContain('starter-YOURNAME');
264
+ });
265
+
266
+ it('F-2abeea73: barrel exports session catalogs under stable names', () => {
267
+ expect(buildCatalog.archetypes.length).toBeGreaterThanOrEqual(1);
268
+ expect(buildCatalog.backgrounds.length).toBeGreaterThanOrEqual(1);
269
+ expect(itemCatalog.items.length).toBeGreaterThanOrEqual(1);
270
+ expect(districts.length).toBeGreaterThanOrEqual(1);
271
+ expect(progressionTree.nodes.length).toBeGreaterThanOrEqual(1);
272
+ expect(Array.isArray(statusDefinitions)).toBe(true);
273
+ const index = read('./index.ts');
274
+ for (const name of ['buildCatalog', 'itemCatalog', 'districts', 'progressionTree', 'statusDefinitions', 'pack', 'toContentPack']) {
275
+ expect(index, `index.ts must re-export ${name}`).toContain(name);
276
+ }
277
+ const pkg = JSON.parse(read('../package.json')) as { dependencies?: Record<string, string> };
278
+ const deps = pkg.dependencies ?? {};
279
+ expect(deps['@ai-rpg-engine/pack-registry']).toBeTruthy();
280
+ expect(deps['@ai-rpg-engine/character-creation']).toBeTruthy();
281
+ expect(deps['@ai-rpg-engine/equipment']).toBeTruthy();
282
+ });
218
283
  });
219
284
 
220
285
  // ═══════════════════════════════════════════════════════════════════