Visual novel example
The examples/ folder ships a complete visual novel built on me-gosta. It demonstrates the full lifecycle: data authoring → engine bootstrap → PixiJS rendering → dialogue UI → save/load.
Project structure
examples/src/
├── engine/ # GameEngine wrapper, React provider, PixiJS stage
├── assets/ # Scenario data, character sprites, scene backgrounds
│ └── dialogues/ # Branching dialogue trees (rooftop scenario)
├── components/
│ └── dialogue/ # DialogueOverlay, typewriter hook, option selection
└── App.tsx # Demo harness: start → play → game-over statesScenario data
A full GameBaseData defines the entire game state — narrative nodes, scenes, dialogues, characters, variables, achievements — in one serializable object:
import {
achievementId,
chapterId,
characterId,
dialogueId,
type GameBaseData,
narrativeNodeId,
playerActionId,
sceneId,
variableId,
} from "@tikab-interactive/me-gosta";
// Full scenario definition — one serializable object owns all game content
const scenario: GameBaseData = {
notificationManager: {},
narrative: {
firstNodeId: narrativeNodeId("node_rooftop_intro"),
nodes: [
{
id: narrativeNodeId("node_rooftop_intro"),
effects: [
{
kind: "scene",
name: "set-scene",
sceneId: sceneId("scene_rooftop"),
},
{
kind: "dialogue",
name: "start-dialogue",
dialogueId: dialogueId("dlg_rooftop"),
},
],
conditions: [],
children: [],
},
],
},
dialogueManager: {
dialogues: [
{
id: dialogueId("dlg_rooftop"),
name: "Rooftop Evening",
firstMessages: [{ id: "intro" }],
messages: {
intro: {
id: "intro",
internalMonologue: true,
message: "What should I say?",
options: [
{
text: "Look at Jacob",
nextMessages: [{ id: "greeting" }],
},
],
},
greeting: {
id: "greeting",
speaker: { characterId: characterId("char_jacob") },
message: "Hey. Everything okay up here?",
options: [],
},
},
},
],
dialoguePoints: [],
},
characterManager: {
characters: [
{
id: characterId("char_jacob"),
name: "Jacob",
morale: 3,
trust: 3,
stress: 1,
expression: "Neutral",
},
],
},
sceneManager: {
scenes: [
{
id: sceneId("scene_rooftop"),
objects: {
background: {
kind: "image",
imageUrl: "https://cdn.example.com/rooftop.webp",
position: { x: 0, y: 0 },
scale: 1,
depth: 0,
},
},
},
],
},
timeManager: { time: 0, timeStep: 1 },
achievementManager: {
achievements: [
{ id: achievementId("achv_rooftop"), description: "Visited rooftop" },
],
actions: [
{ id: playerActionId("act_flirt"), description: "Flirted with Jacob" },
],
},
chapterManager: {
chapters: [{ id: chapterId("chapter_1"), name: "Rooftop Evening" }],
},
variableManager: {
variables: [{ id: variableId("var_energy"), type: "int", value: 1 }],
},
};
export { scenario };Dialogue authoring
Messages are keyed by ID in a flat map. Options on each message branch the tree. Effects and conditions on options gate progression and modify state:
import type { DialogueData } from "@tikab-interactive/me-gosta";
import { characterId, playerActionId } from "@tikab-interactive/me-gosta";
const jacobId = characterId("char_jacob");
const flirtAction = playerActionId("act_flirt");
// Dialogue options carry effects (fired on selection) and conditions (gate visibility)
const messages: DialogueData["messages"] = {
jacobGreeting: {
id: "jacobGreeting",
speaker: { characterId: jacobId },
message: "Hey, you look thoughtful. Everything okay up here?",
options: [
{
text: "Flirt: the view is almost as good as his smile",
effects: [
{
kind: "achievement",
name: "perform-action",
playerActionId: flirtAction,
},
{
kind: "character",
name: "change-trust",
characterId: jacobId,
amount: 2,
},
{
kind: "character",
name: "change-stress",
characterId: jacobId,
amount: -1,
},
],
nextMessages: [{ id: "jacobTease" }],
},
{
text: "Offer a dramatic reading for coffee",
// Only visible if trust >= 5 AND the player previously flirted
conditions: [
{
name: "character-trust",
characterId: jacobId,
kind: "min",
threshold: 5,
},
{ name: "action-performed", playerActionId: flirtAction },
],
nextMessages: [{ id: "coffeeEnding" }],
},
],
},
};
export { messages };Key patterns:
internalMonologue: true— renders differently in the UI (no speaker name, italic styling)effectson options — fire when selected (character trust changes, achievement unlocks)conditionson options — only show the option if met (trust threshold, action performed)
Engine lifecycle
import {
GameBase,
type GameBaseData,
GameBaseEffectSchema,
GameEffectSchemaRegister,
type SaveGameBase,
SaveGameBaseSchema,
} from "@tikab-interactive/me-gosta";
// 1. Register effect schemas (once at startup)
GameEffectSchemaRegister.instance.register(GameBaseEffectSchema);
// 2. Create game from authored data
declare const scenarioData: GameBaseData;
const game = new GameBase(scenarioData);
game.getNarrativeTree().init();
// 3. Save — produces a serializable snapshot
const snapshot: SaveGameBase = game.save();
// 4. Load — validate then rehydrate
const parsed = SaveGameBaseSchema.safeParse(snapshot);
if (parsed.success && parsed.data) {
game.load(parsed.data);
}
// 5. Fresh restart — new GameBase from same data
const freshGame = new GameBase(scenarioData);
freshGame.getNarrativeTree().init();
export { game, freshGame };The engine exposes observables (game$, currentScene$, currentDialogueId$) so the React layer subscribes and re-renders without polling.
PixiJS rendering
The PixiStage component subscribes to currentScene$. When the scene changes, it:
- Destroys current sprites
- Loads new background and character textures via
Assets.load() - Positions sprites using normalized coordinates ([-1, 1] → viewport center)
- Respects
depthfor z-ordering (background at 0, characters at 5+)
Scene data drives the renderer — no imperative draw calls in game logic.
Dialogue UI
useDialogue hook subscribes to DialogueManager.getCurrentDialogue() and the current message observable. It drives:
- Typewriter text animation (Typed.js)
- Speaker name display
- Numbered option buttons (1–9 hotkeys + Enter to advance)
- Dialogue-end detection
When the player selects an option, effects fire immediately through GameBase.handleEffect(), which routes to the relevant manager (character trust, scene change, achievement unlock, etc.).