Core concepts
Branded IDs
Every domain entity uses a branded string ID (SceneId, DialogueId, CharacterId, VariableId, etc.). These are created via factory helpers and validated with Zod schemas at runtime.
import {
DialogueIdSchema,
dialogueId,
GameBaseDataSchema,
sceneId,
variableId,
} from "@tikab-interactive/me-gosta";
const scene = sceneId("scene_intro");
const dialogue = dialogueId("dlg_intro");
const variable = variableId("trust");
const dialogueParse = DialogueIdSchema.safeParse(dialogue);
const gameParse = GameBaseDataSchema.safeParse({});
export const usageSummary = {
scene,
dialogue,
variable,
dialogueParseOk: dialogueParse.success,
gameParseOk: gameParse.success,
};The branded types prevent accidental cross-domain assignment at compile time while remaining plain strings at runtime (serializable, indexable, debuggable).
Effects
Player actions produce effects — a discriminated union keyed by kind. GameBase.handleEffect() dispatches each to the owning manager:
import type { GameBaseEffect } from "@tikab-interactive/me-gosta";
import { variableId } from "@tikab-interactive/me-gosta";
const baseEffects: GameBaseEffect[] = [
{
kind: "variable",
name: "set-variable",
variableId: variableId("trust"),
value: 10,
},
];
export function enqueueEffects(next: GameBaseEffect[]) {
return [...baseEffects, ...next];
}Effects are attached to dialogue options, narrative node transitions, or triggered programmatically. The full set of kinds: dialogue, scene, character, variable, time, achievement, notification.
Dialogue trees
A dialogue is a flat map of messages keyed by ID. Each message has optional speaker, internalMonologue flag, and a list of options. Options can carry conditions (gates) and effects (side-effects on selection).
import type { DialogueData } from "@tikab-interactive/me-gosta";
import { characterId, playerActionId } from "@tikab-interactive/me-gosta";
const jacobId = characterId("char_jacob");
const flirtActionId = playerActionId("act_flirt");
const messages: DialogueData["messages"] = {
intro: {
id: "intro",
internalMonologue: true,
message: "I am feeling nervous. What should I say?",
options: [
{
text: "Steady yourself and look at Jacob",
nextMessages: [{ id: "jacobGreeting" }],
},
{
text: "Stall by admiring the skyline",
nextMessages: [{ id: "skylineMonologue" }],
},
],
},
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: flirtActionId,
},
{
kind: "character",
name: "change-trust",
characterId: jacobId,
amount: 2,
},
],
nextMessages: [{ id: "jacobTease" }],
},
{
text: "Play it safe: talk about the weather",
effects: [
{
kind: "character",
name: "change-trust",
characterId: jacobId,
amount: -1,
},
],
nextMessages: [{ id: "weatherChat" }],
},
],
},
coffeeEnding: {
id: "coffeeEnding",
speaker: { characterId: jacobId },
message: "Coffee sounds perfect. Let's sneak out.",
options: [
{
text: "Accept the invitation",
conditions: [
{
name: "character-trust",
characterId: jacobId,
kind: "min",
threshold: 5,
},
{ name: "action-performed", playerActionId: flirtActionId },
],
nextMessages: [],
},
],
},
};
export { messages };Conditions reference character state (character-trust, threshold) or prior actions (action-performed). This enables gated branches without external logic.
Narrative graph
NarrativeTree is a DAG of nodes. Each node carries an effect list (what happens when the node activates) and a condition list (whether it's reachable). The firstNodeId kicks off traversal.
import type { GameBaseData } from "@tikab-interactive/me-gosta";
import {
dialogueId,
narrativeNodeId,
sceneId,
} from "@tikab-interactive/me-gosta";
const narrative: GameBaseData["narrative"] = {
firstNodeId: narrativeNodeId("node_rooftop"),
nodes: [
{
id: narrativeNodeId("node_rooftop"),
effects: [
{ kind: "scene", name: "set-scene", sceneId: sceneId("scene_rooftop") },
{
kind: "dialogue",
name: "start-dialogue",
dialogueId: dialogueId("dlg_rooftop"),
},
],
conditions: [],
children: [narrativeNodeId("node_cafe")],
},
{
id: narrativeNodeId("node_cafe"),
effects: [
{ kind: "scene", name: "set-scene", sceneId: sceneId("scene_cafe") },
],
conditions: [
// Only reachable if trust is high enough
],
children: [],
},
],
};
export { narrative };Scenes
A scene is a composition of visual objects — backgrounds, character sprites, clickable areas. The renderer (PixiJS in the example) reads the scene data and renders accordingly.
import type { SceneData } from "@tikab-interactive/me-gosta";
import { characterId, sceneId } from "@tikab-interactive/me-gosta";
const scene: SceneData = {
id: sceneId("scene_rooftop"),
objects: {
background: {
kind: "image",
imageUrl: "https://cdn.example.com/rooftop-night.webp",
position: { x: 0, y: 0 },
scale: 1,
depth: 0,
},
jacob: {
kind: "character",
characterId: characterId("char_jacob"),
spritesheet: {
assetId: "https://cdn.example.com/jacob-neutral.json",
frameKey: "neutral",
},
position: { x: -0.2, y: 0 },
scale: 1,
depth: 5,
},
},
};
export { scene };Save / Load
GameBase.save() serializes all manager state into a SaveGameBase. GameBase.load() rehydrates from it. The schema validates on load, making migrations explicit — if the shape changes, the parse fails and you handle it.
import type { SaveGameBase } from "@tikab-interactive/me-gosta";
export function cloneSave(save: SaveGameBase): SaveGameBase {
return structuredClone(save);
}