Handlers
Handlers
Les handlers sont le pont entre le engine et votre jeu. Ils fonctionnent comme des observateurs — vous abonnez une fonction, et le engine l'exécute quand l'événement correspondant se produit. C'est à travers eux que vous déclenchez les bons comportements dans votre moteur : afficher du texte, jouer une animation, évaluer un état, etc.
Le engine expose les handlers suivants :
| Handler | Niveau | Description |
|---|---|---|
onDialog | global / scene | Block dialog — afficher du texte |
onChoice | global / scene | Block choice — présenter des choix |
onCondition | global / scene | Block condition — évaluer et brancher |
onAction | global / scene | Block action — déclencher des effets |
onResolveCharacter | global / scene | Résoudre quel personnage parle |
onBeforeBlock | global | Avant chaque block (delay, animations d'entrée…) |
onValidateNextBlock | global | Valider avant de progresser vers un block |
onInvalidateBlock | global | Réagir quand la validation échoue |
onSceneEnter | global / scene | Une scène démarre |
onSceneExit | global / scene | Une scène se termine |
onBlock | scene | Override un block par son id (DIALOG-001) |
onDialogId | scene | Override un block DIALOG par son id (type-safe) |
onChoiceId | scene | Override un block CHOICE par son id (type-safe) |
onConditionId | scene | Override un block CONDITION par son id (type-safe) |
onActionId | scene | Override un block ACTION par son id (type-safe) |
onResolveCondition | global | Résolveur unifié de conditions (visibilité des choix + pré-évaluation des conditions) |
Les quatre handlers de type — onDialog, onChoice, onCondition, onAction — sont requis : le engine valide leur présence à l'appel de start() et lève une erreur nommant ceux qui manquent. Seule exception, onCondition devient facultatif dès que onResolveCondition est installé : le engine connaît alors déjà le port de sortie, et le handler ne sert plus qu'à journaliser ou à forcer une sortie.
Un block ROUTER n'a pas de handler et n'en a pas besoin : le engine évalue chaque cas, lance le port de chaque cas vrai et continue tout seul par then (tous ont tenu) ou catch (un a manqué). Pour l'observer, handle.onBlock(id) — voir Le bloc Router.
// required — the engine won't start without these
engine.onDialog(dialogHandler);
engine.onChoice(choiceHandler);
engine.onCondition(conditionHandler);
engine.onAction(actionHandler);
// optional — lifecycle, validation, pre-execution
engine.onBeforeBlock(beforeBlockHandler);
engine.onResolveCharacter(resolveCharacterHandler);
engine.onValidateNextBlock(validateHandler);
engine.onSceneEnter(sceneEnterHandler);
engine.onSceneExit(sceneExitHandler);
const handle = engine.scene(sceneId);
handle.start();// required — the engine won't start without these
engine.OnDialog(dialogHandler);
engine.OnChoice(choiceHandler);
engine.OnCondition(conditionHandler);
engine.OnAction(actionHandler);
// optional — lifecycle, validation, pre-execution
engine.OnBeforeBlock(beforeBlockHandler);
engine.OnResolveCharacter(resolveCharacterHandler);
engine.OnValidateNextBlock(validateHandler);
engine.OnSceneEnter(sceneEnterHandler);
engine.OnSceneExit(sceneExitHandler);
var handle = engine.Scene(sceneId);
handle.Start();// required — the engine won't start without these
engine.onDialog(dialogHandler);
engine.onChoice(choiceHandler);
engine.onCondition(conditionHandler);
engine.onAction(actionHandler);
// optional — lifecycle, validation, pre-execution
engine.onBeforeBlock(beforeBlockHandler);
engine.onResolveCharacter(resolveCharacterHandler);
engine.onValidateNextBlock(validateHandler);
engine.onSceneEnter(sceneEnterHandler);
engine.onSceneExit(sceneExitHandler);
auto handle = engine.scene(sceneId);
handle->start();# required — the engine won't start without these
engine.on_dialog(dialog_handler)
engine.on_choice(choice_handler)
engine.on_condition(condition_handler)
engine.on_action(action_handler)
# optional — lifecycle, validation, pre-execution
engine.on_before_block(before_block_handler)
engine.on_resolve_character(resolve_character_handler)
engine.on_validate_next_block(validate_handler)
engine.on_scene_enter(scene_enter_handler)
engine.on_scene_exit(scene_exit_handler)
var handle = engine.scene(scene_id)
handle.start()Two-Tier Handler System
Le engine résout les handlers sur deux niveaux :
- Global handlers — enregistrés sur le engine, ils définissent le comportement par défaut de chaque scène. Ils suffisent dans la majorité des cas.
- Scene handlers — enregistrés sur un
SceneHandlespécifique, ils permettent de court-circuiter ou d'étendre le comportement par défaut quand une scène nécessite un rendu ou un contrôle différent. C'est rare, mais disponible.
Quand un block est dispatché, le engine résout le handler dans cet ordre :
handle.onBlock(blockId)ouhandle.onDialogId(blockId)/handle.onActionId(blockId)/ ... — override spécifique à un blockhandle.onDialog()/handle.onChoice()/ ... — handler de type au niveau scèneengine.onDialog()/engine.onChoice()/ ... — handler global
Quand les deux niveaux sont présents, les deux s'exécutent en séquence — scène d'abord, puis global — sauf si le scene handler appelle context.preventGlobalHandler() pour supprimer le passage global.
// Tier 1 — global
engine.onDialog(({ block, context, next }) => {
console.log('Global dialog handler');
next();
});
// Tier 2 — scene-specific
const handle = engine.scene(sceneId);
handle.onDialog(({ block, context, next }) => {
console.log('Scene-specific dialog handler');
context.preventGlobalHandler();
next();
});
handle.start();// Tier 1 — global
engine.OnDialog(args => {
Console.WriteLine("Global dialog handler");
args.Next();
return null;
});
// Tier 2 — scene-specific
var handle = engine.Scene(sceneId);
handle.OnDialog(args => {
Console.WriteLine("Scene-specific dialog handler");
args.Context.PreventGlobalHandler();
args.Next();
return null;
});
handle.Start();// Tier 1 — global
engine.onDialog([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
std::cout << "Global dialog handler\n";
next();
return {};
});
// Tier 2 — scene-specific
auto handle = engine.scene(sceneId);
handle->onDialog([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
std::cout << "Scene-specific dialog handler\n";
ctx->preventGlobalHandler();
next();
return {};
});
handle->start();# Tier 1 — global
engine.on_dialog(func(args):
print("Global dialog handler")
args["next"].call()
return Callable()
)
# Tier 2 — scene-specific
var handle = engine.scene(scene_id)
handle.on_dialog(func(args):
print("Scene-specific dialog handler")
args["context"].prevent_global_handler()
args["next"].call()
return Callable()
)
handle.start()Character Resolution
Le système de résolution de personnage est optionnel. En enregistrant un callback onResolveCharacter, le engine l'invoque avant chaque block qui contient des personnages dans ses actors. Le callback reçoit la liste des personnages assignés au block et retourne celui qui doit être actif — ou undefined si aucun n'est disponible. Le personnage résolu est ensuite accessible via context.character dans tous les handlers.
C'est le point d'intégration idéal pour interroger l'état de votre jeu : vérifier si un personnage est présent dans la scène, en vie, dans le champ de la caméra, etc. Retourner undefined ouvre la porte à plusieurs stratégies : sauter le block via skipIfMissingActor, annuler la scène via handle.cancel(), ou gérer le cas directement dans le handler.
// Engine-level — applies to all scenes
engine.onResolveCharacter((characters) => {
return party.getActiveLeader(characters);
});
// Scene-level override
const handle = engine.scene(sceneId);
handle.onResolveCharacter((characters) => {
return battle.getActiveUnit(characters);
});engine.OnResolveCharacter(chars => party.GetActiveLeader(chars));
var handle = engine.Scene(sceneId);
handle.OnResolveCharacter(chars => battle.GetActiveUnit(chars));engine.onResolveCharacter([](const auto& chars) {
return party.getActiveLeader(chars);
});
auto handle = engine.scene(sceneId);
handle->onResolveCharacter([](const auto& chars) {
return battle.getActiveUnit(chars);
});engine.on_resolve_character(func(chars):
return party.get_active_leader(chars)
)
var handle = engine.scene(scene_id)
handle.on_resolve_character(func(chars):
return battle.get_active_unit(chars)
)Scene Lifecycle
Les callbacks onSceneEnter et onSceneExit permettent de réagir au démarrage et à la fin d'une scène — activer un mode cinématique, arrêter les NPC, préparer l'UI, nettoyer les ressources, etc. Ils sont disponibles au niveau global (sur le engine) et au niveau scène (via handle.onEnter() / handle.onExit()). Le scene handler remplace le global s'il est défini.
engine.onSceneEnter(({ scene }) => {
game.cinemaMode(true);
game.stopNpcMovements();
});
engine.onSceneExit(() => {
game.cinemaMode(false);
game.resumeNpcMovements();
});
// scene-level override
const handle = engine.scene(sceneId);
handle.onEnter(({ scene }) => {
game.playIntroSequence(scene);
});engine.OnSceneEnter(args => {
Game.CinemaMode(true);
Game.StopNpcMovements();
});
engine.OnSceneExit(args => {
Game.CinemaMode(false);
Game.ResumeNpcMovements();
});
// scene-level override
var handle = engine.Scene(sceneId);
handle.OnEnter(args => {
Game.PlayIntroSequence(args.Scene);
});engine.onSceneEnter([&game](const lsde::SceneLifecycleArgs&) {
game.cinemaMode(true);
game.stopNpcMovements();
});
engine.onSceneExit([&game](const lsde::SceneLifecycleArgs&) {
game.cinemaMode(false);
game.resumeNpcMovements();
});
// scene-level override
auto handle = engine.scene(sceneId);
handle->onEnter([&game](const lsde::SceneLifecycleArgs& args) {
game.playIntroSequence(args.scene);
});engine.on_scene_enter(func(args):
game.cinema_mode(true)
game.stop_npc_movements()
)
engine.on_scene_exit(func(args):
game.cinema_mode(false)
game.resume_npc_movements()
)
# scene-level override
var handle = engine.scene(scene_id)
handle.on_enter(func(args):
game.play_intro_sequence(args["scene"])
)Block Override
onBlock(blockId) permet de cibler un block précis par son identifiant pour lui attribuer un handler dédié. C'est un cas d'usage rare — les handlers génériques couvrent la grande majorité des besoins — mais pour des scénarios très spécifiques où un block individuel nécessite un comportement distinct, c'est disponible.
const handle = engine.scene(sceneId);
handle.onBlock('DIALOG-001', ({ block, context, next }) => {
next();
});var handle = engine.Scene(sceneId);
handle.OnBlock("DIALOG-001", args => {
args.Next();
return null;
});auto handle = engine.scene(sceneId);
handle->onBlock("DIALOG-001", [](auto*, auto*, auto*, auto next) -> CleanupFn {
next();
return {};
});var handle = engine.scene(scene_id)
handle.on_block("DIALOG-001", func(args):
args["next"].call()
return Callable()
)Type-Safe Block Override
onDialogId(blockId), onChoiceId(blockId), onConditionId(blockId) et onActionId(blockId) sont des alternatives type-safe à onBlock(blockId). Ils fonctionnent exactement de la même façon — même priorité, même support de preventGlobalHandler — mais le handler reçoit le type de block spécialisé et le contexte au lieu de l'union générique.
Utilisez-les quand vous connaissez le type du block au moment de l'enregistrement et que vous voulez l'autocomplétion complète sur block et context.
const handle = engine.scene(sceneId);
handle.onActionId('ACTION-001', ({ block, context, next }) => {
// block is ActionBlock — actions is directly accessible
for (const action of block.actions ?? []) {
executeAction(action);
}
// context is ActionContext — resolve/reject are available
context.resolve();
next();
});var handle = engine.Scene(sceneId);
handle.OnActionId("ACTION-001", args => {
// args.Block is ActionBlock — Actions is directly accessible
foreach (var action in args.Block.Actions ?? [])
ExecuteAction(action);
// args.Context is IActionContext — Resolve/Reject are available
args.Context.Resolve();
args.Next();
});auto handle = engine.scene(sceneId);
handle->onActionId("ACTION-001", [](auto* scene, const ActionBlock* block, IActionContext* ctx, auto next) -> CleanupFn {
// block is const ActionBlock* — actions is directly accessible
for (const auto& action : block->actions)
executeAction(action);
// ctx is IActionContext* — resolve/reject are available
ctx->resolve();
next();
return {};
});var handle = engine.scene(scene_id)
handle.on_action_id("ACTION-001", func(args):
# args["block"] contains actions directly
for action in args["block"].get("actions", []):
execute_action(action)
# args["context"] has resolve/reject
args["context"]["resolve"].call()
args["next"].call()
return Callable()
)