RPGの「ボリューム」は、どう増やすのか。職業を足す、敵を足す、ステージを足す。そのたびに戦闘やマップのコードを書き換えていたら、すぐに破綻する。Archipelago Saga は逆だ。中身(データ)と仕組み(ロジック)を完全に分け、コンテンツは“データを1行書き足すだけ”で増えるように作ってある。第2回は、その「データ駆動設計」を実スキーマつきで開く。
▶ 第1回(無ビルドの土台と window.G)の続き。全体像は連載ハブから。まず完成版を遊ぶと、これから足す「中身」が何かが分かる。
目次
第2回のゴール
「敵・職業・スキル・ステージ」を、ロジックから切り離した純データとして設計する考え方を身につける。最後に、実際に新しい敵を1体追加して、戦闘にもステージにも自動で登場させる(戦闘・マップのコードは1文字も触らずに)。これがデータ駆動の威力だ。
Step 1. 「データ駆動」とは何か
「データ駆動」とは、ゲームの中身を“コード”ではなく“データ”として持ち、仕組み側はそのデータを読んで動くだけという設計のこと。第1回の window.G を思い出してほしい。js/data/ の各ファイルは、G に「ただのデータ」を置くだけ。js/engine/ や js/scenes/ は、そのデータを読むだけ。両者は G を介してしか出会わない。
この分離が効くのは、「足す」ときに触る場所が1か所で済むから。新しい敵のデータを足せば、戦闘エンジンも図鑑画面も、勝手にそれを拾う。逆に言えば、データの「形(スキーマ)」さえ決めておけば、あとは埋めるだけでコンテンツが増える。
Step 2. 職業を定義する — classes.js
純データの実例を見よう。9職業はそれぞれ「初期値 base・成長 growth・覚える技 skills」を持つだけのオブジェクトだ。ロジックは無い。
// js/data/classes.js(抜粋)
window.G = window.G || {};
var P = "../assets_ai/characters_norm/"; // 立ち絵フォルダ
G.CLASSES = {
ranger: {
name: "Ranger", role: "バランス型", portrait: P + "hero_ranger.png",
blurb: "弓と罠で戦う万能の主人公。",
base: { hp: 32, mp: 12, atk: 11, def: 8, mag: 7, res: 7, spd: 12, luk: 8 }, // Lv1の能力
growth: { hp: 6, mp: 2, atk: 2, def: 1, mag: 1, res: 1, spd: 2, luk: 1 }, // 1レベルごとの伸び
skills: ["quick_shot", "trap", "beast_call", "piercing_arrow", /* … */] // 覚える技のid
},
warrior: { /* … 同じ形 … */ },
mage: { /* … 同じ形 … */ }
// …全9職業が、まったく同じ“形”で並ぶ
};
G.CLASS_ORDER = ["ranger","warrior","mage","archer","paladin","rogue","cleric","dwarf","bard"];
能力値は base と growth の2つだけ。レベル n の能力は「base + growth × (n-1)」で計算できる。テーブルを全レベル分持たず、初期値と伸び率だけ持つのがコツだ。データが小さく保て、バランス調整も2つの数字をいじるだけで済む。
「同じ形のデータを、計算で少し加工する」処理もデータ側に置ける。例えば各職業に 男女の立ち絵と コスト資源(MP/SP)を後付けするのは、ループ1つだ。
// 魔法職はMP、それ以外はSP(内部キーはmpのまま=セーブ互換)
G.MAGIC_CLASSES = ["mage", "paladin", "cleric", "bard"];
G.CLASS_ORDER.forEach(function (k) {
G.CLASSES[k].resource = G.MAGIC_CLASSES.indexOf(k) >= 0 ? "MP" : "SP";
});

