Choice の表示制御
概要
CHOICE block がディスパッチされると、context.choices には blueprint で定義されたすべての choice が常に含まれます — 事前にフィルタリングされるものはありません。engine は配列から choice を削除することはありません。
表示制御フィルタリングが必要な場合(例:ゲームステートや以前の選択に基づいて choice を非表示にする)、engine はオプトイン方式のタグ付けシステムを提供します。condition リゾルバーを一度インストールすると、onChoice handler が呼ばれる前に、engine が各 choice に visible: true | false をタグ付けします。
セットアップ
engine に condition リゾルバーを登録します — scene を開始する前に一度だけ:
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)
)インストールされると、engine は onChoice を呼び出す前に各 choice の visibilityConditions を評価します。同じリゾルバーは condition block のグループも事前評価します — 詳細は Condition blocks を参照してください。
choice:condition(以前のプレイヤー選択を参照)は、engine の内部 choice 履歴によって自動的に解決されます — 登録された callback には渡されません。- ゲームステート condition(その他すべて)は、登録された callback に委任されます。
&(AND)と|(OR)によるチェーンは、両方のタイプにまたがって正しく動作します。
onChoice でのフィルタリング
handler 内で、1行でフィルタリングできます:
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()
)なぜ visible !== false であって === true ではないのか?
リゾルバーがインストールされていない場合、visible は undefined です。undefined !== false は true に評価されるため、すべての choice が通過します — デフォルトで後方互換性があります。リゾルバーがインストールされている場合、choice は明示的に true または false でタグ付けされます。
visible の値 | 意味 | !== false |
|---|---|---|
true | リゾルバーインストール済み、choice は通過 | true |
false | リゾルバーインストール済み、choice は非表示 | false |
undefined | リゾルバー未インストール | true |
RuntimeChoiceItem
リゾルバーがインストールされている場合、context.choices 内の各 choice は RuntimeChoiceItem です — visible タグが追加された ChoiceItem の拡張です:
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 }リゾルバーなしの場合、choice は RuntimeChoiceItem のままですが、visible は undefined/null/nullopt/absent のままです。
使用例
標準 — 表示可能な choice を表示
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()
)タイムアウト付き choice — タイムアウト時に自動選択
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()
)非表示の choice をグレーアウト表示
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.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()
)エバリュエーターの共有
onResolveCondition を使えば、1つの callback で choice の可視性と condition block の事前評価の両方を処理できます。ロジックを重複させる必要はありません:
// 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()
)なぜ1つの callback なのか?
onResolveCondition 以前は、同じ gameState.check(...) ロジックを setChoiceFilter と onCondition に別々に登録する必要がありました。統合リゾルバーでは1つの callback で済みます — engine が両方を自動的に処理します。
上級: 手動フィルタリング
グローバルリゾルバーをインストールしたくない場合、LsdeUtils がローレベルのユーティリティを提供します:
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
)scene パラメーターを指定すると、choice: condition の自動解決が有効になります。指定しない場合、すべての condition は登録されたリゾルバー callback に委任されます。
