@ai-rpg-engine/starter-template 2.4.0 → 2.6.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MCP Tool Shop
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,15 +1,47 @@
1
- # Starter Template
1
+ # My Game
2
2
 
3
- Copy this directory to `packages/starter-yourname/` to create a new game.
3
+ An [ai-rpg-engine](https://mcp-tool-shop-org.github.io/ai-rpg-engine/) content
4
+ pack, scaffolded from the starter template. The project is **standalone**: it
5
+ carries its own `tsconfig.json` and declares every dependency it needs
6
+ (including `typescript` and `vitest` as devDependencies), so it builds and
7
+ tests anywhere — no engine monorepo required.
8
+
9
+ ## Getting started
10
+
11
+ If you are reading this inside `templates/starter`, scaffold your own copy
12
+ first (it names everything for you):
4
13
 
5
14
  ```bash
6
- cp -r templates/starter packages/starter-mygame
15
+ npx --package=@ai-rpg-engine/cli ai-rpg-engine create-starter my-game --out=./my-game
7
16
  ```
8
17
 
9
- Then edit:
10
- - `package.json` — name and description
11
- - `src/ruleset.ts` — your stats, resources, verbs
12
- - `src/content.ts` entities and zones
13
- - `src/setup.ts` wire your modules alongside `buildCombatStack`
18
+ Then, from the project directory:
19
+
20
+ ```bash
21
+ npm install # pulls @ai-rpg-engine/* plus typescript + vitest
22
+ npx tsc --noEmit # typecheck
23
+ npx vitest run # run the pack's tests
24
+ ```
25
+
26
+ ## Make it yours
27
+
28
+ | File | What to edit |
29
+ |------|--------------|
30
+ | `package.json` | name and description |
31
+ | `src/ruleset.ts` | your stats, resources, verbs |
32
+ | `src/content.ts` | entities and zones — keep each enemy's `ai.profileId` paired with a profile in setup |
33
+ | `src/setup.ts` | wire your modules alongside `buildCombatStack`; the intent profiles list lives here |
34
+ | `src/starter.test.ts` | grows with your content — register dialogues/abilities in the integrity lists |
35
+
36
+ Two wiring rules worth knowing from day one:
37
+
38
+ - **Enemies act only if their `ai.profileId` resolves.** Every profile id
39
+ declared in `src/content.ts` must appear in the `cognition.profiles` list in
40
+ `src/setup.ts` (built-ins: `aggressive`, `cautious`). An empty profiles list
41
+ means enemies never select an intent.
42
+ - **`buildCombatStack` owns the combat stack** (cognition, tactics, resources,
43
+ intent, recovery, narration). Your custom modules go in the marked
44
+ starter-owned section of `src/setup.ts`.
14
45
 
15
- See [Chapter 58 — Create Your Own Starter](../../site/src/content/docs/handbook/58-create-your-own-starter.md) for the full walkthrough.
46
+ See [Chapter 58 — Create Your Own Starter](https://mcp-tool-shop-org.github.io/ai-rpg-engine/handbook/58-create-your-own-starter/)
47
+ in the handbook for the full walkthrough.
package/package.json CHANGED
@@ -1,21 +1,27 @@
1
1
  {
2
2
  "name": "@ai-rpg-engine/starter-template",
3
- "version": "2.4.0",
4
- "description": "Starter template for ai-rpg-engine copy, rename, and build your own game",
3
+ "version": "2.6.0",
4
+ "description": "Starter template for ai-rpg-engine \u00e2\u20ac\u201d scaffold a standalone game project from it",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "src",
8
8
  "README.md",
9
+ "LICENSE",
9
10
  "tsconfig.json"
10
11
  ],
11
12
  "scripts": {
12
13
  "build": "tsc",
14
+ "typecheck": "tsc --noEmit",
13
15
  "test": "vitest run"
14
16
  },
15
17
  "dependencies": {
16
- "@ai-rpg-engine/core": "*",
17
- "@ai-rpg-engine/content-schema": "*",
18
- "@ai-rpg-engine/modules": "*"
18
+ "@ai-rpg-engine/core": "^2.6.0",
19
+ "@ai-rpg-engine/content-schema": "^2.6.0",
20
+ "@ai-rpg-engine/modules": "^2.6.0"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.7.0",
24
+ "vitest": "^3.0.0"
19
25
  },
20
26
  "license": "MIT",
21
27
  "author": "mcp-tool-shop",
package/src/content.ts CHANGED
@@ -15,7 +15,13 @@ export const manifest: GameManifest = {
15
15
  };
16
16
 
17
17
  // ═══════════════════════════════════════════════════════════════════
18
- // PACK METADATA (used by the CLI pack selector)
18
+ // 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.
19
25
  // ═══════════════════════════════════════════════════════════════════
20
26
 
21
27
  export const packMeta = {
@@ -56,6 +62,11 @@ export const enemy: EntityState = {
56
62
  resources: { hp: 12 },
57
63
  statuses: [],
58
64
  zoneId: 'danger-zone',
65
+ // ai.profileId picks this enemy's combat brain. It must match a profile
66
+ // provided in setup.ts (`cognition: { profiles: [...] }`) — without that
67
+ // pairing the enemy never selects an intent and just stands there.
68
+ // Built-ins: 'aggressive' (attack on sight) and 'cautious' (observe first).
69
+ ai: { profileId: 'aggressive', goals: ['guard-zone'], fears: [], alertLevel: 0, knowledge: {} },
59
70
  };
60
71
 
61
72
  // ═══════════════════════════════════════════════════════════════════
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  // @ai-rpg-engine/starter-YOURNAME
2
2
 
3
- export { createGame } from './setup.js';
3
+ export { createGame, myIntentProfiles } from './setup.js';
4
4
  export { manifest, packMeta } from './content.js';
5
5
  export { myRuleset } from './ruleset.js';
package/src/setup.ts CHANGED
@@ -14,7 +14,9 @@ import {
14
14
  traversalCore,
15
15
  statusCore,
16
16
  buildCombatStack,
17
+ aggressiveProfile,
17
18
  } from '@ai-rpg-engine/modules';
19
+ import type { IntentProfile } from '@ai-rpg-engine/modules';
18
20
  import { manifest, player, enemy, zones } from './content.js';
19
21
  import { myRuleset } from './ruleset.js';
20
22
 
@@ -24,13 +26,17 @@ import { myRuleset } from './ruleset.js';
24
26
  // your starter feel different from other starters.
25
27
  // ═══════════════════════════════════════════════════════════════════
26
28
 
27
- // Example: a custom module that ticks your "tension" resource each combat round
29
+ // Example: a custom module that raises your "tension" resource whenever
30
+ // damage lands in combat. It listens on 'combat.damage.applied' — a real
31
+ // event emitted by combat-core every time an attack connects (other engine
32
+ // events you can hook: 'combat.contact.hit', 'combat.contact.miss',
33
+ // 'combat.entity.defeated', 'status.applied', 'world.zone.entered').
28
34
  function createTensionPressure(): EngineModule {
29
35
  return {
30
36
  id: 'tension-pressure',
31
37
  version: '1.0.0',
32
38
  register(ctx) {
33
- ctx.events.on('combat.round.end', (event: ResolvedEvent, world: WorldState) => {
39
+ ctx.events.on('combat.damage.applied', (event: ResolvedEvent, world: WorldState) => {
34
40
  const p = world.entities['player'];
35
41
  if (p && p.resources.tension !== undefined) {
36
42
  p.resources.tension = Math.min(100, (p.resources.tension ?? 0) + 5);
@@ -40,6 +46,19 @@ function createTensionPressure(): EngineModule {
40
46
  };
41
47
  }
42
48
 
49
+ // ═══════════════════════════════════════════════════════════════════
50
+ // INTENT PROFILES — required for enemies to act
51
+ // Every entity in content.ts that declares an `ai.profileId` must find a
52
+ // matching profile in this list: cognition builds its profile map from
53
+ // `cognition.profiles`, and with an empty map no enemy ever selects an
54
+ // intent — they stand still forever. Built-ins from @ai-rpg-engine/modules:
55
+ // aggressiveProfile ('aggressive': attack visible hostiles, flee at low
56
+ // morale) and cautiousProfile ('cautious': observe first, strike when
57
+ // confident). Add your own IntentProfile objects here for custom brains.
58
+ // ═══════════════════════════════════════════════════════════════════
59
+
60
+ export const myIntentProfiles: IntentProfile[] = [aggressiveProfile];
61
+
43
62
  // ═══════════════════════════════════════════════════════════════════
44
63
  // GAME FACTORY
45
64
  // ═══════════════════════════════════════════════════════════════════
@@ -49,18 +68,34 @@ export function createGame(seed?: number): Engine {
49
68
  const combat = buildCombatStack({
50
69
  statMapping: { attack: 'power', precision: 'speed', resolve: 'grit' },
51
70
  playerId: 'player',
52
- // resourceProfile — uncomment and customize for your game's resource pressure:
71
+ // resourceProfile — uncomment and customize for your game's resource
72
+ // pressure. A CombatResourceProfile needs packId + the four arrays
73
+ // (gains / spends / drains / aiModifiers — empty arrays are fine):
53
74
  // resourceProfile: {
75
+ // packId: 'my-game',
76
+ // gains: [
77
+ // // +2 tension every time an attack you make lands
78
+ // { trigger: 'attack-hit', resourceId: 'tension', amount: 2 },
79
+ // ],
54
80
  // spends: [
55
- // { verbId: 'attack', costStat: 'stamina', amount: 2 },
81
+ // // spend 2 stamina on each attack for +1 damage
82
+ // { action: 'attack', resourceId: 'stamina', amount: 2, effects: { damageBonus: 1 } },
56
83
  // ],
84
+ // drains: [],
85
+ // aiModifiers: [],
57
86
  // },
58
- // biasTags — entity tags that cognition uses for faction bias:
59
- // biasTags: ['enemy'],
60
- // engagement customize if your game uses ranged/melee distance:
61
- // engagement: { defaultRange: 'melee' },
87
+ // biasTags — built-in pack bias tags that shape combat AI intent.
88
+ // Must come from PACK_BIAS_TAGS (exported by @ai-rpg-engine/modules,
89
+ // e.g. 'undead', 'beast', 'feral'); unknown tags warn and are dropped:
90
+ // biasTags: ['undead', 'beast'],
91
+ // engagement — backline/protector behavior for ranged parties
92
+ // (fields: backlineTags, protectorTags, chokepointTag, ambushTag):
93
+ // engagement: { backlineTags: ['ranged', 'caster'], protectorTags: ['bodyguard'] },
62
94
  // recovery — safe zone recovery:
63
95
  recovery: { safeZoneTags: ['safe'] },
96
+ // cognition — wires the intent profiles above into the enemy AI.
97
+ // Every ai.profileId declared in content.ts must resolve here.
98
+ cognition: { profiles: myIntentProfiles },
64
99
  });
65
100
 
66
101
  const engine = new Engine({
@@ -84,8 +119,8 @@ export function createGame(seed?: number): Engine {
84
119
  }
85
120
 
86
121
  // Register entities
87
- engine.store.addEntity({ ...player });
88
- engine.store.addEntity({ ...enemy });
122
+ engine.store.addEntity(structuredClone(player));
123
+ engine.store.addEntity(structuredClone(enemy));
89
124
 
90
125
  // Set player context
91
126
  engine.store.state.playerId = 'player';
@@ -1,7 +1,12 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import { describe, it, expect } from 'vitest';
2
- import { validateRulesetDefinition } from '@ai-rpg-engine/content-schema';
4
+ import { validateRulesetDefinition, validateGameContent, validateAbilityPack } from '@ai-rpg-engine/content-schema';
5
+ import type { ContentPack, AbilityDefinition, DialogueDefinition } from '@ai-rpg-engine/content-schema';
6
+ import { selectIntent } from '@ai-rpg-engine/modules';
7
+ import type { CognitionState } from '@ai-rpg-engine/modules';
3
8
  import { myRuleset } from './ruleset.js';
4
- import { createGame } from './setup.js';
9
+ import { createGame, myIntentProfiles } from './setup.js';
5
10
 
6
11
  describe('starter template', () => {
7
12
  it('ruleset validates against schema', () => {
@@ -27,9 +32,211 @@ describe('starter template', () => {
27
32
  expect(engine).toBeDefined();
28
33
  });
29
34
 
30
- it('tension pressure module is wired', () => {
35
+ it('tension rises when combat damage lands (tension-pressure fires)', () => {
31
36
  const engine = createGame(1);
32
- const p = engine.world.entities['player'];
33
- expect(p?.resources.tension).toBe(0);
37
+ expect(engine.world.entities['player']?.resources.tension).toBe(0);
38
+
39
+ // Walk into the danger zone and fight. combat-core emits
40
+ // 'combat.damage.applied' whenever a hit lands — that is the event
41
+ // the tension-pressure module listens on.
42
+ engine.submitAction('move', { targetIds: ['danger-zone'] });
43
+ for (let i = 0; i < 8; i++) {
44
+ engine.submitAction('attack', { targetIds: ['grunt'] });
45
+ if ((engine.world.entities['player']?.resources.tension ?? 0) > 0) break;
46
+ }
47
+
48
+ // Meta-test property: deleting the tension-pressure listener in
49
+ // setup.ts turns this RED — nothing else writes the tension resource.
50
+ expect(engine.world.entities['player']?.resources.tension).toBeGreaterThan(0);
51
+ });
52
+ });
53
+
54
+ // ═══════════════════════════════════════════════════════════════════
55
+ // CROSS-REFERENCE INTEGRITY
56
+ // The schema test above checks each piece in isolation. These tests check
57
+ // the connections BETWEEN pieces: dangling zone neighbors, duplicate ids,
58
+ // dialogue speakers that name an entity's display name instead of its id,
59
+ // abilities that reference undeclared stats/resources. They validate the
60
+ // REAL composed game (createGame()'s world state), so they automatically
61
+ // cover content you add — as long as you keep the two lists below in sync.
62
+ //
63
+ // Why this matters: dialogue-core finds a dialogue by matching its
64
+ // speakers[] against the target ENTITY ID. Writing the display name
65
+ // ('Grunt') instead of the id ('grunt') doesn't error — "talk to NPC"
66
+ // just silently reports "has nothing to say". validateGameContent catches
67
+ // exactly that class of bug at test time.
68
+ // ═══════════════════════════════════════════════════════════════════
69
+ describe('starter template — cross-reference integrity', () => {
70
+ // Register your DialogueDefinitions here as you write them. Remember:
71
+ // speakers: ['grunt'] (entity id), never speakers: ['Grunt'] (name).
72
+ const myDialogues: DialogueDefinition[] = [];
73
+ // Register your AbilityDefinitions here as you write them.
74
+ const myAbilities: AbilityDefinition[] = [];
75
+
76
+ function builtContentPack(): ContentPack {
77
+ const engine = createGame(1);
78
+ 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,
83
+ };
84
+ }
85
+
86
+ it('zones, entities, dialogues, and abilities have no dangling references or duplicate ids', () => {
87
+ const result = validateGameContent(builtContentPack());
88
+ expect(result.errors).toEqual([]);
89
+ expect(result.ok).toBe(true);
90
+ });
91
+
92
+ it('has no one-way zone passages (neighbor symmetry advisory)', () => {
93
+ // A zone listing a neighbor that doesn't list it back is legal but
94
+ // almost always a mistake — the player can walk in and never back out.
95
+ const result = validateGameContent(builtContentPack());
96
+ expect(result.advisories).toEqual([]);
97
+ });
98
+
99
+ it('abilities reference only stats/resources declared in the ruleset', () => {
100
+ const result = validateAbilityPack(myAbilities, myRuleset);
101
+ expect(result.errors).toEqual([]);
102
+ });
103
+ });
104
+
105
+ // ═══════════════════════════════════════════════════════════════════
106
+ // INTENT PROFILE WIRING
107
+ // Enemies act only if their ai.profileId resolves to a profile provided
108
+ // in setup.ts's `cognition.profiles`. An empty profiles list is the #1
109
+ // reason a pack's enemies stand still forever. These tests keep every
110
+ // declared profileId resolvable as you add content.
111
+ // ═══════════════════════════════════════════════════════════════════
112
+ describe('starter template — intent profile wiring', () => {
113
+ it('provides a non-empty intent profile list to cognition', () => {
114
+ expect(myIntentProfiles.length).toBeGreaterThan(0);
115
+ for (const profile of myIntentProfiles) {
116
+ expect(typeof profile.id).toBe('string');
117
+ expect(typeof profile.evaluate).toBe('function');
118
+ }
119
+ });
120
+
121
+ it('every hostile entity declares a profileId that resolves to a provided profile', () => {
122
+ const engine = createGame(1);
123
+ const hostiles = Object.values(engine.world.entities).filter(
124
+ (e) => e.type === 'enemy' || e.tags.includes('enemy'),
125
+ );
126
+ expect(hostiles.length).toBeGreaterThan(0);
127
+
128
+ const provided = new Set(myIntentProfiles.map((p) => p.id));
129
+ for (const hostile of hostiles) {
130
+ expect(hostile.ai?.profileId, `${hostile.id} must declare ai.profileId`).toBeTruthy();
131
+ expect(
132
+ provided.has(hostile.ai!.profileId),
133
+ `${hostile.id} declares "${hostile.ai!.profileId}" — not in cognition.profiles`,
134
+ ).toBe(true);
135
+ }
136
+ });
137
+
138
+ it('resolved profiles produce intents — hostiles can act on an intruder', () => {
139
+ const engine = createGame(1);
140
+ const world = engine.world;
141
+ const player = world.entities[world.playerId || 'player'];
142
+ expect(player).toBeDefined();
143
+
144
+ for (const hostile of Object.values(world.entities)) {
145
+ if (!(hostile.type === 'enemy' || hostile.tags.includes('enemy'))) continue;
146
+ const profile = myIntentProfiles.find((p) => p.id === hostile.ai?.profileId);
147
+ expect(profile, `${hostile.id} must resolve an intent profile`).toBeDefined();
148
+
149
+ // Stand the player in the hostile's zone as a believed-hostile intruder.
150
+ player!.zoneId = hostile.zoneId;
151
+ const cognition: CognitionState = {
152
+ beliefs: [{
153
+ subject: player!.id,
154
+ key: 'hostile',
155
+ value: true,
156
+ confidence: 1,
157
+ source: 'observed',
158
+ tick: world.meta.tick,
159
+ }],
160
+ memories: [],
161
+ currentIntent: null,
162
+ morale: 80,
163
+ suspicion: 60,
164
+ };
165
+ const intent = selectIntent(hostile, cognition, world, profile!);
166
+ expect(intent, `${hostile.id} (${profile!.id}) selected no intent`).not.toBeNull();
167
+ }
168
+ });
169
+ });
170
+
171
+ // ═══════════════════════════════════════════════════════════════════
172
+ // STANDALONE SCAFFOLD
173
+ // The scaffold must work OUTSIDE the engine monorepo: its tsconfig may
174
+ // not reach for ../../tsconfig.json or ../../packages/* (that is TS5083
175
+ // for anyone who scaffolds a project of their own), and its package.json
176
+ // must carry real dependency versions plus the typescript/vitest
177
+ // devDependencies the printed "next steps" rely on.
178
+ // ═══════════════════════════════════════════════════════════════════
179
+ describe('starter template — standalone scaffold', () => {
180
+ const read = (rel: string): string =>
181
+ readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf-8');
182
+
183
+ it('tsconfig is self-contained (no extends, no references, no ../.. paths)', () => {
184
+ const raw = read('../tsconfig.json');
185
+ expect(raw).not.toContain('../..');
186
+ const tsconfig = JSON.parse(raw) as Record<string, unknown>;
187
+ expect(tsconfig.extends).toBeUndefined();
188
+ expect(tsconfig.references).toBeUndefined();
189
+ expect(tsconfig.compilerOptions).toBeDefined();
190
+ // The inlined options every scaffold needs to compile on its own:
191
+ const opts = tsconfig.compilerOptions as Record<string, unknown>;
192
+ expect(opts.module).toBeDefined();
193
+ expect(opts.moduleResolution).toBeDefined();
194
+ expect(opts.strict).toBe(true);
195
+ });
196
+
197
+ it('package.json declares real dependency versions and its own toolchain', () => {
198
+ const pkg = JSON.parse(read('../package.json')) as {
199
+ dependencies?: Record<string, string>;
200
+ devDependencies?: Record<string, string>;
201
+ };
202
+ const deps = pkg.dependencies ?? {};
203
+ expect(Object.keys(deps).length).toBeGreaterThan(0);
204
+ for (const [name, range] of Object.entries(deps)) {
205
+ expect(range, `${name} must pin a real semver range, not "*"`).not.toBe('*');
206
+ }
207
+ const devDeps = pkg.devDependencies ?? {};
208
+ expect(devDeps.typescript, 'typescript devDependency required for `npx tsc`').toBeTruthy();
209
+ expect(devDeps.vitest, 'vitest devDependency required for `npx vitest`').toBeTruthy();
210
+ });
211
+
212
+ it('README does not send users into the monorepo', () => {
213
+ const readme = read('../README.md');
214
+ expect(readme).not.toContain('../../');
215
+ expect(readme.toLowerCase()).not.toContain('copy this directory to `packages/');
216
+ expect(readme).not.toMatch(/cp -r templates\/starter/);
217
+ });
218
+ });
219
+
220
+ // ═══════════════════════════════════════════════════════════════════
221
+ // CROSS-INSTANCE STATE ISOLATION
222
+ // setup.ts inserts entities from module-level constants. A shallow spread
223
+ // (`addEntity({ ...enemy })`) shares the nested resources/stats/statuses
224
+ // objects across every engine built in one process, so combat damage (or
225
+ // the NPC turn driver killing the grunt) in engine A would permanently
226
+ // mutate the constant and a LATER createGame() would boot with a dead
227
+ // grunt. structuredClone at insertion is the fix. Same class as F-71ec5dcd.
228
+ // ═══════════════════════════════════════════════════════════════════
229
+ describe('starter template — cross-instance state isolation', () => {
230
+ it('killing the enemy in engine A does not carry into a fresh engine B', () => {
231
+ const a = createGame(1);
232
+ const fullHp = a.world.entities['grunt'].resources.hp;
233
+ expect(fullHp).toBeGreaterThan(0);
234
+
235
+ a.world.entities['grunt'].resources.hp = 0;
236
+
237
+ const b = createGame(1);
238
+ expect(b.world.entities['grunt'].resources.hp).toBe(fullHp);
239
+ expect(b.world.entities['grunt'].resources)
240
+ .not.toBe(a.world.entities['grunt'].resources);
34
241
  });
35
242
  });
package/tsconfig.json CHANGED
@@ -1,24 +1,23 @@
1
1
  {
2
- "extends": "../../tsconfig.json",
3
2
  "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "declaration": true,
7
+ "sourceMap": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
4
14
  "outDir": "dist",
5
15
  "rootDir": "src"
6
16
  },
7
17
  "include": [
8
18
  "src"
9
19
  ],
10
- "references": [
11
- {
12
- "path": "../../packages/core"
13
- },
14
- {
15
- "path": "../../packages/content-schema"
16
- },
17
- {
18
- "path": "../../packages/modules"
19
- }
20
- ],
21
20
  "exclude": [
22
21
  "**/*.test.ts"
23
22
  ]
24
- }
23
+ }