JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

アプリの作り方

JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

RPGの「ボリューム」は、どう増やすのか。職業を足す、敵を足す、ステージを足す。そのたびに戦闘やマップのコードを書き換えていたら、すぐに破綻する。Archipelago Saga は逆だ。中身(データ)と仕組み(ロジック)を完全に分け、コンテンツは“データを1行書き足すだけ”で増えるように作ってある。第2回は、その「データ駆動設計」を実スキーマつきで開く。

▶ 第1回(無ビルドの土台と window.G)の続き。全体像は連載ハブから。まず完成版を遊ぶと、これから足す「中身」が何かが分かる。

目次
  1. 第2回のゴール
  2. Step 1. 「データ駆動」とは何か
  3. Step 2. 職業を定義する — classes.js
  4. Step 3. スキルとステータス — skills.js
  5. Step 4. 敵を定義する — enemies.js
  6. Step 5. 本体を触らず増やす — 拡張ファイル方式
  7. Step 6. ステージも“ただのデータ” — stages.js
  8. Step 7. 手を動かす — 敵を1体、追加してみる
    1. ① 敵データを足す(enemies_ext.js の EXT に1ブロック)
    2. ② ステージの出現リストに id を足す(stages.js)
  9. 設計Q&A:ここで迷いやすい2つ
  10. まとめ:第2回の要点

第2回のゴール

「敵・職業・スキル・ステージ」を、ロジックから切り離した純データとして設計する考え方を身につける。最後に、実際に新しい敵を1体追加して、戦闘にもステージにも自動で登場させる(戦闘・マップのコードは1文字も触らずに)。これがデータ駆動の威力だ。

Step 1. 「データ駆動」とは何か

「データ駆動」とは、ゲームの中身を“コード”ではなく“データ”として持ち、仕組み側はそのデータを読んで動くだけという設計のこと。第1回の window.G を思い出してほしい。js/data/ の各ファイルは、G に「ただのデータ」を置くだけ。js/engine/js/scenes/ は、そのデータを読むだけ。両者は G を介してしか出会わない。

図1:中身(データ)と仕組み(ロジック)は G 越しに出会う data/中身(定義する) classes.js(職業) enemies.js(敵) skills.js / stages.js … window.G 定義 engine / scenes(読むだけ) battle.js(戦闘) worldmap.js(地図) bestiary.js(図鑑) 参照 敵を1体足す = データに1行。仕組み側(右)は一切触らない。 だから戦闘にも図鑑にも、自動で出る。

この分離が効くのは、「足す」ときに触る場所が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"];

能力値は basegrowth の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";
});
Archipelago Saga のパーティ編成(9職業)画面
職業選択画面。9職ぶんのステータス・スキル・立ち絵は、すべて 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"] と書けば、戦闘エンジンが炎属性のダメージを自動で増やす。actionsweight は「行動の出やすさ」。スライムなら「たいあたり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 にするだけで、その敵が世界に出現する。データとアセットの進捗を切り離せる、実戦的な工夫だ。

図2:本体を触らず、後から追記して合流する enemies.js(本体) G.ENEMIES を作る enemies_ext.js(追加分) 同じ G.ENEMIES に追記 G.ENEMIES(全敵) 戦闘・図鑑がここを読む アート未生成の敵は PENDING で保留 → 画像404を防ぐ

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: truestory: "boss1"(戦闘前の会話)、reward: "chest" といったフラグを足すだけで挙動が変わるのも、データ駆動ならでは。マップ画面のコードは、この配列を上から並べて描くだけでいい。

Step 7. 手を動かす — 敵を1体、追加してみる

設計の答え合わせをしよう。新しい敵「マッドクラブ」を追加して、序盤のステージに出現させる。触るのはデータだけだ。

