Kagari
キーワードを入力してドキュメントを検索します。

中核概念

ステップ

ステップとは、物語が止まってプレイヤーを待つ地点です。ステップには 2 種類あり、s!(…) と書く通常のステップと、choice!{…} と書く選択肢です。そのテキストは t![…] で書きます。この 3 つはフォームです。フォームは .kix ファイルでだけ使える固定の構文で、普通の構造体式と呼び出しに脱糖されます。フォームの正確な構文(区切り、属性、要素)はフォームにあります。

ステップを実行できるのはシーンだけです(E3002)。

ステップは push_step の呼び出し

物語を中断するプリミティブは std::narrative にある 1 つだけです。

kis
#[builtin] pub trait Step { type Output; }
#[builtin]
pub pushdown fn push_step<S: Step>(s: S) -> S::Output;

push_step はステップを表示し、入力を待ち、入力が運んできたものを返します。物語を中断するので純粋ではありません。Step を実装する標準の型は 2 つです。

型書き方Output
Says!(…)():NextStep は情報を運ばない
Choice<A, N>choice!{…}A:プレイヤーが選んだアーム

つまり

kix
s!(who = "Azusa", voice = AZUSA_0072, render = hanako_jumps, t!["ぬっ"]);

は次の意味です。

kis
push_step(Say {
who: Some("Azusa"),
voice: Some(AZUSA_0072),
render: Some(hanako_jumps),
text: Some(t!["ぬっ"]),
history: None,
});

構造体の形は普通のコードなので手で書くこともでき、.kis ファイルではこの形でステップを書きます。完全な宣言は STD リファレンスの std::narrative にあります。

s!:通常のステップ

kix
s!(t!["It was the last day of summer."]);
s!(who = "Azusa", render = azusa_smiles, t!["Did you forget again?"]);
s!(render = cg_fireworks);

s! はメッセージを 1 つ表示し、NextStep を待ちます。属性は Say のフィールドです。

属性型意味
whoText表示用テキストとしての話者名。who がなければ地の文
voiceVoiceAssetメッセージと一緒に再生するボイス
renderRenderステップに入ったときの画面の変化。プレゼンテーションを参照
historyTextメッセージのテキストの代わりにバックログが記録するテキスト

属性はすべて省略でき、省略した属性は None になります。要素はメッセージのテキストである t![…] を高々 1 つです。テキストのない s! はメッセージを表示しませんが、入力は待ちます。たとえば CG を表示するレンダーの後に使います。render のないステップは画面を変えません。

1 つのステップの話者は 1 人です。会話はステップの列です。

既定では、バックログは表示時点の埋め込み値でその行を記録します。history = t![…] は記録されるテキストを置き換えます。

kix
s!(who = "Azusa", history = t!["(Azusa names the date)"], t!["Today is day " {DAY} "."]);

NextStep は情報を運ばないので、セーブのトレースには記録されません。記録されるのは選択肢だけです。

テキストとテキスト効果

t![…] は、並べて書いた項目から Text 型の値を作ります。項目は、素の文字列、ルビ(["数多"]("あまた"))、テキスト効果付きのテキスト(!["赤い"](color_text(rgb(200, 0, 0))))、埋め込み値({expr})です。

kix
t!["これは" ["数多"]("あまた") "の" !["赤い"](color_text(rgb(200, 0, 0))) "花。"]

文字列リテラルはそれ自体が Text なので、who = "Mio" に t! は要りません。テキスト効果はコンパイル時に評価される定数なので、メッセージはステップの定数のままです。効果には、スタイル(色、太字、強調など)、表示速度、そしてステップ内のキューがあります。キューは、時間指定の一時停止、クリック待ち(同じステップ内でテキストを続けるクリックで、セーブのトレースには記録されません)、レンダーがアニメーションの時刻を合わせられるマークです。物語の中では、有限な実行時の値の {expr} は他の const fn 呼び出しと同様に表化されます。

項目の構文と規則はフォームに、すべての効果は STD リファレンスの std::narrative::text にあります。

choice!:選択肢ステップ

kix
enum Confess { Yes, No }
let answer = choice!{
id = "confess",
t!["Tell her the truth"] => Confess::Yes,
t!["Say nothing"] => Confess::No,
};
match answer {
Confess::Yes => AzusaConfession(),
Confess::No => s!(t!["I kept quiet."]),
}

choice! は 1 つのステップです。エンジンは提示するアームごとに選択肢を 1 つ表示し、プレイヤーが選ぶのを待ちます。選択肢はそのアームのヴァリアントに評価され、普通のコードがそれで分岐します。アームはコードのブロックではなくデータです。選択後に起きることは選択肢の後に書きます。

属性型必須意味
id文字列リテラルはい関数内で一意な安定した識別子
renderRenderいいえs! と同じく、選択肢に入ったときに実行される

各要素はアーム t![…] => Variant で、ガードを付けて t![…] if condition => Variant とも書けます。

  • テキストはプレイヤーに表示される選択肢です。
  • 1 つの選択肢のアームはすべて、書き手が宣言した同じ列挙型のヴァリアントを使い、各ヴァリアントは高々 1 回です。ヴァリアントはフィールドを持ってはいけません(E3020)。選択肢はその列挙型を持つので、それに対する match は他の match と同じく網羅性が検査されます。アームのないヴァリアントも処理するか、_ で覆う必要があります。

choice!{…} は push_step(Choice { … }) です。各アームは ChoiceArm { text, shown, value } になり、shown はそのガード、ガードがなければ true です。

ガード

kix
t!["Leave"] if VISITED_A && VISITED_B && VISITED_C => Map::Leave,

ガード付きのアームは、選択肢に入った時点で条件が真でなければ表示されません。表示されるアームが 1 つもない状態で到達しうる choice! は、プレイヤーが進めなくなるのでコンパイラが報告します(E5003)。

無効なアーム

まだロックされている行き先のように灰色で表示される選択肢は、choice! のアームではありません。値を持たず、選べません。これはプレゼンテーションであり、選択肢のレンダーが画面の ChoiceMenu に追加します。

kix
match choice!{
id = "map",
render = if VISITED_A { show_map } else { show_map_locked },
t!["Stall A"] => Map::A,
t!["Stall B"] if VISITED_A => Map::B,
} { … }
const fn show_map_locked(mut screen: Screen) -> Screen {
screen.choice_menu().disabled_arm(1, t!["Stall B"]);
screen
}

disabled_arm(at, text) は、表示されるリストの位置 at(リストの長さに切り詰められます)に表示専用の選択肢を挿入します。ChoiceMenu はそのステップだけのもので、次のステップに入ると空になります。無効なアームは choice! の一部ではないので、どの解析にも見えません。プレイヤーが何を選べるかはガードだけが決めます。STD リファレンスの std::screen を参照してください。

安定した選択肢 ID

セーブは各選択を、関数のパス、選択肢の id、ヴァリアント名として記録します。たとえば story::Prologue/confess/Confess::Yes です。呼び出しの文脈はリプレイが補うので、関数が多くの場所から呼ばれたりループの中で実行されたりしても、選択肢の id は関数内で一意であれば十分です。id は文字列リテラルでなければならず、1 つの関数で同じ id を 2 回使うと E3019 です。

ID は行番号やアームの順序に依存しないので、スクリプトの並べ替えや書き直しをしてもセーブは壊れません。関数、選択肢の id、ヴァリアントの名前を変えると、そこを通るセーブは壊れます。そのようなセーブを読み込むと、その選択肢で分岐(divergence)が報告されます。セーブ、リプレイ、決定性を参照してください。

最終更新日 2026年10月29日