@ai-rpg-engine/starter-template 3.5.0 → 3.9.0
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 +2 -9
- package/package.json +5 -2
- package/src/content.json +179 -0
- package/src/content.ts +152 -36
- package/src/index.ts +12 -2
- package/src/setup.ts +31 -14
- package/src/starter.test.ts +95 -10
package/README.md
CHANGED
|
@@ -12,14 +12,7 @@ tests anywhere — no engine monorepo required.
|
|
|
12
12
|
|
|
13
13
|
## Getting started
|
|
14
14
|
|
|
15
|
-
|
|
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
|
+
"version": "3.9.0",
|
|
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",
|
package/src/content.json
ADDED
|
@@ -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 —
|
|
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 {
|
|
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.
|
|
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
|
-
//
|
|
20
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
+
// ENTITY BLUEPRINTS (author data — not runtime EntityState)
|
|
36
52
|
// ═══════════════════════════════════════════════════════════════════
|
|
37
53
|
|
|
38
|
-
export const player:
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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:
|
|
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-
|
|
1
|
+
// @ai-rpg-engine/starter-template
|
|
2
2
|
|
|
3
3
|
export { createGame, myIntentProfiles } from './setup.js';
|
|
4
|
-
export {
|
|
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 {
|
|
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 `
|
|
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
|
|
98
|
+
// Every entityAi profileId (content.ts aiProfile) must resolve here.
|
|
98
99
|
cognition: { profiles: myIntentProfiles },
|
|
99
100
|
});
|
|
100
101
|
|
|
@@ -148,19 +149,35 @@ export function createGame(seed?: number): Engine {
|
|
|
148
149
|
],
|
|
149
150
|
});
|
|
150
151
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
// Author data → runtime. JSON hosts: extractSessionContent(pack) →
|
|
153
|
+
// construct modules (traversalCore/statusCore/combatCore/inventoryCore/
|
|
154
|
+
// cognitionCore/environmentCore/districtCore/encounterSpawn +
|
|
155
|
+
// presence/length-gated quests/dialogues/abilities/equipment — NOT
|
|
156
|
+
// buildWorldStack; see content-schema's extractSessionContent JSDoc for
|
|
157
|
+
// the full recipe, F-c9309691) →
|
|
158
|
+
// applyContentPack({ profiles, channels: createStandardChannels() }).
|
|
159
|
+
// Import createStandardChannels from @ai-rpg-engine/modules — do not
|
|
160
|
+
// auto-inject. This factory still wires named catalogs at construction;
|
|
161
|
+
// apply copies EntityBlueprint relations/custom/resistances/faction/
|
|
162
|
+
// ruleProfileId and resolves aiProfile against cognition profiles +
|
|
163
|
+
// pack.entityAi.
|
|
164
|
+
const applied = applyContentPack(engine, pack, { profiles: myIntentProfiles });
|
|
165
|
+
if (!applied.ok) {
|
|
166
|
+
const detail = applied.errors.map((e) => `${e.path}: ${e.message}`).join('\n');
|
|
167
|
+
throw new Error(`applyContentPack failed:\n${detail}`);
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
//
|
|
162
|
-
|
|
163
|
-
|
|
170
|
+
// F-bc7b8ab1: no manual playerId/locationId stamp needed here —
|
|
171
|
+
// content.ts's placements[] places pack's single type:'player' entity at
|
|
172
|
+
// 'start', so applyContentPack's own identity stamp (F-67786a6c) already
|
|
173
|
+
// derived both onto the store above. Don't add one back for your own
|
|
174
|
+
// starting zone either: if your pack authors exactly one type:'player'
|
|
175
|
+
// entity with a placement, the stamp tracks it for free — a hand-written
|
|
176
|
+
// override here is the exact pattern that can silently diverge from
|
|
177
|
+
// content.ts once you move the starting zone. Only fall back manually
|
|
178
|
+
// for a pack that declares zero or several type:'player' entities (the
|
|
179
|
+
// stamp is deliberately ambiguous-safe: it never guesses):
|
|
180
|
+
// if (!engine.store.state.playerId) engine.store.state.playerId = 'player';
|
|
164
181
|
|
|
165
182
|
return engine;
|
|
166
183
|
}
|
package/src/starter.test.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
77
|
-
const engine = createGame(1);
|
|
85
|
+
function authoredPack() {
|
|
78
86
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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(
|
|
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
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -240,3 +305,23 @@ describe('starter template — cross-instance state isolation', () => {
|
|
|
240
305
|
.not.toBe(a.world.entities['grunt'].resources);
|
|
241
306
|
});
|
|
242
307
|
});
|
|
308
|
+
|
|
309
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
310
|
+
// IDENTITY STAMP (F-bc7b8ab1)
|
|
311
|
+
// Pin, not a bug fix: this must read identically before and after removing
|
|
312
|
+
// setup.ts's post-applyContentPack `engine.store.state.playerId = 'player';
|
|
313
|
+
// engine.store.state.locationId = 'start';` lines — content.ts's
|
|
314
|
+
// placements[] places { entityId: 'player', zoneId: 'start' }, and pack
|
|
315
|
+
// carries exactly one type:'player' entity, so applyContentPack's own
|
|
316
|
+
// identity stamp (F-67786a6c) already derives the same values. The manual
|
|
317
|
+
// lines were the pre-fix-style override the stamp was written to obsolete —
|
|
318
|
+
// this is the scaffold every new game is copied from, so the leftover
|
|
319
|
+
// pattern would otherwise keep propagating into new starters.
|
|
320
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
321
|
+
describe('starter template — identity stamp owns playerId/locationId (F-bc7b8ab1)', () => {
|
|
322
|
+
it('boots with playerId "player" and locationId "start" from applyContentPack\'s own identity stamp, with no manual override line', () => {
|
|
323
|
+
const engine = createGame(1);
|
|
324
|
+
expect(engine.world.playerId).toBe('player');
|
|
325
|
+
expect(engine.world.locationId).toBe('start');
|
|
326
|
+
});
|
|
327
|
+
});
|