気候システム設計
概要
本ドキュメントは、ゲーム内の気候(季節・天気)の設計をまとめたものです。GameElementDefinition.md が掲げる
tick 駆動・値域による状態決定・「ハードコードしない」「汎用エンジンに任せる」という設計方針(2 節)に準拠します。
本書の設計は core.yaml の world プロパティとして実装済みであり、複数の乱数シードによる統計テスト
(tests/world-codex/climateSystem.test.ts。「calm は概ね 2 日に 1 回軽い雨が降る」「wet はほとんど雨だが
稀に止み、後半ほど嵐・大雨が増える」「dry はほとんど晴れで雨はごく稀」等の要件を、95% 以上のシードで
満たすことを検証)で確認されています。土地ごとの気温差(1.1 節)だけは在処が違います——定義は
locations.yaml の各土地、検証は tests/world-codex/landTemperature.test.ts です。本書に載せる具体的な数値は実装と同期した現在の値ですが、バランス調整で
変わりうる初期値であり、確定仕様ではありません。残る未決事項は 7 節に整理しています。
1. 基本方針
気候は次の 2 層で構成します。いずれも world(GameElementDefinition.md 15 節の singleton: true オブジェクト。
core.yaml で既に時間・天気・環境光・気温を直接プロパティとして
持つ)自身のプロパティとして表現し、専用の WorldObject インスタンスは作りません。
- 季節(長期サイクル): 穏やか・雨季・乾季を固定順で巡回する。滞在期間は初回サイクルのみ固定、 2 周目以降はランダムに決まる(2.3 節)。
- 天気(短期変動): 灼熱(
scorching)・快晴(sunny)・晴れ(clear)・曇り(cloudy)・ 小雨(light_rain)・大雨(heavy_rain)・嵐(storm)を段階として並べる。季節が育てる貯水池 (大気水分量・蓄熱量)に重み付けされた抽選で決まる、季節から独立したプロパティ(4 節)。
季節・天気とも、「現在の値」と「残り時間」のプロパティの組で表現し、残り時間が 0 になった瞬間に次の値へ
遷移します。個別の現象を条件分岐やイベントとして作り込むのではなく、独立したパラメータ同士が tick ごとに
加算・減算し合う「数値積分」によって駆動します。derived(導出値。GameElementDefinition.md 16 節・17 節、
採否未確定)のような計算式は使いません。
ambient_temperature だけは world の専有ではありません。 あらゆる場所(core.yaml の location
trait。土地・海区・筏・本土)が同名のプロパティを持ち、祖先の値を土台(base)に自分の差を足します
——明るさと同じ形(IlluminationSystem.md 2 節・6 節)です。world が持つのは
空の側の気温(季節と日射だけで決まる値)で、そこへ土地ごとの差と、据えた炉の暖
(FireSystem.md 9.2 節)が積まれます。土地ごとの差は次節が持ち、以降の節は
world 側だけを扱います。
この値を読む側に、体から熱が奪われる速さが加わります(VitalsSystem.md 8.3 節)。
夜の寒さは既に日射の段が気温へ載せており(ambient_brightness の dark 段。
IlluminationSystem.md 9 節)、雨は気温ではなく
濡れることの側から効きます——本書は寒さを 1 つも新しく作らず、気温と天気を渡すだけです。
1.1 土地の差は、海抜が決める
土地が空の気温へ足す差は、海抜ぶんの 1 つだけです。 気温減率(1,000 m 登るごとに 6.5℃ 下がる、
国際標準大気)を、その型の平均海抜へ掛けて 1℃ へ丸めます。海抜は生成した島の実測
(stats/terrain.yaml の site_elevation_by_location。島の最高点が
海抜 400 m である根拠は TerrainGeneration.md 3.5 節)です。
| 土地 | 平均海抜 | value |
|---|---|---|
| 砂浜 | 7.5 m | — |
| 岩礁海岸 | 15 m | — |
| 断崖海岸 | 40 m | — |
| 草原 | 152 m | −1 |
| 森 | 179 m | −1 |
| 密林 | 183 m | −1 |
| 荒野 | 212 m | −1 |
| 岩石地 | 211 m | −1 |
| 山腹 | 322 m | −2 |
| 山頂 | 400 m | −3 |
丸めて 0 になる土地は宣言しません——core.yaml の location trait が既定の 0 を持つので、
「書き忘れ」と「差が無い」が同じ意味になります(明るさと違うのはここで、あちらは場所ごとに必ず
与えます)。浅い洞窟・海区・筏も差を持ちません: 海の上は海抜 0 m で、岩陰は夜の放射冷却を遮る
一方で昼は日陰になるため、定数 1 つでは向きが決まりません。
この差が、寒さを防ぐ物の段を見分けさせます。 空が素の寒さの入口(16℃。
VitalsSystem.md 8.4 節)を下回るのは涼しい季節の夜と、その薄明・雨天の昼の
2 つだけで、衣類の段(SurvivalItems.md 5.1 節)はそれより細かく
刻まれています。寝床の段(Bedding.md 4.2 節)はそれ自体では細かくあり
ませんが、衣類へ足して重なるので、合わせた刻みを細かくする側に立ちます。海抜ぶんの差が重なると、
その 2 つが 1℃ ずつに割れます。
どの一着にも、そこでちょうど釣り合う土地があります——涼しい季節の薄明に土地の差を足した気温が、素の入口から下がっていく
衣類の段とそのまま並びます(tests/world-codex/landTemperature.test.ts)。
素のままで越せるかは、山頂だけが変わります。 戻る速さは削られる速さより十分に速い(+8/tick と
-2/tick、VitalsSystem.md 8.4 節)なので、晴れた日中に入口を上回りさえすれば、
夜に削られたぶんはその日のうちに戻ります(landTemperature.test.ts の「素のままでも、晴れた1日を通せば…」)。晴れた日中に土地の差を足しても入口を下回るのは山頂だけで(landTemperature.test.ts)、そこも
いちばん安い一着があれば戻ります(山腹はちょうど入口と釣り合うので、素のままで足ります)。
登るほど深い一着が要るという形で、衣類の段が島の高さに対応します。
2. 季節: worldプロパティとしての巡回
2.1 固定順の巡回
季節は「穏やか(calm)→雨季(wet)→乾季(dry)→穏やか→…」という固定順のサイクルです。天気(4 節)とは
異なり、次に来る季節がどれかは確率で決まるものではなく、常に同じ順で巡回します。それぞれの季節が基本 30 日
(tick 換算は DurabilitySystem.md が前提とする 1 tick(15 分)に従うと 2,880 tick)続きます。
2.2 プロパティ構成
season: 現在の季節を表す symbol プロパティ(calm/wet/dry。GameElementDefinition.md 6.6 節)。stagesは季節ごとに 1 つずつ、eqは値名から自動導出されます(同 6.4 節)。各 stage は (1) 貯水池への季節レート (3 節)と、(2) 自分が現在の季節であることを示すフラグ(次項)のmodifyを持ちます。season_is_calm/season_is_wet/season_is_dry: 「今の季節はどれか」を 0/1 で表すフラグ。seasonの 各 stage がmodifyで立てる。2.3 節の遷移pickの重みとして参照される。season_remaining: 残り tick 数。毎 tick-1される蓄積型プロパティ。season_cycle: 季節を何周したかを表すカウンタ。0から始まり、1 周(calm→wet→dry→calm)ごとに+1される。stages(first: 0 /later: 1 以上)がfirst_cycle_flag/later_cycle_flagをmodifyで 立て、2.3 節の固定/ランダム分岐の重みとして参照される。
2.3 遷移の実装: season_remaining の on_min と 0/1 重みの pick
season_remaining の on_min(GameElementDefinition.md 6.3 節)で、次の季節への遷移を行います。
残り tick 数が 0 に達した瞬間に発火します。on_min の中身は、誰かが操作しているとは限らない場所として
解決されます(書ける起点は GameElementDefinition.md 14.1 節の表、操作の関係の役は同 11.5 節
「役を書ける場所」)。遷移が触るのはすべて world 自身のプロパティなので、制約にはなりません。
エンジンの active には「現在の値によって set の内容を変える」条件分岐の記法がないため、遷移は
0/1 の重みプロパティを参照する pick(同 10 節)で表現します。重みが 0 の候補は選ばれないため、0/1 の重みは
そのまま決定的な条件分岐として機能します(確率のための仕組みを、確率 0/100% の極として使う形です)。
pick は次の順に入れ子になります。
- どの季節から遷移するか:
season_is_calm/season_is_wet/season_is_dryを重みとする候補。現在の 季節に対応する候補だけが重み 1、他は 0。 - 初回サイクルか否か:
first_cycle_flag/later_cycle_flagを重みとする候補。 - 持続時間: 初回サイクル側は固定 30 日(2,880 tick)を
setするだけ。2 周目以降側は 24/30/36 日 (2,304/2,880/3,456 tick)のどれかを等確率で選ぶ。
葉の候補はいずれも set で「次の季節」と「新しい season_remaining」を同時に設定し、dry→calm の葉
(=1 周が完了する瞬間)だけがさらに add で season_cycle を +1 します。
著者が書いた on_min は既定の下限クランプを置き換えるため、pick の再設定とクランプが干渉することは
ありません(weather_remaining も同様)。
3. 大気水分量・蓄熱量: 季節が駆動する貯水池
天気(4 節)を決める材料として、天気とは別に「大気水分量」「蓄熱量」という連続値プロパティを
維持します。derived を使わず、季節由来のレートを毎 tick 加算する「貯水池モデル」です。
atmospheric_moisture(worldの蓄積型プロパティ、range 0〜100):seasonの各 stage が持つレートを 毎 tick 加算する(calm: +0.1 / wet: +1 / dry: -0.2)。基準レート(1/tick、GameElementDefinition.md6.0 節)はwetの上昇レートに置く。湿度が上がる季節が基準で、calm/dryはその微変動という位置づけ。 季節レートは意図的に控えめな値に抑えており、季節ごとの天気の対比は、レートの絶対値の大きさではなく、 自己減算(後述)との大小関係(正味でどちらへ動くか)が作る。thermal_level(蓄熱量。同じくworldの蓄積型プロパティ、range 0〜1,920):seasonの各 stage が 毎 tick 加算する(dry: +1 / calm・wet: -1)。気温そのもの(ambient_temperature)へ季節レートを直接 加算すると、±1/tick でも 1 季節のうちに季節の tick 数ぶん動いてしまうため、atmospheric_moistureと同じ「貯水池のstagesが表向きの値をmodifyする」パターンを 使う:thermal_levelのstages(cool: 〜959 /mild: 960〜1,919 /hot: 1,920〜)がambient_temperatureを -5 / ±0 / +8 する。dry は 0 から上限 1,920 へ登り「乾季後半ほど暑い」を、 calm は 0 へ下り「乾季の暑さからじわじわ涼しくなる」を表現する。hotstage は灼熱(scorching)の抽選重みscorching_weightも駆動する(4.3 節)。 上限はhot閾値 1,920 と同値とし、残暑(calm へ持ち越される灼熱)は無い: calm に入った最初の tick で蓄熱が 1,919 へ下がってmildになるため、灼熱と気温 +8 は乾季の終了と同時に消える(stage のminは「その値を含む」判定のため、上限に張り付いている間もhotは有効)。「暑さ = 乾季」という対応を プレイヤーの体感と一致させる意図的な設計で、暑さの長さの揺らぎは季節の持続日数のランダム性 (2.3 節)だけが表現する。- 降雨中の自己減算: 天気(4 節)が
light_rain/heavy_rain/stormのいずれかである間、そのweatherのstages自身が持つpassiveのaddとして、毎 tickatmospheric_moistureを減算する (-1.1 / -1.3 / -2。激しい雨ほど速く消費する。GameElementDefinition.md6.4 節のprogress/feverish例と同じ、ステージ自身が値を変化させるパターン)。どの減算量もwetの季節レート(+1/tick)をやや上回る (正味 -0.1 / -0.3 / -1)ため、降り続けた雨はいずれ水分を減らして晴れ間を呼び込みます。light_rainの 減算量はcalmの連続未降雨時間(3.2 節)を決める主要パラメータでもあります。 - 雨季の「後半ほど嵐・大雨」は、この大気水分量とは別の季節内進行度
monsoon_level(4.3 節)が保証する。 - 上限・下限は既存ルール通り、プロパティ側の
rangeでクランプする。
layered_noise(時間軸のノイズ)によるレート変動は実装していません。天気の遷移自体が数時間ごとの
pick(4 節)という確率過程であるため、雨がいつ降るかは十分にばらつくことを統計テストで確認済みです
(将来オプションは 3.3 節、その採否は 3.4 節)。
「季節後半ほど過酷になる」は、消費要因を持たない単調な積分値が表現します。 thermal_level には
消費要因がないため、dry の間は単調に積み上がり、追加の「経過度」計算をしなくても乾季後半に hot 帯へ
達して暑くなります。一方、大気水分量は雨自身の自己減算という消費要因を持つため単調には積み上がらず、
wet の間は各天気の正味レートが釣り合う水準(humid 閾値付近、4.3 節)を上下し続けます。そこで雨季側の
「後半ほど嵐・大雨」は、thermal_level と対称のもう 1 つの単調な積分値 monsoon_level(雨季の深まり、
4.3 節)が保証します。calm は 3.1 節の通り dry/wet とは逆向き(回復方向)のレートを持ち、後半に
向かうほど穏やかになっていく季節です。
3.1 calm のレートを「ほぼ0」にしない理由
巡回順(2.1 節)は calm が常に dry の直後に来ます。dry の終わり、大気水分量は最も低く気温は最も高い
水準にあるため、calm のレートを「ほぼ 0」にするとこの極端な水準のまま 30 日間ほとんど動かず、4.3 節の
重みが「乾き寄り」に張り付いて calm の間ずっと晴れ・曇りしか起きなくなります。そこで calm にも
意味のある向きを持ったレートを与えます。
- 大気水分量:
dryはマイナス(-0.2)、calmはプラス(+0.1。乾季で下がりきった水準から、雨季にふさわしい 水準へじわじわ回復)、wetはさらにプラス(+1)。 - 蓄熱量:
dryはプラス(+1)、calmはマイナス(-1。乾季の暑さから涼しさへじわじわ下降)、wetも マイナス(-1、下限 0 のcool帯に張り付く)。
これにより、大気水分量・気温はどちらも 3 季節を通じて 1 つの連続した波(谷→回復→頂点→下降→谷…)を描きます。
calm は谷から中腹への回復区間にあたり、4.3 節の重みが「乾き寄り」から「湿り寄り」へ連続的に変化していく
途中を過ごすため、晴れの日も雨の日もあるという 3.2 節の挙動が自然に生まれます。
3.2 calm での降雨頻度の狙い: 「連続未降雨時間を長くしすぎない」雨
calm はゲーム開始直後に来る最初の季節であり、プレイヤーは蓋のない容器だけで水を確保する序盤です。晴れが
続くと蓋のない容器の水は蒸発してしまうため、渇水で詰まないよう、calm の連続未降雨時間(雨が止んでから
次の雨までの長さ)が長くなりすぎないことを狙います(雨が一定周期で規則正しく降ることを求めるものでは
ありません)。
この頻度は新しい仕組みを必要とせず、既存のレートのバランスだけで決まります。
- 上昇側:
calmの大気水分量レート(3.1 節、+0.1/tick)が、雨が止んだ直後の水準から、4.3 節のmoderate段階(light_rain_weightが初めて意味のある値を持つ段階)の閾値まで登り返すのにかかる 日数。これが連続未降雨時間の実体です。 - 下降側: 降雨中の自己減算(3 節)が、一度
light_rainが選ばれてから大気水分量をどこまで押し戻すか。 ここが深いほど、次の上昇側にかかる日数(=連続未降雨時間)は長くなります。
実装値では、calm 中の light_rain の正味レートは自己減算 -1.1 と季節レート +0.1 の和で、
weather_remaining 1 回分(16〜24 tick、4.2 節)の雨がそのぶん水分を押し戻します。押し戻した分を
+0.1/tick で登り返すあいだが連続未降雨時間です(上の「上昇側」。実測は 3.5 節)。moderate 帯の light_rain_weight は 20に抑えており、閾値に触れてから
実際に雨が選ばれるまでの待ち時間も間隔に上乗せされます。閾値のすぐ上では heavy_rain/storm の
重みが 0(moderate 段階、4.3 節)のため雨はほぼ light_rain で、水分が稀に humid(60)まで
登り切った場合にのみ大雨が起こりえます(実測でも calm の大雨は tick のごく一部で、許容する揺らぎです)。
自己減算を弱めると「押し戻す」度合いが浅くなり、連続未降雨時間は短くなります(両者は独立ではなく
直結しています)。逆に連続降雨時間を伸ばす方向には自己減算はあまり効きません: moderate 帯では雨が
続けて選ばれる確率自体が重み比で頭打ちのため、calm の雨は 1〜2 周期(実測は同じファイルの
rain_streak の season: calm・segment: overall)で止むのが基本形です。
3.3 固定レートによる天気の単調さについて
3 節・3.1 節のレートは add(GameElementDefinition.md 8.4 節)のリテラル定数であり、他のプロパティの
現在値を参照して動的に変えることはできません(同 9.2 節)。毎 tick の量を現在値で動かせるのは
transfer だけで(同 8.4.1 節。付随する加減算を実際に移動した量へ比例させる linked_add を含む、
同 9.5 節)、その「実際に移動した量」を決めるのは移送元の残量(allow_overflow が既定の false なら
移送先の空きも)であって、任意のプロパティの現在値ではありません。そして transfer は移し元と
受け取り先がそろって初めて書ける動詞(同 9.5 節)なので、大気水分量のレートはどちらの向きもこの形に
乗りません——増やす向きには移し元となる在庫、つまり湿り気を蓄えていて大気へ出せるプロパティが
ありません。減らす向きは、大気水分量自身が range 0〜100 の貯水池(3 節)なので移し元にはなれますが、
減った分を受け取るプロパティがありません(雨で溜まる容器の水はその容器自身の add で、大気水分量から
移ってくるのではありません。LiquidContainerSystem.md 7 節)。
ここでは量の側を豊かにする道がありません。そのため「同じ季節に入るたびに、大気水分量が毎回ほぼ
同じ軌跡をたどる」という単調さが生じえます。現時点の実装は、
天気遷移そのもののランダム性だけでこの単調さを崩しています。将来単調さが気になった場合の追加手段が、
既存の語彙の範囲にあります。変えられるのは量ではなくブロックの選ばれ方です。
- 季節開始ごとのレート選び直し: 季節の遷移(2.3 節の
on_min)で「今回のレート区分」を表す専用プロパティをpickでsetし、calm/wet/dryそれぞれのstagesに、その区分ごとのconditionsで切り替わる複数のaddブロックを用意する。新しいエンジン機能は不要。
この手段は入れないと決めてあります(3.4 節)。ここに残しているのは、実際に遊んで単調だと感じた ときの入れ方で、まだ決まっていない選択肢ではありません。
3.4 季節ごとのレート選び直しは入れない
3.3 節の追加手段は入れません。 大気水分量のレートは、季節ごとに固定のままにします。
単調さは、軌跡ではなく天気で測ります。 大気水分量は抽選の重みを作るための内部の値で、プレイヤーが 受け取るのはそこから引かれた天気のほうです。同じ季節に何度入っても同じ天気の並びになるなら単調で、 回ごとに違うなら、たとえ水分量の軌跡が似ていても単調ではありません。
実測は「回ごとに違う」側です。 見るのは stats/climate.yaml の
weather_hours です。ここは season_duration と同じく、季節インスタンス1本を1標本として測って
います——tick を1標本に採る temperature・season_moisture_rate や、区間1本を1標本に採る
rain_streak とは標本の単位が違い、標本数が季節インスタンスの本数と一致するのがその印です。散らばりが
そのまま「同じ季節が毎回どれだけ違うか」になります。calm で小雨だった時間は、少ないほうの 5% が
43 時間の回、
多いほうの 5% が 76 時間の回。
wet の嵐はもっと開いていて、34 時間の
回から 128 時間の回まであります。
季節の中でも、雨は規則正しくは降りません。 non_rain_streak は雨の止んでいた区間 1 本を 1 標本として
測ったもので(標本数が季節インスタンスの本数より桁で多いのがその印)、calm では
0.42 日の区間から
4.21 日の区間まで散っています。
こちらは季節どうしの違いではありません——同じ calm の中で、すぐ次が降ることも何日も待たされることも
あるという話です。上の「毎回違う」を支えるのは weather_hours のほうだけで、この節はその中身が
規則的に並んでいないことを言い添えるものです。
レートを季節ごとに引き直しても、既に散っているものへ2つ目の乱数を重ねるだけです。宣言が増えるぶんだけ、
どちらの乱数が今の天気を作ったのかが読めなくなります。実際に遊んで「どの calm も同じに見える」となった
ときに、3.3 節の形で足せます。
3.5 レート・閾値・重みは、この値で据え置く
3 節のレート・貯水池の閾値・4.3 節の重みは、いま宣言されている値のままにします。
狙いとして書いてあることに、実測が届いているためです。calm の連続未降雨時間は「長くしすぎない」
(3.2 節)が狙いで、実測の平均は
2.17 日。季節ごとの天気の
対比も、weather_hours を季節で並べれば雨の時間が段違いに出ています。
これ以上動かす根拠は、シミュレーションからは出ません。 残っているのは「遊んでみて渇きすぎるか、
だれるか」という体感の側の問いで、それは測る道具の答えられる問いではありません。測って出てこないものを、
測る前に動かさない——体感から動かす番が来たときも、概要の要件を満たし続けているかは
tests/world-codex/climateSystem.test.ts が見ます。この検査は npm test に入っているので、
値を触った人が回し忘れても、PR の CI が赤くします。
4. 天気: worldプロパティとしての短期変動
4.1 大気水分量から独立させる理由
天気を atmospheric_moisture の stages(閾値)として表現すると、「大気水分量の値が 1 つ決まれば天気も
1 つに決まる」ため、大雨と嵐のように同程度の大気水分量でも降り方が変わりうる現象を表現できません。そこで
天気は大気水分量から独立した world プロパティとし、大気水分量は「天気がどちらに転びやすいか」の重み
(4.3 節)としてのみ関与させます。
4.2 プロパティ構成
weather: 現在の天気を表す symbol プロパティ。core.yamlの 7 値すべてが遷移先候補です。強度順に、storm(暴風雨)/heavy_rain(大雨)/light_rain(小雨)/cloudy(曇り)/clear(穏やかで 過ごしやすい普通の晴れ。この島の標準的な天気で、晴れ系の中で最も高頻度)/sunny(日差しが強い晴れ)/scorching(灼熱。sunnyより酷い、特別な対策が必要な極端な暑さ。乾季後半にのみ発生する 4.3 節参照)。stormとscorchingは両極の「極端な天候」で、それぞれ雨季後半・乾季後半の風物詩として対称に 扱います(嵐はmonsoon_level、灼熱はthermal_levelという季節内進行度が駆動する。4.3 節参照)。 画面へ出る日本語名を持つのはlocale/ja.yamlのsymbol_texts(Localization.md) で、clearが「晴れ」・sunnyが「快晴」・scorchingが「灼熱」です。weather_remaining: 残り tick 数。16/20/24 tick の等確率pickで、遷移のたびに ロールし直す(再ロールは 4.3 節の遷移pickの葉が天気のsetと同時に行う)。sunny_weight/clear_weight/cloudy_weight/light_rain_weight/heavy_rain_weight/storm_weight/scorching_weight:weatherの各候補に 1 つずつ対応する、ただの数値プロパティ。値は著者が直接setするのではなく、4.3 節の通りatmospheric_moisture・monsoon_levelのstages(scorching_weightのみthermal_levelのhotstage)が常時modifyして維持します(ベース値 0 のため、どの stage もmodifyしない候補の 重みは 0 =選ばれません)。
4.3 遷移先: 天気ごとの重みプロパティを貯水池(大気水分量・蓄熱量)の stages が modify する
weather_remaining の on_min(self)で pick(GameElementDefinition.md 10 節)を実行し、次の天気を選びます。
外側の pick の各候補は対応する *_weight プロパティを重みとして参照し(同 10.2 節の「既存プロパティへの参照」)、
選ばれた候補の内側の pick が持続時間(16/20/24 tick)を等確率で選び、葉の set が「次の天気」と
「新しい weather_remaining」を同時に設定します(構造は 2.3 節の季節遷移と同型。完全な定義は core.yaml 参照)。
weather_remaining:
# 初期値: day1の最初の遷移までの猶予
value: 20
range: # 0に達した瞬間にon_minが発火する
min: 0
max: 999999
passives:
- add:
self:
weather_remaining: -1
on_min:
pick:
- weight: {prop: sunny_weight}
pick:
- weight: 1
set:
self: {weather: sunny, weather_remaining: 16}
- weight: 1
set:
self: {weather: sunny, weather_remaining: 20}
- weight: 1
set:
self: {weather: sunny, weather_remaining: 24}
- weight: {prop: clear_weight}
# ... 以下、残りの候補も同じ形(core.yaml参照)
*_weight プロパティ自身の値は、atmospheric_moisture の stages(数値の半開区間。GameElementDefinition.md
6.4 節)が passives の modify として常時上書きすることで決まります(weather の stages が ambient_brightness を
modify しているのと同じパターンで、新しいエンジン機能は不要です)。実装値は以下の通りです。
atmospheric_moisture:
value: 0
range:
min: 0
max: 100
stages:
- name: dry # 〜29.9: ほぼ晴れ(clearが最多)。雨はlight_rainのみ稀に(乾季の「稀に降る」を担う)
passives:
- modify:
self:
sunny_weight: 25
clear_weight: 45
cloudy_weight: 20
light_rain_weight: 1
- name: moderate # 30〜59.9: 小雨が現実的な選択肢に。大雨・嵐は重み0のまま
min: 30
passives:
- modify:
self:
sunny_weight: 10
clear_weight: 30
cloudy_weight: 25
light_rain_weight: 20
- name: humid # 60〜84.9: 雨が優勢になり、大雨・嵐も現れ始める(降らないのはclear/cloudyだけ、僅かに)
min: 60
passives:
- modify:
self:
clear_weight: 4
cloudy_weight: 3
light_rain_weight: 40
heavy_rain_weight: 30
storm_weight: 3
- name: saturated # 85〜: まず晴れない(sunny/clearの重みを立てない=0)。嵐・大雨が中心
min: 85
passives:
- modify:
self:
cloudy_weight: 5
light_rain_weight: 20
heavy_rain_weight: 40
storm_weight: 40
晴れ系(sunny/clear/cloudy)の重みも雨系と同じく atmospheric_moisture の段階に連動させ、固定の
重み定数にはしません。固定にすると「大気水分量がどれだけ高くても晴れが一定確率で残り続ける」下限が
原理的に外れなくなるためです。上の実装では saturated 段階が sunny_weight/clear_weight を立てない
(=0 のまま。降らない候補として残るのは cloudy だけ)ことで
「まず晴れない」を、対称に dry/moderate 段階が heavy_rain_weight/storm_weight を立てないことで
「乾いている間・穏やかな季節の小雨は大雨・嵐にならない」(3.2 節の保証)を表現しています。clear の重みが
立つ段階では、晴れ系の中で clear を最も大きくし、この島の標準的な天気にしています。
scorching_weight だけは大気水分量ではなく、もう一つの貯水池である蓄熱量(thermal_level、3 節)の
hot stage(1,920 以上)が modify で駆動します(ambient_temperature: +8 と同じブロックで
scorching_weight: 50 を立てる)。蓄熱量が hot に達するのは乾季の後半に入ってからで、灼熱は乾季後半の天気になります
(tests/world-codex/climateSystem.test.ts の「scorching(灼熱)は乾季後半にだけ発生し、雨季には決して発生しない」が、最初の乾季の前半に出ないことも見ます)。
「雨季後半ほど嵐・大雨が増える」は、thermal_level と対称の季節内進行度 monsoon_level(雨季の
深まり)が保証します。 monsoon_level は wet で +1/tick(0→上限 1,920)、calm/dry で
-1/tick と減衰する、range 0〜1,920 の蓄積型プロパティで、stages が重みへ上乗せします:
deepening(960 以上)が heavy_rain_weight +15 を、peak(1,920)が
heavy_rain_weight +30 と storm_weight +40 を立てます(大気水分量は雨自身の自己減算のため季節内で
単調に増えず「後半ほど」を表現できない。3 節末尾と同じ解法)。
この上乗せには conditions(GameElementDefinition.md 14 節)で「大気水分量が閾値以上のとき」というゲートが付いています
(deepening は 60 以上、peak は 45 以上。stage の WhenOwnStage ゲートと conditions は AND で
重なる、同 6.4 節)。無条件に上乗せすると、monsoon_level の減衰が終わらない乾季序盤に、水分が尽きた
空からも大雨が降ってしまうためです。逆にこのゲートのおかげで、乾季に入っても水分がまだ高い間は上乗せが
生き、嵐・大雨の名残が自然に表現されます。実測でも、wet を 3 等分した嵐・大雨の tick 比率は序盤から
終盤へ単調に増えます(stats/climate.yaml の weather_hours の segment。
統計テスト「雨季(wet)は後半ほど嵐・大雨が増える」(tests/world-codex/climateSystem.test.ts)が回帰検知します)。
ただし名残の振る舞いは意図的に非対称です。嵐には名残があります(雨季末の高い水分が消費し尽くされる
までの乾季序盤には嵐・大雨が残る)が、灼熱には名残(残暑)がありません(蓄熱量の上限 = hot 閾値のため、
calm に入った瞬間に灼熱は消える。3 節)。プレイヤーから見た「乾季の暑さが長い年」は、残暑ではなく季節の
持続日数のランダム性(2.3 節)が表現します。ゲーム開始直後の最初の calm は、蓄熱量の初期値が中立
(1,000 = mild)のため灼熱は発生しません。
段階の境界(min)を跨いだ瞬間に重みが階段状に飛ぶ点は、weather_remaining の遷移自体が数時間に 1 回
(4.2 節)しか起きないため、体感上の不自然さにはならないと考えます。
雨季の降雨の緩急は、季節レートと自己減算の大小関係が作ります。 wet 中の各天気の正味レートは、
light_rain -0.1/tick(季節レート +1 - 自己減算 1.1)、heavy_rain -0.3/tick、storm -1/tick、
非降雨時 +1/tick です。どの雨も正味マイナスのため降り続けばいずれ晴れ間を呼び込み、非降雨時の +1 が
素早く水分を回復させて次の雨へつなぐ——という充放電の往復が、humid 閾値(60)付近を上下しながら
「ほとんど雨だが、まとまった雨と晴れ間が交互に来る」という雨季の体感を作ります。実測でも wet は雨の
tick が大半を占め、連続降雨時間は連続未降雨時間より長くなります(同じファイルの weather_hours・
rain_streak・non_rain_streak の season: wet・segment: overall)。なお light_rain の
自己減算(-1.1)は雨季のためではなく、calm の連続未降雨時間を約 2 日にする(3.2 節)ことから決まって
います。1 つの定数が複数の季節の挙動に効くため、変更時は 3.2 節の充放電サイクルと本節の正味レートの
両方を確認する必要があります。
4.4 二回連続で同じ天気になった場合
weather_remaining は遷移のたびに 4.2 節の長さの範囲で改めて pick されるため、たまたま同じ天気が連続して
選ばれ、プレイヤーからは天気が変わらないまま 1 回の持続の上限を超えて継続しているように見えることがあります。これは
独立試行の結果として自然に起こる不確実性であり、狙って作り込んだ特別なルールではありません。
4.5 風向きは季節が配る
風向き(wind)も同じ形の短期変動です。 tailwind/crosswind/headwind を、季節の段が配る
重み(tailwind_weight ほか)で、wind_remaining の 32 tick ごとに引き直します。天気と違って持続の長さは振りません。
島の上での暮らしには効かず、読むのは航路を渡るのにかかる時間だけなので、値の意味と重みの表は
Voyage.md 3.1 節が持ちます。ここに置くのは「季節が駆動する短期変動という点で
天気と同じ仕組みだ」ということだけです。
5. 難易度の初期補正: 専用カウンタのstagesによる大気水分量の底上げ
序盤の理不尽な過酷さを避けるため、ゲーム開始から特定の期間だけ大気水分量を底上げし、天気(4 節)が雨に
転びやすい状態を作ります。天気そのものを固定で雨にするのではなく、あくまで抽選の元になる
atmospheric_moisture の値を操作するだけであり、それでも晴れる可能性はゼロではありません(5.3 節)。
補正は「瞬間に 1 回だけ発火する」仕組みではなく、「特定の期間中ずっと、通常より高いレートで水分を
積み増し続ける」継続的な加算として、stages(現在値がその区間内にある間ずっと passives が有効。
GameElementDefinition.md 6.4 節)で表現します。
補正(ゲーム開始 2 日目、最初の乾季 10 日目前後)は無関係な別々の意図によるものなので、1 つのプロパティに
両方の stages をまとめず、それぞれ専用の「ゲーム開始からの経過 tick を数えるだけの」プロパティを 1 つずつ
用意します。
5.1 ゲーム開始2日目
early_rain_calibration:
value: 0
passives:
- add:
self:
early_rain_calibration: 1
stages:
- name: idle
- name: boosting
# 96 tick(1 日)が明けた、2日目の開始
min: 96
passives:
- add:
self:
atmospheric_moisture: 0.6
- name: done
# 3日目の開始でオフに戻る
min: 192
early_rain_calibration はゲーム開始と同時に毎 tick +1 されるだけの専用カウンタです。boosting 区間
(2 日目の 96 tick 分)にいる間だけ atmospheric_moisture への追加加算(+0.6/tick。calm の +0.1 と合わせて
+0.7/tick)が有効になり、3 日目に入ると自動的にオフへ戻ります(stages は現在値だけで区間を決めるため、
明示的なリセットや発火済みフラグは不要です)。2 日目の朝には moderate を超えて雨の抽選が現実的になり、
夕方までに humid に達します。統計テストでは全シードで 2〜3 日目に雨が降りました。
5.2 最初の乾季、10日目前後
季節の巡回順(2.1 節)と初回サイクル固定 30 日(2.3 節)により、最初の dry は 61 日目に始まり、その
10 日目前後は絶対 71 日目(tick 換算で (71-1) × 96 = 6,720 tick)と、ゲーム開始前から確定します。
first_dry_rain_calibration:
value: 0
passives:
- add:
self:
first_dry_rain_calibration: 1
stages:
- name: idle
- name: boosting
# 71日目の開始(最初の乾季開始=61日目 + 10日)
min: 6720
passives:
- add:
self:
atmospheric_moisture: 2
- name: done
# 72日目の開始でオフに戻る
min: 6816
boosting の +2/tick は dry の -0.2/tick を大きく上回る(正味 +1.8/tick)ため、乾ききった水分量が
71 日目のうちに moderate→humid へ達して雨の抽選が大きく傾きます(統計テストでは全シードで
71〜73 日目に雨が降りました)。5.1 節と全く同じパターンで、season/season_cycle を実行時に参照する
必要はありません。ただし 6720/6816 という具体的な tick 数は「初回サイクルは固定 30 日」という
2.3 節の前提に依存しており、季節の巡回順や初回サイクルの日数が変われば再計算が必要です。
5.3 なぜ天気を直接固定しないか
5.1・5.2節とも、天気を強制的に light_rain 等へ set するのではなく atmospheric_moisture を操作するだけに
留め、「天気は常に抽選で決まり、不確実性を持つ」という 4 節の設計原則を難易度補正でも崩しません。補正は
あくまで確率を傾けるだけであり、絶対に雨が降ると保証するものではありません。
6. 設計原則のまとめ
- 季節・天気とも、専用の
WorldObjectインスタンスではなくworld自身のプロパティ(現在値+残り時間)として 表現する - 季節は穏やか/雨季/乾季を固定順で巡回する。天気は季節から独立した symbol プロパティであり、季節の 値そのものからは直接決まらない
- 天気・大気水分量・蓄熱量はいずれも「独立変数への tick ごとの加減算」のみで表現し、
derivedは使わない。 気温・天気の重みのような「表向きの値」は、スケールした貯水池のstagesがmodifyで駆動する (3 節・4.3 節) - 状態遷移の条件分岐(今の季節・初回サイクルか否か)は、0/1 の重みプロパティを参照する
pickとして表現する (2.3 節) - 大気水分量は、天気そのものの stages 閾値としては使わず(4.1 節)、天気候補ごとの重み専用プロパティを
駆動する入力としてのみ使う(4.3 節)。晴れ系・雨系のどちらの重みも大気水分量に連動させ、固定の重み定数には
しない(極端な条件下で「絶対に晴れない」「絶対に嵐にならない」を表現できるようにするため)。
clearの 重みが立つ段階では晴れ系の中でclearを最も重くし、この島の標準的な天気にする - 両極の極端な天候は、消費要因を持たない単調な季節内進行度が駆動する: 嵐・後半の大雨は
monsoon_levelが、 灼熱はthermal_levelのhot帯(乾季後半)が重みを立てる(4.3 節)。名残は意図的に非対称: 嵐・大雨は 乾季序盤に名残が残るが、灼熱は蓄熱量の上限 =hot閾値により乾季の終了と同時に消える(残暑なし) calmはdry/wetと逆向き(回復方向)のレートを持たせ、大気水分量・蓄熱量を 3 季節を通じた 1 つの 連続した波にする(3.1 節)。calmでは上昇レートと自己減算のバランスにより、連続未降雨時間を概ね 2 日に 抑える(3.2 節)- 季節レートは控えめな値に抑え、季節ごとの天気の対比はレートの絶対値ではなく自己減算との大小関係で作る (3 節・4.3 節)
dryの雨は「一度も降らないか、多くても2〜3回」に抑える: dry帯のlight_rain_weightを最小(1)にし、 雨季の名残と序盤補正(5.2 節)だけが乾季の数少ない雨を担う(4.3 節)- 難易度の初期補正は、天気を固定せず大気水分量という抽選の元データだけを操作し、専用カウンタの
stagesに よる期間中の継続加算として表現する(5 節) - 季節の持続時間は初回サイクルのみ固定 30 日、2 周目以降はランダム化する(2.3 節)
- 要件が乱数に依存する部分は、複数シードのシミュレーションによる統計テストで検証する(概要参照)
7. 未決事項・今後の検討課題
- 5.2 節の
6720/6816という tick 数は 2.3 節の「初回サイクル固定 30 日」に依存する導出値であり、2.3 節の 前提が変わった場合はこの節も再計算が必要 - scorching への「特別な対策が必要な極端な天候」としてのゲームプレイ上の意味づけ(現状は天気の値が
変わるだけで、明るさ以外への影響は未実装)。雨側は凍死が受け持つことになった
(
VitalsSystem.md8.3 節)ので、残るのは暑さの側だけ——対称に置くなら 熱中症だが、暑さで死ぬ道を足すかは同 未決事項節が持つ - 天気に反応する別オブジェクト(例: 装備の防水性)を作る場合の参照経路。
subject: worldを条件・重みから 参照する仕組みが未実装(GameElementDefinition.md14.1 節・17 節)のため、ancestor経由での 参照が必要になる。本書の季節・天気遷移ロジック自体はすべてworld自身のon_min(対象は常にself)で完結しており、影響を受けない