Skip to content

Chapter 58 — Create Your Own Starter

The fastest path to a new game: scaffold from the CLI.

Terminal window
ai-rpg-engine create-starter my-game

This generates packages/starter-my-game/ with:

  • buildCombatStack as the default combat composition
  • A marked starter-owned systems section for your custom modules
  • A pressure example (createTensionPressure) to build from
  • A smoke test that validates the scaffold boots

Then:

Terminal window
cd packages/starter-my-game
npm install
npx tsc --noEmit
npx vitest run
Terminal window
ai-rpg-engine create-starter my-game --out=./somewhere-else
ai-rpg-engine create-starter my-game --force # overwrite existing

If you prefer to work without the CLI, the template is published on npm as an artifact to copy from (not a runtime dependency):

Terminal window
npm pack @ai-rpg-engine/starter-template
tar -xzf ai-rpg-engine-starter-template-*.tgz
mv package packages/starter-mygame
rm ai-rpg-engine-starter-template-*.tgz

Or inside the monorepo:

Terminal window
cp -r templates/starter packages/starter-mygame
LayerOwnerYou Touch?
Combat formulas (hit/damage/dodge)buildCombatStackNo
Cognition, engagement, tacticsbuildCombatStackNo
Recovery, intent, narrationbuildCombatStackNo
Stat mappingYouConfig only
Resource profileYouConfig only
Traversal + zonesYouYes
Entities + contentYouYes
Custom modulesYouYes
Ruleset definitionYouYes

1. src/ruleset.ts — Declarative contract

Section titled “1. src/ruleset.ts — Declarative contract”

Define your stats, resources, verbs, and formulas. The three combat stats must map to attack, precision, and resolve via buildCombatStack’s statMapping.

stats: [
{ id: 'brawn', name: 'Brawn', min: 1, max: 20, default: 5 }, // → attack
{ id: 'reflex', name: 'Reflex', min: 1, max: 20, default: 5 }, // → precision
{ id: 'nerve', name: 'Nerve', min: 1, max: 20, default: 5 }, // → resolve
],

Add your starter-specific resource — this is what makes your game feel different:

resources: [
{ id: 'hp', name: 'HP', min: 0, max: 100, default: 25 },
{ id: 'guts', name: 'Guts', min: 0, max: 50, default: 10 }, // your pressure lever
],

Define your manifest, player, enemies, NPCs, and zones. Zones need id, roomId, name, tags, and neighbors.

This is where the composition contract lives:

const combat = buildCombatStack({
statMapping: { attack: 'brawn', precision: 'reflex', resolve: 'nerve' },
playerId: 'player',
recovery: { safeZoneTags: ['safe'] },
});
const engine = new Engine({
modules: [
traversalCore,
statusCore,
...combat.modules, // ← the full combat stack (9 modules)
// YOUR MODULES HERE
createMyCustomPressure(),
],
});

At minimum, verify:

  • Ruleset validates against schema
  • createGame() boots without throwing
  • Your custom resource exists on the player
import type { EngineModule, ResolvedEvent, WorldState } from '@ai-rpg-engine/core';
function createMyPressure(): EngineModule {
return {
id: 'my-pressure',
version: '1.0.0',
register(ctx) {
ctx.events.on('combat.round.end', (event: ResolvedEvent, world: WorldState) => {
const p = world.entities['player'];
if (p) {
p.resources.guts = Math.max(0, (p.resources.guts ?? 0) - 3);
}
});
},
};
}

If your combat costs resources (stamina for attacks, mana for abilities), configure resourceProfile:

buildCombatStack({
statMapping: { attack: 'brawn', precision: 'reflex', resolve: 'nerve' },
resourceProfile: {
spends: [
{ verbId: 'attack', costStat: 'stamina', amount: 2 },
{ verbId: 'use-ability', costStat: 'mana', amount: 5 },
],
},
});

Control AI belief decay and morale:

buildCombatStack({
// ...
cognition: {
decay: { baseRate: 0.03, pruneThreshold: 0.1 },
moraleFleeThresholds: { low: 20, critical: 10 },
},
// cognition: false, // ← to exclude AI cognition entirely
});
  • Three stats mapped to attack/precision/resolve
  • At least one starter-specific resource
  • buildCombatStack with statMapping (no custom formulas needed)
  • At least one custom module demonstrating your game’s pressure
  • createGame() boots and tests pass
  • package.json name follows @ai-rpg-engine/starter-* convention