STEP10:enemies層の実装(敵テンプレート・ランダム生成・敵専用魔法)

STEP10では、RPGの戦闘を「毎回同じ敵」から卒業させるために、敵テンプレート(図鑑)を用意し、そこからランダム生成で敵を作り、さらに敵が使う敵専用魔法を実装します。
この層がしっかりすると、マップで敵(E)に当たったときに「エリアに合った敵が出る」「敵がたまに魔法を使う」ようになって、戦闘が一気にゲームらしくなります。

enemies層の役割(このSTEPで決める設計)

enemies層は「敵に関するデータと生成」を担当します。
battle層は「戦闘進行(ターン、勝敗、報酬)」を担当します。

項目enemies層battle層
敵の種類データ(図鑑)
エリアに応じた敵生成
敵の初期化(HP/MP/ステータス)
敵の魔法データ
敵の行動決定(通常攻撃 or 魔法)
(今回はここまで)
ダメージ計算・適用(最終処理)

※「敵AIの意思決定」まで enemies でやると battle が読みやすくなります(おすすめ)。

追加するデータ型と定数

EnemyTemplate(敵テンプレート=敵図鑑)

テンプレートは「生成元」なので Enemy の材料です。

フィールド意味
name敵名
min_level / max_level出現レベル帯
max_hp / max_mp最大HP/MP
atk / def / mag攻撃・防御・魔力
exp / gold報酬
spells[]敵専用魔法

既存データ型の想定

STEP09で Spell を作っている前提で、敵魔法も同じ Spell を使います。

  • Spell は name / type / mp_cost / power を持つ
  • SpellType は SPELL_DMG / SPELL_HEAL / SPELL_BUFF を持つ
    (もし名前が違うなら、あなたのプロジェクト側に合わせて読み替えてOKです)

enemies.h を実装する

enemies.h は「敵生成」と「敵行動」の入口だけを公開します。

#ifndef ENEMIES_H
#define ENEMIES_H

#include "common.h"

/* 敵テンプレート(敵図鑑) */
typedef struct {
    const char* name;

    int min_level;
    int max_level;

    int max_hp;
    int max_mp;

    int atk;
    int def;
    int mag;

    int exp;
    int gold;

    Spell spells[4];
} EnemyTemplate;

/* エリアレベルに応じた敵を1体生成 */
Enemy enemies_spawn(int area_level);

/* 敵の行動:0=通常攻撃、1=魔法 */
int enemies_will_cast_spell(const Enemy* e);

/* 敵が魔法を使う(成功=1 / 不発=0) */
int enemies_cast_spell(Enemy* e, Character party[PARTY_SIZE]);

#endif

enemies.c:敵テンプレート(図鑑)を作る

ポイントは2つです。

  • 「出現レベル帯」で候補を絞れるようにする。
  • 魔法枠は固定数(ここでは4枠)で扱いを単純化する。
#include "enemies.h"
#include <stdlib.h>
#include <string.h>

#define ENEMY_DB_COUNT 6
#define ENEMY_SPELLS   4

