novel-src/episodes/*.txt の書き方。npm run build:novel で自動変換される。
執筆者が、以下のフォーマットに沿ったテキストファイルを用意し、決められた1つの操作(コマンド実行など)を行うと、下記の解釈ルールに従って自動的にサイトの小説ページへ反映される仕組みを実装すること。
例えば、執筆者が以下のようなテキストを用意した場合、
私は闇の中で、キャラAの声を聞いた。
▼
>「大丈夫、ここにいるから」
▼
キャラAはそう囁いた。
サイト上では「▼で区切られたブロックごとに、タップで1つずつ表示が進む」「>で始まる行はセリフとして左に線が入った見た目で表示される」という形に、自動的に解釈・反映される。
以下、詳細な解釈ルールを記載する。
Episode <数字>、2行目はタイトル文字列(「」で囲むかどうかも含め、そのまま画面に表示される)。3行目以降がすべて本文となる(ヘッダーと本文の間に空行を入れるかどうかは自由で、構文上の意味は持たない)Episode <数字>をそのまま流用して、システム側が自動的に生成する▼だけの行は、段落の区切り(空行)とは別に、画面上で一旦表示を止める区切り位置を示す。▼で区切られた単位が1ブロックとなる>で始まる行はセリフ(見た目に左線が付く)である。>自体は制御記号であり画面には表示されない。「」はセリフの判定条件ではなく、単なる本文中の記号として自由に使える(セリフの中で>「〜」のように使うのが自然だが、地の文で「」を使っても判定には影響しない)>の直後に半角スペース区切りでキャラクター名を書くと、そのキャラクターの話者として扱われる(例: >タイチ いってー!)。話者名はcharacters.jsonに登録されている名前と完全一致させること。一致しない場合(名前を書かない場合も含む)は、これまで通り話者なしのセリフとして扱われる(スペースを含む本文をそのまま書いても誤判定はされない)>で始まらない行は地の文である《改行》 と書く(例: >タイチ 一行目《改行》二行目)。このフォーマットは1行(生テキストの1行)がそのまま1つのセリフ/地の文になる仕組みのため、生テキストの中でふつうに改行すると別の行として切り離されてしまう。その代わりに使う記法Episode 3
「微睡の人魚、シノ」
私は闇の中で、キャラAの声を聞いた。
▼
>「大丈夫、ここにいるから」
▼
キャラAはそう囁いた。
以下のように書かれている箇所は、挿絵を挟む位置を示す。
《挿絵:scene03_01.png》
ファイル名だけを書けばよい(パスは不要)。画像自体はpublic/img/web-novel/直下に置く。例えば《挿絵:scene03_01.png》と書いた場合、public/img/web-novel/scene03_01.pngを用意する(サイト上では自動的に/img/web-novel/scene03_01.pngとして読み込まれる)。まだ画像が無い場合は「[挿絵]」というプレースホルダー表示になる。
話しかけられた!
▼
===選択===
殴る
好物を渡す
===
【殴る】
私はキャラAを殴った。
▼
>「いってー! なにすんだよ」
▼
キャラAは怒った声をあげた。
▼
《好感度:キャラA-1》
▼
【殴る:終了】
【好物を渡す】
私はキャラAに好物を差し出した。
▼
>「え、くれんの? ラッキー」
▼
キャラAは嬉しそうに受け取った。
▼
《好感度:キャラA+1》
▼
【好物を渡す:終了】
(ここから下は、どちらを選んでも共通)
二人はそのまま歩き出した。
===選択=== 〜 === で囲まれた範囲は選択肢であり、1行につき1つの選択肢を示す【選択肢と同じ文言】 という見出しは、その選択肢が選ばれた場合にのみ表示される内容の開始を示す【選択肢と同じ文言:終了】 という行は、その見出し区間の終わりを示す。これより後、次の【見出し】が無ければ全ルート共通の内容として扱う【見出し】(および対応する【〜:終了】)の数は必ず一致する《好感度:キャラ名+1》 / 《好感度:キャラ名-1》 は、ブロック内の任意の位置に置かれる、好感度変化の指定である(選択の直後である必要はない)===重要選択===と書かれている場合、対象は敵・味方を問わず誰かの生死が決まる場面であり、通常の選択肢とは別のUI(長押し確定)で扱う。
===重要選択===
キャラAを助ける
キャラAを見捨てる
===
【キャラAを助ける】
(本文)
【キャラAを助ける:終了】
【キャラAを見捨てる】
(本文)
《状態:キャラA=死亡》
【キャラAを見捨てる:終了】
構文は===選択===と同一。===重要選択===である場合のみ、通常のタップではなく長押し確定のUIとして扱う。
《状態:キャラ名=値》 は、《好感度:キャラ名+1》と同じくブロック内の任意の位置に置かれる、そのキャラクターの状態を記録するマーカーである。値は執筆者が自由に決めてよい(例:死亡、離脱、復帰)。同一キャラクターに対して複数回このマーカーが置かれた場合、最も後に書かれたものが最新の状態として上書きされる(例:「離脱」の後に「復帰」と書けば、以降は「復帰」状態として扱われる)。この記録は、Episode12のエンディング分岐の条件リストから参照される。
《アイテム:回復薬=取得》
《アイテム:回復薬=喪失》
《アイテム:アイテム名=取得》 / 《アイテム:アイテム名=喪失》 は、ブロック内の任意の位置に置かれる、アイテムの所持状態を記録するマーカーである。使用による消費も「喪失」として扱う。《状態:キャラ名=値》と同様、最も後に書かれたものが最新の所持状態として上書きされる。この記録は、Episode12のエンディング分岐の条件リストから参照される。
画面に表示される文言を、記録される値とは別に指定したい場合は、|の後に表示文言を書く。
《アイテム:カードキー=喪失|カードキーを使用した》
この場合、記録される値は喪失のまま(条件分岐などから通常通り参照される)だが、画面には「カードキーを使用した」という文言が表示される。|が無い場合は、これまで通りの標準表示(例:「アイテム「カードキー」を喪失した」)になる。
アイテムの所持状況などに応じて、読者に選ばせるのではなく、システム側が自動で判定してルートを振り分ける場面を、以下のように書く。
===条件===
《アイテム:カードキー=取得》
===
【はい】
私はカードキーを使って扉を開けた。
▼
《アイテム:カードキー=喪失|カードキーを使用した》
▼
>「よく来たね」
▼
===選択===
話を聞く
すぐに立ち去る
===
【話を聞く】
(本文)
▼
《好感度:キャラA+1》
【話を聞く:終了】
【すぐに立ち去る】
(本文)
▼
《好感度:キャラA-1》
【すぐに立ち去る:終了】
【はい:終了】
【いいえ】
扉には鍵がかかっていて、開かなかった。
【いいえ:終了】
(ここから下は、どちらのルートでも共通)
私は先へ進んだ。
===条件=== 〜 === で囲まれた範囲には、判定する条件(《アイテム:〜》・《状態:〜》・《好感度:〜》のいずれか)を1つ書く【はい】 / 【いいえ】 は、条件に合致した場合/しなかった場合にのみ表示される内容を示す。構文・終了マーカーの扱いは===選択===の【見出し】と同一【はい】 / 【いいえ】 の内容は、===選択===や好感度・状態・アイテムのマーカーを含む、通常のブロックと同じ内容を書ける(条件分岐の中でさらに選択肢を挟むことができる)【いいえ】の内容は省略してもよい(何も起こらず、そのまま共通部分に進む)エンディングは固定の計算式ではなく、それまでの物語で本文中の《状態:キャラ名=値》・《好感度:キャラ名±1》・《アイテム:アイテム名=取得/喪失》によって積み重なったフラグを、優先順位付きの条件リストとして上から順に照合し、最初に合致したものに決定する。
1. キャラAの状態が「死亡」 → 特定キャラA喪失エンド
2. キャラBの状態が「離脱」 → 特定キャラB不在エンド
3. (条件は随時追加される)
最後. どれにも当てはまらなければ → デフォルトエンド
episode12-endings.txt)として管理し、執筆者が直接編集する原稿テキストファイルからサイトへの反映は、AIの都度の介在を必要としない、決まったコマンド1つで完結する仕組みとすること(変換処理+公開までを自動化する)。
npm run build:novel がこの変換を行う。公開(firebase deploy)は別コマンドとして手動実行する運用になっている。