ステップとは、物語が止まってプレイヤーを待つ地点です。ステップには 2 種類あり、s!(…) と書く通常のステップと、choice!{…} と書く選択肢です。そのテキストは t![…] で書きます。この 3 つはフォームです。フォームは .kix ファイルでだけ使える固定の構文で、普通の構造体式と呼び出しに脱糖されます。フォームの正確な構文(区切り、属性、要素)はフォームにあります。
ステップを実行できるのはシーンだけです(E3002)。
ステップは push_step の呼び出し
物語を中断するプリミティブは std::narrative にある 1 つだけです。
#[builtin] pub trait Step { type Output; }
#[builtin]pub pushdown fn push_step<S: Step>(s: S) -> S::Output;push_step はステップを表示し、入力を待ち、入力が運んできたものを返します。物語を中断するので純粋ではありません。Step を実装する標準の型は 2 つです。
| 型 | 書き方 | Output |
|---|---|---|
Say | s!(…) | ():NextStep は情報を運ばない |
Choice<A, N> | choice!{…} | A:プレイヤーが選んだアーム |
つまり
s!(who = "Azusa", voice = AZUSA_0072, render = hanako_jumps, t!["ぬっ"]);は次の意味です。
push_step(Say { who: Some("Azusa"), voice: Some(AZUSA_0072), render: Some(hanako_jumps), text: Some(t!["ぬっ"]), history: None,});構造体の形は普通のコードなので手で書くこともでき、.kis ファイルではこの形でステップを書きます。完全な宣言は STD リファレンスの std::narrative にあります。
s!:通常のステップ
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 のフィールドです。
| 属性 | 型 | 意味 |
|---|---|---|
who | Text | 表示用テキストとしての話者名。who がなければ地の文 |
voice | VoiceAsset | メッセージと一緒に再生するボイス |
render | Render | ステップに入ったときの画面の変化。プレゼンテーションを参照 |
history | Text | メッセージのテキストの代わりにバックログが記録するテキスト |
属性はすべて省略でき、省略した属性は None になります。要素はメッセージのテキストである t![…] を高々 1 つです。テキストのない s! はメッセージを表示しませんが、入力は待ちます。たとえば CG を表示するレンダーの後に使います。render のないステップは画面を変えません。
1 つのステップの話者は 1 人です。会話はステップの列です。
既定では、バックログは表示時点の埋め込み値でその行を記録します。history = t![…] は記録されるテキストを置き換えます。
s!(who = "Azusa", history = t!["(Azusa names the date)"], t!["Today is day " {DAY} "."]);NextStep は情報を運ばないので、セーブのトレースには記録されません。記録されるのは選択肢だけです。
テキストとテキスト効果
t![…] は、並べて書いた項目から Text 型の値を作ります。項目は、素の文字列、ルビ(["数多"]("あまた"))、テキスト効果付きのテキスト()))、埋め込み値({expr})です。
t "の" )) "花。"]文字列リテラルはそれ自体が Text なので、who = "Mio" に t! は要りません。テキスト効果はコンパイル時に評価される定数なので、メッセージはステップの定数のままです。効果には、スタイル(色、太字、強調など)、表示速度、そしてステップ内のキューがあります。キューは、時間指定の一時停止、クリック待ち(同じステップ内でテキストを続けるクリックで、セーブのトレースには記録されません)、レンダーがアニメーションの時刻を合わせられるマークです。物語の中では、有限な実行時の値の {expr} は他の const fn 呼び出しと同様に表化されます。
項目の構文と規則はフォームに、すべての効果は STD リファレンスの std::narrative::text にあります。
choice!:選択肢ステップ
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 | 文字列リテラル | はい | 関数内で一意な安定した識別子 |
render | Render | いいえ | s! と同じく、選択肢に入ったときに実行される |
各要素はアーム t![…] => Variant で、ガードを付けて t![…] if condition => Variant とも書けます。
- テキストはプレイヤーに表示される選択肢です。
- 1 つの選択肢のアームはすべて、書き手が宣言した同じ列挙型のヴァリアントを使い、各ヴァリアントは高々 1 回です。ヴァリアントはフィールドを持ってはいけません(
E3020)。選択肢はその列挙型を持つので、それに対するmatchは他のmatchと同じく網羅性が検査されます。アームのないヴァリアントも処理するか、_で覆う必要があります。
choice!{…} は push_step(Choice { … }) です。各アームは ChoiceArm { text, shown, value } になり、shown はそのガード、ガードがなければ true です。
ガード
t!["Leave"] if VISITED_A && VISITED_B && VISITED_C => Map::Leave,ガード付きのアームは、選択肢に入った時点で条件が真でなければ表示されません。表示されるアームが 1 つもない状態で到達しうる choice! は、プレイヤーが進めなくなるのでコンパイラが報告します(E5003)。
無効なアーム
まだロックされている行き先のように灰色で表示される選択肢は、choice! のアームではありません。値を持たず、選べません。これはプレゼンテーションであり、選択肢のレンダーが画面の ChoiceMenu に追加します。
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)が報告されます。セーブ、リプレイ、決定性を参照してください。