classes.js / skills.js の純データから描かれる。データを足せば選択肢がそのまま増える。Step 3. スキルとステータス — skills.js
データ駆動の真価が出るのが技だ。1つの技は「効果を表す数項目」のオブジェクトに過ぎない。新しい技=この形を1行足すだけ。戦闘エンジン(第3回)は、この項目を読んで処理を分岐する。
// js/data/skills.js(schema と抜粋)
// mp:消費 / target:対象 / kind:種別 / power:威力(100=通常attack相当) / element:属性 / effect:状態異常
window.G = window.G || {};
G.SKILLS = {
ice_lance: { name: "アイスランス", mp: 6, target: "enemy", kind: "mag", power: 135, element: "ice", desc: "氷の槍で貫く。" },
fireball: { name: "ファイアボール", mp: 8, target: "all_enemies", kind: "mag", power: 95, element: "fire", desc: "炎で全体を焼く。" },
heal: { name: "ヒール", mp: 6, target: "ally", kind: "heal", power: 115, element: null, desc: "味方一人を回復。" },
poison_arrow:{ name:"ポイズンアロー", mp: 6, target: "enemy", kind: "phys", power: 80, element: null,
effect: { status: "poison", turns: 4, chance: 0.9 }, desc: "毒を盛る矢。" }, // ← 状態異常つき
double_shot:{ name: "ダブルショット", mp: 7, target: "enemy", kind: "phys", power: 78, element: null, hits: 2, desc: "二連射。" }
// …全72技が同じ形で並ぶ(9職 × 8技)
};
ここで効いてくるのが「状態異常も、ただのデータ表」という設計。effect.status: "poison" が何を意味するかは、別の G.STATUSES という倍率・フラグの早見表に書いてある。技側は「poison を付ける」とだけ言い、効果の中身は表が持つ。
// 状態異常レジストリ(battle-coreが参照する“早見表”)
G.STATUSES = {
poison: { name: "毒", dot: 0.06, bad: true }, // 毎ターン最大HPの6%減
guard: { name: "ガード", stat: { def: 1.6, res: 1.4 }, dmgTaken: 0.75 },
atkup: { name: "攻撃UP", stat: { atk: 1.4, mag: 1.4 } },
spddown: { name: "鈍足", stat: { spd: 0.6 }, bad: true }
// …
};
この二段構え(技は「poisonを付ける」と宣言/効果の実体は表が持つ)のおかげで、毒のダメージ量を変えたければ dot: 0.06 の1か所を直すだけ。全部の毒技に一斉に反映される。
Step 4. 敵を定義する — enemies.js
敵も同じ。能力・弱点・ドロップ・行動AIまで、すべてデータで宣言する。AIは「重み付きの抽選表」だ。
// js/data/enemies.js(抜粋)
var EA = "../assets_ai/enemies/";
function atk(name, power, extra) { // 行動を作る小さなヘルパー
return Object.assign({ name: name, kind: "phys", power: power, target: "enemy", element: null }, extra || {});
}
G.ENEMIES = {
slime: {
name: "スライム", art: EA + "enemy_slime.png", element: null, weak: ["fire"], resist: [],
stats: { hp: 28, mp: 0, atk: 8, def: 6, mag: 0, res: 4, spd: 6, luk: 4 },
xp: 8, gold: 6, drops: [{ item: "potion", chance: 0.25 }],
actions: [ // ← 重み付きAI:weightが大きいほど選ばれやすい
{ weight: 4, action: atk("たいあたり", 100) },
{ weight: 1, action: atk("とける", 80, { effect: { status: "defdown", turns: 2, chance: 0.6 } }) }
]
}
// …
};
weak: ["fire"] と書けば、戦闘エンジンが炎属性のダメージを自動で増やす。actions の weight は「行動の出やすさ」。スライムなら「たいあたり4:とける1」の比率で抽選される。敵の“賢さ”すら、コードでなくデータの数字で決まる。
Step 5. 本体を触らず増やす — 拡張ファイル方式
敵を大量に増やすとき、enemies.js 本体をどんどん膨らませると、レビューも差分管理も辛くなる。そこで 追加分は別ファイル enemies_ext.js に書き、本体の後で合流させる。第1回の「読込順」の出番だ。
// js/data/enemies_ext.js(末尾の合流処理)
(function () {
var EXT = { crab: {/*…*/}, boar: {/*…*/}, /* …追加の敵たち… */ };
// アート未生成の敵は“保留リスト”でスキップ → 図鑑が画像404にならない
var PENDING_ART_READY = true;
var PENDING = ["yeti", "griffon", "frostwolf", /* … */];
for (var k in EXT) {
if (!EXT.hasOwnProperty(k)) continue;
if (!PENDING_ART_READY && PENDING.indexOf(k) !== -1) continue;
G.ENEMIES[k] = EXT[k]; // ← 本体 G.ENEMIES に1体ずつ合流
}
})();
仕組みはシンプル。enemies.js が先に G.ENEMIES を作り、enemies_ext.js が後から同じ G.ENEMIES に追記する。本体ファイルは一切変更しない。副産物として、変更履歴も読みやすくなる。「今週足した敵」は拡張ファイルの差分にだけ現れるので、git で見返すときも、後から自分がレビューするときも、追加と変更が混ざらない。本体を触らない方式は、安全のためだけでなく、記録のためにも効く。増えていく物ほど、増やし方の規律が価値を持つ。さらに賢いのが PENDING リストだ。「データはあるが立ち絵をまだ生成していない敵」を安全に保留できる。アートが揃ったらフラグを true にするだけで、その敵が世界に出現する。データとアセットの進捗を切り離せる、実戦的な工夫だ。
Step 6. ステージも“ただのデータ” — stages.js
ワールドマップ(第4回)も、その正体はデータの配列だ。1つのノード(島・町・ボス)が、1つのオブジェクト。座標・出現する敵・ボスフラグ・報酬まで、すべてここに宣言する。
// js/data/stages.js(抜粋)
G.REGIONS = [ // 6地域の見出し(地図上の位置)
{ label: "Green Shores", x: 70, y: 120 },
{ label: "Verdant Coast", x: 420, y: 2100 },
// …全6地域
];
G.STAGES = [
{ id: "town_0_1", type: "town", name: "はじまりの村", x: 80, y: 200, region: 0, shopTier: 1, innCost: 10 },
{ id: 0, no: 1, name: "潮だまりの浜", x: 150, y: 380, region: 0, enemies: ["slime", "crab"] }, // ← 出現する敵
{ id: 1, no: 2, name: "さざ波の入江", x: 350, y: 560, region: 0, enemies: ["slime", "bat", "bandit"] },
// …
{ id: 8, no: 9, name: "守護者の心臓", x: 660, y: 2000, region: 0,
enemies: ["golem_titan"], boss: true, story: "boss1", reward: "chest" } // ← ボス戦
];
注目は enemies: ["slime", "crab"]。ここに書く文字列は、Step 4 で定義した G.ENEMIES のキー(id)だ。ステージは敵の“実体”を持たず、idで参照するだけ。boss: true や story: "boss1"(戦闘前の会話)、reward: "chest" といったフラグを足すだけで挙動が変わるのも、データ駆動ならでは。マップ画面のコードは、この配列を上から並べて描くだけでいい。
Step 7. 手を動かす — 敵を1体、追加してみる
設計の答え合わせをしよう。新しい敵「マッドクラブ」を追加して、序盤のステージに出現させる。触るのはデータだけだ。
① 敵データを足す(enemies_ext.js の EXT に1ブロック)
mudcrab: {
name: "マッドクラブ", art: EA + "enemy_mudcrab.png", element: null, weak: ["fire"], resist: [],
stats: { hp: 34, mp: 0, atk: 10, def: 9, mag: 0, res: 5, spd: 5, luk: 4 },
xp: 12, gold: 9, drops: [{ item: "potion", chance: 0.3 }],
actions: [
{ weight: 4, action: atk("はさみ", 110) },
{ weight: 1, action: atk("どろあそび", 60, { effect: { status: "spddown", turns: 2, chance: 0.6 } }) }
]
}
② ステージの出現リストに id を足す(stages.js)
{ id: 0, no: 1, name: "潮だまりの浜", x: 150, y: 380, region: 0,
enemies: ["slime", "crab", "mudcrab"] }, // ← "mudcrab" を1語足すだけ
これだけ。立ち絵 enemy_mudcrab.png を用意すれば(第5回のAI量産フローの出番)、戦闘に出現し、図鑑にも自動で載り、弱点の炎で多くダメージが入る。battle.js も worldmap.js も bestiary.js も、1文字も書き換えていない。これがデータ駆動の到達点だ。この気軽さがあると、「敵をあと20体増やす」が作業でなく楽しみに変わる。
💡 つまずきポイント:id のスペルミスは静かに失敗する。enemies: ["mudcrab"] と書いたのに敵キーが mud_crab だと、戦闘が「いない敵」を引いてエラーになる。参照する文字列=定義したキーを必ず一致させること(第1回の unknown scene と同じ落とし穴だ)。
設計Q&A:ここで迷いやすい2つ
Q. データは JSON ファイルにして読み込む方が普通では?
A. 普通のWeb開発ならそうなる。だがこのゲームは第1回で「file:// のダブルクリックで動く」と決めた。ブラウザは file:// で開いたページからの fetch(外部ファイル読み込み)を制限するので、JSONを読みに行く構成はローカルで動かない。データを .js ファイルにして <script> で読み込めば、この制限を踏まずに済む。「データ=JSファイル」は、無ビルド方針から逆算した必然の形だ。
Q. バランス調整はどうやる?
A. データだけで完結する。例えば「序盤の敵が固すぎる」と感じたら、enemies.js の def を2下げて再読み込みするだけ。ビルドも再起動も無いので、数値を変えて→ブラウザ更新→1戦してみる、の1周が数十秒で回る。この回転の速さが、データ駆動のもう1つの恩恵になる。調整した数値は「なぜその値にしたか」をコメントで残しておくと、後から自分が助かる。数字の理由は、1週間後の自分にとっては他人の決定と同じくらい謎になるからだ。
まとめ:第2回の要点
- 純データ:
js/data/はGにオブジェクトを置くだけ。ロジックは持たない。 - 能力は base+growth:全レベルのテーブルでなく、初期値と伸び率だけ持つ。
- 効果は早見表に集約:技は「poisonを付ける」と宣言し、実体は
G.STATUSESが持つ。 - AIもデータ:
actionsのweight(重み付き抽選)で敵の行動が決まる。 - 拡張ファイル方式:追加分は別ファイルで後から合流。本体は不変。未完成アートは保留できる。
- ステージは参照の配列:敵を
idで参照。boss/rewardはフラグ1つ。
次回(第3回)は、このデータを実際に動かす「戦闘コア」へ。weak/resist/effect/weight を読んで、CTB(チャージタイム制)でダメージと行動順を計算する battle-core.js を、画面(DOM)に一切依存しない形でどう書くか、実コードで開く。
※ AI生成コンテンツの開示:本作の BGM・効果音・キャラクター画像・ボイスは、すべて生成AIの出力物です(BGM=ACE-Step 1.5〈MIT・出力の商用利用可を配布元モデルカードで確認済み〉 / ボイス=Irodori-TTS 系の合成設計音声・実在人物の複製ではありません / 効果音=数値合成 / 画像=画像生成AI〈各サービスの利用条件で出力の商用利用可を確認のうえ使用。透過処理は BiRefNet〈MIT〉を rembg 経由で使用〉)。第三者の著作物は同梱していません。© 2026 Cooliris(本作のコード・構成・データの権利は作者に帰属します)。
👉 ← 第1回:無ビルドの土台 ・ 連載ハブ ・ ▶ 遊ぶ ・ ほかのアプリの作り方