Step 17: Putting It All Together
What This Version Does
This is the complete Family Zoo as a multi-file story. Every concept from steps 1 through 16 — rooms, objects, containers, locked doors, light and dark, NPCs, event handlers, custom actions, capability dispatch, timed events, and scoring — comes together in a production-ready structure.
Instead of a single .ts file that grows ever longer, v17 splits the story into focused modules. Each file has a single responsibility, making the code easier to navigate, test, and extend.
Multi-File Organization
The v17 directory looks like this:
tutorials/familyzoo/src/v17/
├── index.ts # Story class — wires everything together
├── zoo-map.ts # Rooms, exits, scenery, locked door
├── zoo-items.ts # Portable objects, containers, supporters
├── characters.ts # Zookeeper, parrot, pettable animals, NPC behaviors
├── events.ts # PA announcements, feeding time, after-hours daemons
├── scoring.ts # Score IDs, point values, victory condition
└── language.ts # All player-facing text
Why does this matter? When your story has seven rooms it might fit comfortably in one file. But the Family Zoo already has NPCs, timed events, custom actions, and a scoring system. Finding the code for "what happens when the player feeds the goats" means scrolling past room definitions, NPC setup, grammar patterns, and language text.
With one responsibility per file, you can find code by what it does:
- Room connections wrong? Open
zoo-map.ts. - NPC acting oddly? Open
characters.ts. - Typo in a message? Open
language.ts. - Score not awarding? Open
scoring.ts.
The Story Class
index.ts is the hub. It implements the Story interface and imports from the other files, wiring everything together through Sharpee's lifecycle methods:
| Method | What It Does |
|---|---|
initializeWorld() | Calls helpers in zoo-map.ts, zoo-items.ts, characters.ts, and scoring.ts to build the world |
createPlayer() | Creates the player entity and places them at the entrance |
onEngineReady() | Registers NPC plugin, scheduler plugin, event handlers from events.ts |
extendParser() | Adds story-specific grammar patterns (feed, photograph, pet) |
extendLanguage() | Delegates to language.ts to register all message text |
getCustomActions() | Returns the feed, photograph, and petting actions |
The story class itself stays short — it delegates to the specialist modules rather than containing all the logic.
Entity Helpers
By v17, the story uses world.helpers() instead of manual entity creation. The helpers provide a fluent builder pattern that is more readable and less error-prone:
const { room, object, door, container, actor } = world.helpers();
const entrance = room('Zoo Entrance')
.description('You stand before the wrought-iron gates of the city zoo.')
.aliases('entrance', 'gates')
.build();
const brochure = object('zoo brochure')
.description('A colorful tri-fold brochure.')
.aliases('brochure', 'pamphlet')
.readable('Welcome to the Family Zoo! ...')
.in(entrance)
.build();
const backpack = container('backpack')
.description('A sturdy canvas backpack.')
.aliases('pack', 'bag')
.capacity(5)
.openable({ isOpen: false })
.in(entrance)
.build();
Compare this to the v01 approach of creating an entity, then adding IdentityTrait, then LocationTrait, then each additional trait one by one. The helpers do the same thing under the hood, but the code reads like a description of the thing you're building.
The Language Layer
All player-facing text lives in language.ts. No English strings appear in the story logic files. This is the same pattern Sharpee uses internally — the engine emits message IDs, and the language layer provides the actual prose.
// language.ts
export function registerMessages(language: LanguageProvider): void {
// Feeding
language.addMessage('zoo.feeding.fed_goats',
'You scatter the feed on the ground. The goats rush over!');
language.addMessage('zoo.feeding.already_fed',
'The goats are still happily munching away.');
// Petting
language.addMessage('zoo.petting.goats',
'You reach over the fence and pet a goat. It bleats contentedly.');
language.addMessage('zoo.petting.parrot',
'You extend a finger. The parrot bites it. Hard.');
// Timed events
language.addMessage('zoo.pa.announcement',
'A tinny PA speaker crackles: "The zoo closes in one hour."');
// ...
}
This pays off in several ways: you can review all your game text in one place, a proofreader can edit language.ts without touching game logic, and if you ever want to translate the game, you write a second language file and swap it in.
Build and Play
npx @sharpee/sharpee build
open dist/web/index.html
What You Learned
Over 17 steps, you built a complete interactive fiction game. Here is what each step covered:
| Step | Concept |
|---|---|
| 1 | Story interface, entities, traits — a single room |
| 2 | Room connections and compass directions |
| 3 | Scenery — non-portable objects that enrich rooms |
| 4 | Portable objects, inventory, taking and dropping |
| 5 | Containers and supporters |
| 6 | Openable containers with hidden contents |
| 7 | Locked doors and keys |
| 8 | Light, dark rooms, and switchable light sources |
| 9 | Readable objects |
| 10 | Switchable devices |
| 11 | Non-player characters with behaviors |
| 12 | Event handlers and chain events |
| 13 | Custom actions with the four-phase pattern |
| 14 | Capability dispatch — per-entity verb behaviors |
| 15 | Timed events with daemons and fuses |
| 16 | Scoring and endgame conditions |
| 17 | Multi-file organization and entity helpers |
You now have the tools to build your own interactive fiction with Sharpee. Start with a single room, add things one concept at a time, and split into multiple files when the code outgrows a single module. The architecture scales from a tutorial to a full-sized game.
The Code
See src/v17/ for the complete, multi-file source.