miaoda-game-scenario-core
v0.3.0
Published
Engine-agnostic story / event-script runtime: a safe expression evaluator, a compact text DSL, variable memory, and a pausable runner with label/goto jumps, if/else branches and player choices. No rendering, no engine dependency. Backs visual-novel / AVG,
Maintainers
Readme
miaoda-game-scenario-core
Use this engine-independent runtime for branching dialogue, cutscenes, quests, and event scripts. Authors provide line-oriented text; the runtime parses labels, conditions, choices, variables, jumps, and blocking host commands.
Install
pnpm add miaoda-game-scenario-coreScript and run
import { Memory, ScenarioRunner } from 'miaoda-game-scenario-core';
const script = `
label start
say Guard "Halt. Toll is 5 coin."
choice "Pay" -> pay if coin >= 5
choice "Leave" -> leave
label pay
set coin = coin - 5
say Guard "Pass."
exit
label leave
say You "Another time."
exit
`;
const runner = new ScenarioRunner({
memory: new Memory({ coin: 8 }),
commandExecutor: async (ctx) => {
if (ctx.name === 'say') await dialogue.show(ctx.args[0], ctx.args[1]);
},
choiceHandler: (options) => menu.present(options),
});
runner.load(script);
await runner.run();Built-in control statements are label, goto, if/elseif/else/endif, set, choice, and exit. Other verbs such as say, show, and playBgm go to your commandExecutor. Command arguments are unquoted strings; convert numeric values yourself. Return a promise to pause until the host operation finishes.
Choice options are already filtered by their guards. Resolve with the option's index, not an index from the full authored list. Unknown commands are no-ops, so explicitly validate script verbs if a typo should fail authoring.
Cross-engine dialogue presentation
DialoguePlayer provides the engine-independent reveal/page/advance state machine. Pass a
string containing explicit \f page breaks, or an array of pages measured by the host UI.
Drive update(deltaSeconds) from Phaser/Cocos update or a React timer, subscribe once to
render snapshot.visibleText, and bind one input action to advance().
const dialogue = new DialoguePlayer({ charactersPerSecond: 30 });
const unsubscribe = dialogue.subscribe((view) => renderText(view.speaker, view.visibleText));
dialogue.start({ speaker: 'Guard', text: ['Halt.', 'State your business.'] });
dialogue.update(deltaSeconds);
dialogue.advance(); // reveal page, then next page, then completeThe player deliberately does not measure fonts, create timers, subscribe to input, or own
engine objects. Concurrent start, invalid timing, and resetting active dialogue throw instead
of silently replacing presentation state. This makes the same contract suitable for Phaser,
Cocos, React DOM, tests, and coding-agent generated hosts.
Save and restore
Memory.toJSON() is suitable for chapter/label saves; restore it and call run(startLabel). For exact continuation, save runner.snapshot at a quiescent statement boundary, load the same script, call loadSnapshot(snapshot), then resume(). Snapshots waiting on a command or choice are rejected because host UI work cannot be safely replayed. The script signature detects content mismatch but is not authentication.
Public API
DialoguePlayer, parse, compile, analyzeScenario, evaluate, evaluateCondition, Memory, ScenarioRunner, CommandContext, ChoiceOption, and scenario statement types are exported. The runtime does not render dialogue, create timers, play audio, or own a scene.