① 敵データを足す(enemies_ext.jsEXT に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.jsworldmap.jsbestiary.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.jsdef を2下げて再読み込みするだけ。ビルドも再起動も無いので、数値を変えて→ブラウザ更新→1戦してみる、の1周が数十秒で回る。この回転の速さが、データ駆動のもう1つの恩恵になる。調整した数値は「なぜその値にしたか」をコメントで残しておくと、後から自分が助かる。数字の理由は、1週間後の自分にとっては他人の決定と同じくらい謎になるからだ。

まとめ:第2回の要点

  • 純データjs/data/G にオブジェクトを置くだけ。ロジックは持たない。
  • 能力は base+growth:全レベルのテーブルでなく、初期値と伸び率だけ持つ。
  • 効果は早見表に集約:技は「poisonを付ける」と宣言し、実体は G.STATUSES が持つ。
  • AIもデータactionsweight(重み付き抽選)で敵の行動が決まる。
  • 拡張ファイル方式:追加分は別ファイルで後から合流。本体は不変。未完成アートは保留できる。
  • ステージは参照の配列:敵を 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回:無ビルドの土台連載ハブ▶ 遊ぶほかのアプリの作り方

JavaScriptでブラウザゲームを作る①|ビルド不要(file://)で動くシーン制の土台と全体設計【Archipelago Saga制作】JavaScriptでブラウザゲームを作る①|ビルド不要(file://)で動くシーン制の土台と全体設計【Archipelago Saga制作】前のページ

JavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipelago Saga制作】次のページJavaScriptでRPGを作る③|CTB戦闘システムをDOM非依存で実装する【Archipelago Saga制作】

ピックアップ記事

  1. Claude Mission Control の作り方 — Tauri+Pyth…

  2. New Eden Intelligence Hub の作り方 — EVE Onl…

  3. 競艇予想AIの作り方 — LightGBMで「当たる順位」を学習させる実装ガイド…

  4. 高精度OCRデスクトップアプリの作り方 — PaddleOCR-VLとPyIns…

関連記事

  1. 日本語の文字起こし、Whisperより良いモデルはあるのか。測り方を3回間違えながら比べた記録
  2. 正解が分かる音声を自分で作って、日本語ASRを測る(そして、その限界)

    アプリの作り方

    正解が分かる音声を自分で作って、日本語ASRを測る(そして、その限界)

    文字起こしの精度を測ろうとして、正解テキストが無いことに詰まる。音声合…

  3. large-v3-turbo は large-v3 の代わりになるか。日本語で測って、速度の測り方を2回間違えた記録
  4. 背景除去アプリを作る④|BiRefNetをONNXでローカル推論する — モデル別前処理とDirectML→CPUフォールバック【背景除去Studio制作】
  5. ローカルAIで背景除去アプリを作る①|PyQt6と3枚の純関数レイヤで「壊れない土台」を作る【背景除去Studio制作】
  6. YouTubeチャンネル分析ツールの作り方 — Data APIで統計・投稿パターン・キーワードを可視化する

    アプリの作り方

    YouTubeチャンネル分析ツールの作り方 — Data APIで統計・投稿パターン・キーワードを可…

    キーワードでチャンネルを検索し統計・人気動画・投稿パターン・頻出ワード…

注目

AIで、ここまで作れる

AIで作った2D RPGを、ブラウザでそのまま遊べます。その「作り方=最後まで完成させる進め方」も実例つきで公開中。

▶ ゲームを遊ぶやり方を読む

PR

ロリポップ!レンタルサーバー(PR)

レンタルサーバ:ロリポップ!(本サイトの稼働環境・PR)

  1. JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“データ追記だけ”で増やす【Archipelago Saga制作】

    アプリの作り方

    JavaScriptでRPGを作る②|データ駆動設計 — 敵・職業・スキルを“デ…
  2. Meta「Muse Image」がアプリ内に — @メンション撤回とAPI無しをどう見るか

    AI・テック動向

    Meta「Muse Image」がアプリ内に — @メンション撤回とAPI無しを…
  3. xAI が Grok の新画像モデル「Aurora」を展開 — 自己回帰型で既存パイプラインの何を検証すべきか

    AI・テック動向

    xAI が Grok の新画像モデル「Aurora」を展開 — 自己回帰型で既存…
  4. マイク録音テストを数値化するアプリの作り方 — ノイズフロア測定の落とし穴と、どのオーディオインターフェースでも使える手順

    アプリの作り方

    マイク録音テストを数値化するアプリの作り方 — ノイズフロア測定の落とし穴と、ど…
  5. AI・テック動向

    2026年8月に並ぶ5つの廃止期限 — grep 2本で自分のリポジトリを監査す…
PAGE TOP

TAG CLOUD

ドラッグで回転・クリックでそのタグの記事一覧へ