static EnemyTemplate g_enemy_db[ENEMY_DB_COUNT] = {
    /* 1) スライム */
    {
        "Slime", 1, 3,
        35, 8,
        6, 2, 3,
        12, 8,
        {
            {"Sticky Shot", SPELL_DMG, 3, 2},
            {"Jelly Heal",  SPELL_HEAL, 4, 15},
            {"Brace",       SPELL_BUFF, 3, 1},
            {"-",           SPELL_DMG,  0, 0}
        }
    },

    /* 2) ゴブリン */
    {
        "Goblin", 2, 5,
        60, 10,
        10, 4, 4,
        22, 15,
        {
            {"Throw Knife", SPELL_DMG, 3, 2},
            {"Brace",      SPELL_BUFF, 3, 1},
            {"-",          SPELL_DMG,  0, 0},
            {"-",          SPELL_DMG,  0, 0}
        }
    },

    /* 3) コウモリ */
    {
        "Bat", 3, 6,
        55, 14,
        8, 3, 6,
        26, 18,
        {
            {"Sonic",      SPELL_DMG,  4, 2},
            {"Night Heal", SPELL_HEAL, 5, 18},
            {"-",          SPELL_DMG,  0, 0},
            {"-",          SPELL_DMG,  0, 0}
        }
    },

    /* 4) スケルトン */
    {
        "Skeleton", 5, 9,
        95, 16,
        14, 7, 6,
        40, 30,
        {
            {"Dark Slash", SPELL_DMG,  5, 2},
            {"Iron Guard", SPELL_BUFF, 4, 1},
            {"-",          SPELL_DMG,  0, 0},
            {"-",          SPELL_DMG,  0, 0}
        }
    },

    /* 5) ウォーロック */
    {
        "Warlock", 7, 12,
        110, 30,
        10, 6, 12,
        60, 45,
        {
            {"Shadow Bolt", SPELL_DMG,  6, 3},
            {"Recover",     SPELL_HEAL, 6, 20},
            {"Barrier",     SPELL_BUFF, 5, 1},
            {"-",           SPELL_DMG,  0, 0}
        }
    },

    /* 6) ボス枠 */
    {
        "Master Dragon", 12, 99,
        260, 60,
        22, 12, 18,
        220, 180,
        {
            {"Breath",      SPELL_DMG,  10, 4},
            {"Regenerate",  SPELL_HEAL, 10, 40},
            {"Dragon Guard",SPELL_BUFF, 8, 1},
            {"-",           SPELL_DMG,  0, 0}
        }
    }
};

敵をランダム生成する(enemies_spawn)

候補抽出 → ランダム選択

  • area_level がレベル帯に入るテンプレートだけ候補にする。
  • 候補から1つ選ぶ。
  • Enemy を初期化して返す。
static int in_range(int v, int minv, int maxv) {
    return (v >= minv && v <= maxv);
}

Enemy enemies_spawn(int area_level) {
    int idxs[ENEMY_DB_COUNT];
    int n = 0;

    for (int i = 0; i < ENEMY_DB_COUNT; i++) {
        if (in_range(area_level, g_enemy_db[i].min_level, g_enemy_db[i].max_level)) {
            idxs[n++] = i;
        }
    }

    /* 候補が0なら最弱(安全策) */
    int pick = 0;
    if (n > 0) pick = idxs[rand() % n];

    EnemyTemplate* t = &g_enemy_db[pick];

    Enemy e;
    memset(&e, 0, sizeof(Enemy));

    strncpy(e.name, t->name, sizeof(e.name) - 1);

    e.max_hp = t->max_hp;
    e.hp     = t->max_hp;

    e.max_mp = t->max_mp;
    e.mp     = t->max_mp;

    e.base_atk = t->atk;
    e.base_def = t->def;
    e.base_mag = t->mag;

    e.exp  = t->exp;
    e.gold = t->gold;

    for (int s = 0; s < ENEMY_SPELLS; s++) {
        e.spells[s] = t->spells[s];
    }

    e.defending = 0;
    e.alive = 1;

    return e;
}

area_level をどう決める?

おすすめは次のどれかです。

方法area_level の決め方特徴
A主人公レベル実装が簡単
Bパーティ平均レベル安定して強さが追従
Cマップ深度/フロアダンジョン感が出る

STEP10では A or B が一番楽です。

敵専用魔法の発動(enemies_cast_spell)

敵魔法の特徴は「対象入力なし」でAIが決める点です。

対象選択ルール(シンプル版)

  • 攻撃魔法:生存している味方からランダム1人
  • 回復魔法:敵自身を回復
  • 防御魔法:敵自身に defending を付与
static int pick_random_alive(Character party[PARTY_SIZE]) {
    int list[PARTY_SIZE];
    int n = 0;
    for (int i = 0; i < PARTY_SIZE; i++) {
        if (party[i].alive) list[n++] = i;
    }
    if (n == 0) return -1;
    return list[rand() % n];
}

発動本体

ここでは、魔法効果を次のように統一します。

