Visibilité des choix
Aperçu
Quand un block CHOICE est dispatché, context.choices contient toujours tous les choix définis dans le blueprint — rien n'est pré-filtré. Le engine n'enlève jamais de choix du array.
Pour du filtrage de visibilité (ex. cacher des choix basés sur le game state ou des sélections précédentes), le engine fournit un système de tagging opt-in. Un condition resolver est installé une seule fois, et le engine tag chaque choix avec visible: true | false avant que le handler onChoice le reçoive.
Setup
Enregistrez un condition resolver sur le engine — une seule fois, avant de démarrer une scène :
engine.onResolveCondition((condition) => {
// Evaluate game-state conditions only.
// choice: conditions are handled internally by the engine.
return gameState.check(condition.key, condition.operator, condition.value);
});engine.OnResolveCondition(cond => {
return GameState.Check(cond.Key, cond.Operator, cond.Value);
});engine.onResolveCondition([](const ExportCondition& cond) {
return gameState.check(cond.key, cond.op, cond.value);
});engine.on_resolve_condition(func(cond):
return game_state.check(cond)
)Quand le resolver est installé, le engine évalue les visibilityConditions de chaque choix avant d'appeler onChoice. Le même resolver pré-évalue aussi les groupes de condition blocks -- voir Condition blocks pour les détails.
- Conditions
choice:(qui référencent des sélections précédentes du joueur) sont résolues automatiquement par le engine via son historique de choix interne — le callback ne les reçoit jamais. - Conditions de game-state (tout le reste) sont déléguées au callback.
- Le chaining avec
&(AND) et|(OR) fonctionne correctement entre les deux types.
Filtrage dans onChoice
Dans le handler, le filtrage se fait avec une seule ligne :
engine.onChoice(({ block, context, next }) => {
const visible = context.choices.filter(c => c.visible !== false);
showChoicesUI(visible, (uuid) => {
context.selectChoice(uuid);
next();
});
});engine.OnChoice(args => {
var visible = args.Context.Choices
.Where(c => c.Visible != false).ToList();
ShowChoicesUI(visible, uuid => {
args.Context.SelectChoice(uuid);
args.Next();
});
return null;
});engine.onChoice([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
std::vector<const RuntimeChoiceItem*> visible;
for (const auto& c : ctx->choices())
if (!c.visible.has_value() || c.visible.value())
visible.push_back(&c);
showChoicesUI(visible, [ctx, next](auto& uuid) {
ctx->selectChoice(uuid);
next();
});
return {};
});engine.on_choice(func(args):
var visible = []
for c in args["context"].choices:
if c.get("visible") != false:
visible.append(c)
show_choices_ui(visible, func(uuid):
args["context"].select_choice(uuid)
args["next"].call()
)
return Callable()
)Pourquoi visible !== false et pas === true?
Quand aucun resolver n'est installé, visible est undefined. Comme undefined !== false donne true, tous les choix passent — rétrocompatible par défaut. Quand un resolver est installé, les choix sont taggés true ou false explicitement.
Valeur de visible | Signification | !== false |
|---|---|---|
true | Resolver installé, le choix passe | true |
false | Resolver installé, choix caché | false |
undefined | Pas de resolver installé | true |
RuntimeChoiceItem
Quand un resolver est installé, chaque choix dans context.choices est un RuntimeChoiceItem — une extension de ChoiceItem avec le tag visible :
interface RuntimeChoiceItem extends ChoiceItem {
visible?: boolean; // true | false | undefined
}public class RuntimeChoiceItem : ChoiceItem
{
public bool? Visible { get; set; } // true | false | null
}struct RuntimeChoiceItem : ChoiceItem {
std::optional<bool> visible; // true | false | nullopt
};# RuntimeChoiceItem is a Dictionary with an extra "visible" key:
# { "uuid": "...", "dialogueText": {...}, "visible": true/false/absent }Sans resolver, les choix sont toujours des RuntimeChoiceItem mais visible reste undefined/null/nullopt/absent.
Exemples
Standard — afficher les choix visibles
engine.onChoice(({ context, next }) => {
const visible = context.choices.filter(c => c.visible !== false);
ui.showChoices(visible, (uuid) => {
context.selectChoice(uuid);
next();
});
});engine.OnChoice(args => {
var visible = args.Context.Choices
.Where(c => c.Visible != false).ToList();
ShowChoicesUI(visible, uuid => {
args.Context.SelectChoice(uuid);
args.Next();
});
return null;
});engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn {
std::vector<const RuntimeChoiceItem*> visible;
for (const auto& c : ctx->choices())
if (!c.visible.has_value() || c.visible.value())
visible.push_back(&c);
showChoicesUI(visible, [ctx, next](const auto& uuid) {
ctx->selectChoice(uuid);
next();
});
return {};
});engine.on_choice(func(args):
var visible = []
for c in args["context"].choices:
if c.get("visible") != false:
visible.append(c)
show_choices_ui(visible, func(uuid):
args["context"].select_choice(uuid)
args["next"].call()
)
return Callable()
)Choix minuté — auto-select au timeout
engine.onChoice(({ block, context, next }) => {
const visible = context.choices.filter(c => c.visible !== false);
const timeout = block.nativeProperties?.timeout;
const resolve = (choice) => {
context.selectChoice(choice.uuid);
next();
};
if (timeout) {
const timer = setTimeout(() => resolve(visible[0]), timeout * 1000);
ui.showChoices(visible, (uuid) => {
clearTimeout(timer);
resolve(visible.find(c => c.uuid === uuid));
});
} else {
ui.showChoices(visible, (uuid) => resolve(visible.find(c => c.uuid === uuid)));
}
});engine.OnChoice(args => {
var (_, block, context, next) = args;
var visible = context.Choices
.Where(c => c.Visible != false).ToList();
var timeout = block.NativeProperties?.Timeout;
void Resolve(RuntimeChoiceItem choice) {
context.SelectChoice(choice.Uuid);
next();
}
if (timeout.HasValue)
{
// use your engine's timer — cancel on player selection
var timer = ScheduleTimer((float)timeout.Value, () => Resolve(visible[0]));
ShowChoicesUI(visible, uuid => {
timer.Cancel();
Resolve(visible.First(c => c.Uuid == uuid));
});
}
else
{
ShowChoicesUI(visible, uuid => Resolve(visible.First(c => c.Uuid == uuid)));
}
return null;
});engine.onChoice([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
std::vector<const RuntimeChoiceItem*> visible;
for (const auto& c : ctx->choices())
if (!c.visible.has_value() || c.visible.value())
visible.push_back(&c);
auto timeout = block->nativeProperties
? block->nativeProperties->timeout : std::nullopt;
auto resolve = [ctx, next](const std::string& uuid) {
ctx->selectChoice(uuid);
next();
};
if (timeout.has_value()) {
// use your engine's timer — cancel on player selection
auto timer = scheduleDelay(timeout.value(), [&]() { resolve(visible[0]->uuid); });
showChoicesUI(visible, [resolve, timer](const auto& uuid) {
timer->cancel();
resolve(uuid);
});
} else {
showChoicesUI(visible, resolve);
}
return {};
});engine.on_choice(func(args):
var ctx = args["context"]
var next_fn = args["next"]
var block = args["block"]
var visible = []
for c in ctx.choices:
if c.get("visible") != false:
visible.append(c)
var timeout_val = block.get("nativeProperties", {}).get("timeout", 0)
if timeout_val > 0:
# use your engine's timer — cancel on player selection
var timer = get_tree().create_timer(timeout_val)
timer.timeout.connect(func():
ctx.select_choice(visible[0]["uuid"])
next_fn.call()
)
show_choices_ui(visible, func(uuid):
timer.time_left = 0 # cancel
ctx.select_choice(uuid)
next_fn.call()
)
else:
show_choices_ui(visible, func(uuid):
ctx.select_choice(uuid)
next_fn.call()
)
return Callable()
)Choix cachés affichés en grisé
engine.onChoice(({ context, next }) => {
for (const choice of context.choices) {
if (choice.visible === false) {
ui.addGreyed(choice); // show but disabled
} else {
ui.addNormal(choice); // selectable
}
}
// wait for player selection...
});engine.OnChoice(args => {
foreach (var choice in args.Context.Choices)
{
if (choice.Visible == false)
AddGreyed(choice); // show but disabled
else
AddNormal(choice); // selectable
}
// wait for player selection...
return null;
});engine.onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn {
for (const auto& choice : ctx->choices()) {
if (choice.visible.has_value() && !choice.visible.value())
addGreyed(choice); // show but disabled
else
addNormal(choice); // selectable
}
// wait for player selection...
return {};
});engine.on_choice(func(args):
for choice in args["context"].choices:
if choice.get("visible") == false:
add_greyed(choice) # show but disabled
else:
add_normal(choice) # selectable
# wait for player selection...
return Callable()
)Tutorial — ignorer complètement la visibilité
tutorial.onChoice(({ context, next }) => {
// force-select the first choice, no filtering
context.selectChoice(context.choices[0].uuid);
next();
});tutorial.OnChoice(args => {
// force-select the first choice, no filtering
args.Context.SelectChoice(args.Context.Choices[0].Uuid);
args.Next();
return null;
});tutorial->onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn {
// force-select the first choice, no filtering
ctx->selectChoice(ctx->choices()[0].uuid);
next();
return {};
});tutorial.on_choice(func(args):
# force-select the first choice, no filtering
args["context"].select_choice(args["context"].choices[0]["uuid"])
args["next"].call()
return Callable()
)Partager l'évaluateur
Avec onResolveCondition, un seul callback gère à la fois la visibilité des choix et la pré-évaluation des condition blocks. Plus besoin de dupliquer la logique :
// One callback handles both choice visibility AND condition block pre-evaluation.
// choice: conditions are resolved internally — you only evaluate game-state conditions.
engine.onResolveCondition((cond) =>
gameState.check(cond.key, cond.operator, cond.value)
);
// onCondition receives pre-evaluated conditionGroups — just route the result.
// onCondition is optional when onResolveCondition is installed.
engine.onCondition(({ block, context, next }) => {
const { conditionGroups } = context;
const isDispatcher = !!block.nativeProperties?.enableDispatcher;
const matched = conditionGroups
.filter((g) => g.result)
.map((g) => g.portIndex);
const result = isDispatcher ? matched : (matched[0] ?? -1);
context.resolve(result);
next();
});// One callback handles both choice visibility AND condition block pre-evaluation.
engine.OnResolveCondition(cond =>
GameState.Instance.Evaluate(cond.Key, cond.Operator, cond.Value));
// onCondition receives pre-evaluated ConditionGroups — just route the result.
engine.OnCondition(args => {
var groups = args.Context.ConditionGroups!;
var isDispatcher = args.Block.NativeProperties?.EnableDispatcher == true;
var matched = groups.Where(g => g.Result == true).Select(g => g.PortIndex).ToList();
object result = isDispatcher ? (object)matched : (object)(matched.Count > 0 ? matched[0] : -1);
args.Context.Resolve(result);
args.Next();
return null;
});// One callback handles both choice visibility AND condition block pre-evaluation.
engine.onResolveCondition([this](const ExportCondition& cond) {
return GetGameState()->Evaluate(cond.key, cond.op, cond.value);
});
// onCondition receives auto-resolved result — just call next().
// The engine pre-evaluates and routes automatically.
engine.onCondition([](auto*, auto* block, auto* ctx, auto next) -> CleanupFn {
// Result is already pre-resolved by the engine.
next();
return {};
});# One callback handles both choice visibility AND condition block pre-evaluation.
engine.on_resolve_condition(func(cond):
return GameState.evaluate(cond.get("key"), cond.get("operator"), cond.get("value"))
)
# on_condition receives pre-evaluated condition_groups — just route the result.
engine.on_condition(func(args):
var ctx = args["context"]
var groups = ctx.condition_groups
var np = args["block"].get("nativeProperties")
var is_dispatcher = np is Dictionary and np.get("enableDispatcher", false)
var matched = []
for g in groups:
if g.get("result", false):
matched.append(g.get("port_index", 0))
var result = matched if is_dispatcher else (matched[0] if matched.size() > 0 else -1)
ctx.resolve(result)
args["next"].call()
return Callable()
)Pourquoi un seul callback?
Avant onResolveCondition, la même logique gameState.check(...) devait être enregistrée séparément dans setChoiceFilter et onCondition. Avec le resolver unifié, c'est un seul callback — le engine gère les deux automatiquement.
Avancé : Filtrage manuel
Si un resolver global n'est pas souhaité, LsdeUtils fournit un utilitaire low-level :
import { LsdeUtils } from '@lsde/dialog-engine';
const visible = LsdeUtils.filterVisibleChoices(
block.choices ?? [],
(cond) => gameState.check(cond.key, cond.operator, cond.value),
scene, // optional — enables choice: condition resolution via history
);var visible = LsdeUtils.FilterVisibleChoices(
block.Choices ?? new(),
cond => GameState.Check(cond.Key, cond.Operator, cond.Value),
scene // optional — enables choice: condition resolution via history
);auto visible = lsde::LsdeUtils::FilterVisibleChoices(
block->choices,
[](const auto& cond) { return gameState.check(cond.key, cond.op, cond.value); },
scene // optional — enables choice: condition resolution via history
);var visible = LsdeUtils.filter_visible_choices(
block.get("choices", []),
func(cond): return game_state.check(cond),
scene # optional — enables choice: condition resolution via history
)Le paramètre scene active la résolution automatique des conditions choice:. Sans celui-ci, toutes les conditions sont déléguées au evaluator callback.
