シナリオ MOD は原作のステージにイベントを追加します。何ターン目、誰かが撃墜されたとき、誰と誰が戦ったとき、「説得」を選んだとき……に、書いたセリフを流したり、人物を加入・退場させたり、シナリオ変数を変えて後の原作シナリオを別の分岐に進ませたりできます。いつ起こすかはゲームが判断するので、書くのは条件と内容だけで、原作のイベント表を知る必要はありません。原作のイベントが書き換えられたり押し出されたりすることはありません。
パッケージのインストールと管理は MOD パッケージ:導入と作り方 を見てください。バージョン 0.5.3 以降が必要です。
完全な例は「東方不敗 生存」で、ソースは config/recomp/mods/master-asia にあります。独立軍ルート「死闘!ランタオ島大決戦(後)」と OZ ルート「絶望と悲しみの拳(後)」で、戦闘中に東方不敗と言葉を交わしたドモンは東方不敗を説得でき、撃墜数 50 以上なら東方不敗は退いてステージクリア後に仲間になります。
パッケージの中身
yourname.story/
mod.json
story/
<任意の名前>.json シナリオスクリプト、いくつでも
data/
pilots.json 必要なら数値もあわせて変更(数値 MOD を参照)
パッケージ化(tools/mods/pack.py)のとき、story/ のシナリオスクリプトはゲームが読む形式にコンパイルされます。書き間違いはどのイベントのどの項目かが報告されます。
書き方
{
"schema": "srw64.story.v1",
"events": [
{
"id": "persuade",
"scene": 129,
"when": { "persuade": { "by": 4, "target": 18 } },
"if": { "vars": { "131": 0 }, "kills": { "4": 50 } },
"do": [
{ "say": 4, "text": { "ja": "「師匠!」", "zh-Hans": "“师父!”", "en": "“Master!”" } },
{ "say": 18, "mode": 2, "text": { "ja": "「……」", "zh-Hans": "“……”", "en": "“...”" } },
{ "leave": 18 },
{ "set": { "131": 3 } }
]
},
{
"id": "joins",
"scene": 129,
"when": { "after": "end" },
"if": { "fired": ["persuade"] },
"do": [ { "join": { "pilot": 18, "unit": 27 } } ]
}
]
}
イベントごとの項目:
| 項目 | 説明 |
|---|---|
id |
イベント名。パッケージ内で一意で、ほかのイベントから参照するのに使います |
scene |
どのステージか、シーン番号(0〜255)で書きます。各ステージのシーン番号はシーン番号対照表にあります |
when |
いつ起こるか(下を参照) |
if |
任意。ほかに満たす条件 |
do |
何をするか、順に実行 |
repeat |
任意。true で何度でも起こる |
set_flags |
任意。イベントが終わったときに立てるフラグ。後のステージで使います |
note |
任意。自由なメモで、ゲームは読みません |
各イベントは標準ではステージごとに 1 回だけ起こります。 ゲームが記録し、中断セーブにも残るので、原作のように変数を書いて再生の重複を防ぐ必要はありません。
人物番号と機体番号は数値 MOD の人物表と機体表にあります。
when:いつ
| 書き方 | 意味 |
|---|---|
{"turn": {"turn": 3, "side": "player"}} |
3 ターン目以降、その陣営のフェイズ開始時(player、enemy、third) |
{"defeated": 18} |
その人物のユニットが撃墜された(退場は含まない) |
{"hp_below": {"who": 18, "percent": 30}} |
その人物の HP が 30% を下回った |
{"battle": {"between": [4, 18], "moment": "after"}} |
2 人が戦った後(before は戦闘前。人物番号 0 は誰でも) |
{"enemies_gone": true} |
敵が全滅した。{"by_turn": 5} で 5 ターン目までに限る |
{"remaining": {"side": "enemy", "at_most": 3}} |
敵(third なら第三勢力)の残りが 3 機以下 |
{"persuade": {"by": 4, "target": 18}} |
4 番が 18 番の隣にいるとコマンドに「説得」が出て、選ぶと起こる |
{"after": "opening"}、"setup"、"end" |
そのステージの原作のオープニング、初期配置、終了イベントの直後 |
同じタイミングで原作のイベントも起こるときは原作のものが先で、こちらのイベントは次の確認のときに見られます。オープニング、初期配置、終了の後のイベントは repeat にできません。
if:ほかの条件
| 書き方 | 意味 |
|---|---|
"vars": {"131": 0} |
シナリオ変数(0〜199)がこの値(0〜3)。原作は分岐をこれで記録しています。たとえばシーン 129 の 131 番は「ドモンが戦闘中に東方不敗と話した」(話していれば 0) |
"kills": {"4": 50} |
味方のこの人物の撃墜数がこれ以上 |
"kills_below": {"4": 50} |
撃墜数がこれ未満 |
"fired": ["persuade"] |
このパッケージのこれらのイベントがこのステージですでに起きた |
"not_fired": [...] |
このステージでまだ起きていない |
"roster": {"pilots": [18], "units": [27]} |
これらの人物と機体が味方の名簿にいる(出撃の有無は問わない) |
"not_roster": {...} |
どれも名簿にいない |
"flags": ["persuaded"] |
このパッケージのこれらのフラグが立っている |
"not_flags": [...] |
どれも立っていない |
その人物が味方の名簿にいないときは、kills も kills_below も成り立ちません。
成功と失敗の 2 通りのセリフ:同じタイミングに条件が重ならない 2 つのイベントを書くと、ゲームは成り立つほうを選びます。例では撃墜数が足りれば成功、足りなければ断られるセリフが流れます:
{ "id": "persuade", "when": { "persuade": { "by": 4, "target": 18 } },
"if": { "vars": { "131": 0 }, "kills": { "4": 50 }, "not_fired": ["refused"] }, "do": [] },
{ "id": "refused", "when": { "persuade": { "by": 4, "target": 18 } },
"if": { "vars": { "131": 0 }, "kills_below": { "4": 50 } }, "do": [] }
断られるイベントは 1 回だけ起こり、成功のほうは not_fired: refused を条件にしているので、断られた後はそのステージでどちらも成り立たず、「説得」はコマンドから消えて、シナリオは原作どおりに進みます。何度でも試せるようにするなら、断られるほうに "repeat": true を書き、成功のほうの not_fired を外します。
ステージをまたぐ:フラグ
fired はそのステージの中だけで、次のステージでは消えます。後のステージに出来事を伝えるには、イベントに "set_flags": ["フラグ名"](英小文字、数字、_、-、32 文字まで)を書くと、イベントが終わったときに立ちます。フラグはそのパッケージだけのもので、原作のシナリオ変数とは関係なく、中断セーブとインターミッションのセーブについていき、新しいゲームを始めたときだけ消えます。
{ "id": "persuade", "scene": 129, "set_flags": ["persuaded"], "when": { "persuade": { "by": 4, "target": 18 } }, "do": [] },
{ "id": "comes-along", "scene": 81, "when": { "after": "opening" },
"if": { "roster": { "pilots": [18] }, "flags": ["persuaded"] },
"do": [ { "say": 18, "text": { "ja": "「ドモン、遅れを取るでないぞ!」" } } ] }
名簿も見ているのは、その後に原作のシナリオやほかのイベントで名簿から外されることがあるからです。フラグだけでは、いなくなっても話してしまいます。
do:何をするか
| 動作 | 意味 |
|---|---|
{"say": 18, "text": {...}, "mode": 0} |
セリフ 1 つ(下を参照) |
{"join": {"pilot": 18, "unit": 27}} |
人物と機体が味方に加入 |
{"remove": {"pilot": 18, "unit": 27}} |
名簿から外す(ユニットは先に退場させる) |
{"leave": 18} |
その人物のユニットが退場(撃墜扱いではない) |
{"leave_group": 11}、{"deploy": 11} |
そのステージの原作の出撃グループが退場、登場 |
{"set": {"131": 3}} |
シナリオ変数を設定 |
{"if": {"var": 128, "is": 2}, "then": [...], "else": [...]} |
シナリオ変数で分岐 |
{"focus": 18} |
カメラをその人物へ |
{"bgm": 11}、{"sound": 124} |
音楽を変える、効果音を鳴らす |
{"fade": "out"}、{"fade": "in"}、{"wait": 30} |
フェードアウト、フェードイン、待つ(フレーム) |
{"close": "window"} |
会話ウィンドウを閉じる |
{"op": "3D35", "args": [2335, 1]} |
原作のスクリプト命令をそのまま。ステージスクリプト命令リファレンスを参照。原作のセリフも直接使えます(例:{"op": "3D40", "args": [43215]}) |
セリフ:text には ja が必須で、書かなかった言語は日本語で表示されます(繁体字がなければ簡体字)。1 つ 200 文字までで、改ページはリーダーが自動で行うので <STOP> は書きません。かぎかっこは原作のセリフと同じく自分で書きます。日本語と繁体字は「」、簡体字と英語は “”。mode は原作の会話の表示方法で、マップでは味方 0、敵 2 が普通、ワールドマップでは 0 と 1 で上下を交互にします。
加入時のレベルは原作の決まりどおり、そのステージの基準レベル(味方上位 15 人の平均から計算)です。"level": {"offset": 3} で基準から増減し、"level": {"like": 4} で 4 番の人物と同じレベル(加入した時点)にします。SP、能力、技能、精神コマンドは原作がレベルから計算します。基本値を変えるなら data/pilots.json を使います(敵の人物は SP や精神コマンドが空のことが多く、例では SP を補っています)。
注意
- 原作のイベントは原作の変数しか見ません。 原作のあるイベントを流さない、または別の部分を流すには、それが判定に使う変数を変えます。例では説得の後に
131 = 3とし、原作の終了イベントは 131 が 1 か 2 のときだけ東方不敗との死別を流します。 - ユニットの退場(
leave)では原作の撃墜イベントは起こりません。 撃墜後に原作がすること(例ではウォンの登場と勝利条件の開放)は、自分のイベントでやり直す必要があります。 - 同じシナリオを別のステージに移す前に、そのステージの原作イベントを読んでください。会話でどの変数を使うか、撃墜の後に原作が何をするか、終了イベントがどの変数でどの部分を流すか。例の OZ ルートのイベントはシーン 137 の原作イベントに合わせて書き直したものです。
- フラグはセーブの横に保存されます(セーブフォルダー
saves/の中のstory-state/)。セーブファイル自体には入っていません。セーブをほかのエミュレーターに持っていったりこのフォルダーを消したりするとフラグはなくなり、ステージをまたぐイベントは起こりません。1 つのステージの中のイベントには影響しません。 - 追加シナリオ(
campaign/)のステージではシナリオ MOD は動きません。
確認とパッケージ化
python3 tools/mods/story.py compile story/master.json -o out.json
python3 tools/mods/pack.py yourname.story -o build/mods
1 つ目はシナリオスクリプト 1 つだけをコンパイルし、各イベントがどんな命令になるかを確かめられます。2 つ目はパッケージを作り、ゲームと同じ決まりで確認します。ゲームが読めないパッケージはログに SRW64_STORY_MOD_PROBLEM と出ます。
リポジトリの docs/guide/story-mods/ フォルダーに完全な形式の説明とシーン番号対照表があり、まるごと AI アシスタントに渡して、考えたシナリオをスクリプトに書いてもらえます。