種類効果
SPELL_DMGbase_mag × power のダメージ
SPELL_HEALpower だけ回復(固定値)
SPELL_BUFFdefending=1 を付与
int enemies_cast_spell(Enemy* e, Character party[PARTY_SIZE]) {
    int usable[ENEMY_SPELLS];
    int n = 0;

    for (int s = 0; s < ENEMY_SPELLS; s++) {
        Spell sp = e->spells[s];
        if (sp.mp_cost <= 0) continue;     /* "-"枠 */
        if (e->mp < sp.mp_cost) continue;
        usable[n++] = s;
    }
    if (n == 0) return 0;

    /* HPが減ってると回復を優先しやすくする */
    int hp_pct = e->hp * 100 / e->max_hp;
    int chosen = usable[rand() % n];

    if (hp_pct <= 30) {
        for (int i = 0; i < n; i++) {
            Spell sp = e->spells[usable[i]];
            if (sp.type == SPELL_HEAL) { chosen = usable[i]; break; }
        }
    }

    Spell sp = e->spells[chosen];
    e->mp -= sp.mp_cost;

    if (sp.type == SPELL_DMG) {
        int t = pick_random_alive(party);
        if (t < 0) return 0;

        int dmg = e->base_mag * sp.power;

        if (party[t].defending) {
            dmg /= 2;
            party[t].defending = 0;
        }

        party[t].hp -= dmg;
        if (party[t].hp < 0) party[t].hp = 0;

        printf("%s casts %s! %s takes %d damage!\n", e->name, sp.name, party[t].name, dmg);
        sfx_beep(1);

        if (party[t].hp == 0) party[t].alive = 0;
        return 1;
    }

    if (sp.type == SPELL_HEAL) {
        int before = e->hp;
        e->hp += sp.power;
        if (e->hp > e->max_hp) e->hp = e->max_hp;

        printf("%s casts %s! HP +%d!\n", e->name, sp.name, (e->hp - before));
        sfx_beep(2);
        return 1;
    }

    /* SPELL_BUFF */
    e->defending = 1;
    printf("%s casts %s! Defending!\n", e->name, sp.name);
    sfx_beep(0);
    return 1;
}

敵が魔法を使うか決める(enemies_will_cast_spell)

「魔法使う頻度」を決める関数です。
ここがあるだけで、battle側が読みやすくなります。

int enemies_will_cast_spell(const Enemy* e) {
    if (e->mp <= 0) return 0;

    int hp_pct = e->hp * 100 / e->max_hp;
    int r = rand() % 100;

    if (hp_pct <= 30) return (r < 70); /* ピンチなら魔法多め */
    return (r < 35);                   /* 平常時はほどほど */
}

battle側での使い方(敵ターンの最小変更)

敵ターンで「魔法 or 通常攻撃」を切り替えるだけにします。

if (enemies_will_cast_spell(&enemy)) {
    if (!enemies_cast_spell(&enemy, gs->party)) {
        enemy_attack(&enemy, gs->party);
    }
} else {
    enemy_attack(&enemy, gs->party);
}

「魔法が不発でも殴る」にしておくとテンポがいいです。

動作確認(ここだけは必ず見る)

確認期待する挙動
候補抽出area_level を変えると敵の種類が変わる。
初期化hp=max_hp, mp=max_mp で開始する。
魔法枠"-" を引いても落ちない。
MP不足MP不足なら魔法を使わず通常攻撃へ。
回復AIHPが30%以下で回復が出やすい。
defendingdefending の半減→解除が正しく動く。

演習問題+模範解答例

演習:敵を1種類追加する(Orc)

問題
敵テンプレートに Orc を追加してください。出現レベル帯は 6〜10。
HP 120 / MP 12 / ATK 16 / DEF 8 / MAG 5 / EXP 55 / GOLD 40。
魔法は以下:

  • Uppercut(SPELL_DMG, mp_cost=4, power=2)
  • Brace(SPELL_BUFF, mp_cost=3, power=1)

模範解答例(テンプレート追加)

{
    "Orc", 6, 10,
    120, 12,
    16, 8, 5,
    55, 40,
    {
        {"Uppercut", SPELL_DMG, 4, 2},
        {"Brace",    SPELL_BUFF, 3, 1},
        {"-",        SPELL_DMG,  0, 0},
        {"-",        SPELL_DMG,  0, 0}
    }
},

STEP10のまとめ

  • 敵はテンプレート(図鑑)で管理すると拡張が楽
  • area_level で候補を絞ってランダム生成するとゲームらしい出現になる。
  • 敵魔法は「対象入力なし」でAIが選ぶとテンポが良い。
  • enemies_will_cast_spell を用意すると battle.c がスッキリする。