Skip to content

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 :

ts
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);
});
csharp
engine.OnResolveCondition(cond => {
    return GameState.Check(cond.Key, cond.Operator, cond.Value);
});
cpp
engine.onResolveCondition([](const ExportCondition& cond) {
    return gameState.check(cond.key, cond.op, cond.value);
});
gdscript
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 :

ts
engine.onChoice(({ block, context, next }) => {
  const visible = context.choices.filter(c => c.visible !== false);
  showChoicesUI(visible, (uuid) => {
    context.selectChoice(uuid);
    next();
  });
});
csharp
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;
});
cpp
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 {};
});
gdscript
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 visibleSignification!== false
trueResolver installé, le choix passetrue
falseResolver installé, choix cachéfalse
undefinedPas 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 :

ts
interface RuntimeChoiceItem extends ChoiceItem {
  visible?: boolean; // true | false | undefined
}
csharp
public class RuntimeChoiceItem : ChoiceItem
{
    public bool? Visible { get; set; } // true | false | null
}
cpp
struct RuntimeChoiceItem : ChoiceItem {
    std::optional<bool> visible; // true | false | nullopt
};
gdscript
# 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

ts
engine.onChoice(({ context, next }) => {
  const visible = context.choices.filter(c => c.visible !== false);
  ui.showChoices(visible, (uuid) => {
    context.selectChoice(uuid);
    next();
  });
});
csharp
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;
});
cpp
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 {};
});
gdscript
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

ts
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)));
  }
});
csharp
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;
});
cpp
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 {};
});
gdscript
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é

ts
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...
});
csharp
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;
});
cpp
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 {};
});
gdscript
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é

ts
tutorial.onChoice(({ context, next }) => {
  // force-select the first choice, no filtering
  context.selectChoice(context.choices[0].uuid);
  next();
});
csharp
tutorial.OnChoice(args => {
    // force-select the first choice, no filtering
    args.Context.SelectChoice(args.Context.Choices[0].Uuid);
    args.Next();
    return null;
});
cpp
tutorial->onChoice([](auto*, auto*, auto* ctx, auto next) -> CleanupFn {
    // force-select the first choice, no filtering
    ctx->selectChoice(ctx->choices()[0].uuid);
    next();
    return {};
});
gdscript
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 :

ts
// 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();
});
csharp
// 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;
});
cpp
// 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 {};
});
gdscript
# 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 :

ts
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
);
csharp
var visible = LsdeUtils.FilterVisibleChoices(
    block.Choices ?? new(),
    cond => GameState.Check(cond.Key, cond.Operator, cond.Value),
    scene // optional — enables choice: condition resolution via history
);
cpp
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
);
gdscript
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.