STEP11:battle層の実装(ターン制バトル・行動選択・勝利処理)

STEP11では、いよいよゲームの核になる ターン制バトル を完成させます。
このSTEPのゴールは次の3つです。

  • 味方→敵の順で進む「ターン制」
  • 味方の「行動選択」(攻撃 / 魔法 / アイテム / 防御 / 逃走)
  • 勝利時の「報酬処理」(EXP・Gold、レベルアップ反映)

以降のSTEPでボス戦や演出を増やしても、battle層が安定していれば拡張がラクになります。

battle層の責務(設計を最初に固める)

battle層は「戦闘の進行役」です。
ダメージの“最終適用”や勝敗判定、報酬配布をここに集約します。

項目担当
ターン進行(味方→敵)battle
味方の行動選択UI(メニュー)battle(表示はui層に依頼)
ダメージ計算・適用battle
defending(防御)状態の扱いbattle
敵の行動(通常/魔法の選択)enemies(判断)+ battle(適用)
勝利処理(EXP/GOLD)battle
レベルアップ反映characters(関数呼び出し)

battle層が公開するAPI(battle.h)

ここでは「敵1体 vs 味方パーティ」の形式で作ります(まずはシンプルが正義)。

#ifndef BATTLE_H
#define BATTLE_H

#include "common.h"

typedef enum {
    BATTLE_WIN = 0,
    BATTLE_LOSE = 1,
    BATTLE_RUN = 2
} BattleResult;

/* enemy は enemies_spawn() で作ったものを渡す想定 */
BattleResult battle_run(Character party[PARTY_SIZE], Enemy* enemy, Inventory* inv);

#endif

ポイント

  • battle_run は「戦闘を開始して結果を返す」だけ
  • マップ側は「敵に接触したら battle_run を呼ぶ」だけでOK

戦闘の流れ(状態遷移を頭に入れる)

戦闘ループはこの形が一番バグりにくいです。

フェーズ内容
1画面描画(敵・味方ステータス)
2味方ターン(生存メンバー順に行動)
3勝敗判定(敵HP0なら勝利)
4敵ターン(通常攻撃 or 敵魔法)
5勝敗判定(味方全滅なら敗北)
6次ターンへ

battle.c 実装(中核)

便利関数(生存判定・乱数・ダメージ下限)

#include "battle.h"
#include "ui.h"
#include "input.h"
#include "platform.h"
#include "enemies.h"
#include "items.h"
#include "characters.h"

#include <stdio.h>
#include <stdlib.h>

static int party_alive_count(Character party[PARTY_SIZE]) {
    int n = 0;
    for (int i = 0; i < PARTY_SIZE; i++) {
        if (party[i].alive) n++;
    }
    return n;
}

static int clamp_min(int v, int minv) {
    return (v < minv) ? minv : v;
}

/* ざっくりしたゆらぎ:80%〜120% */
static int apply_variance(int base) {
    int r = 80 + (rand() % 41);
    return base * r / 100;
}

物理攻撃(味方→敵 / 敵→味方)

防御(defending)があるときは半減して、当たったら解除する運用にすると分かりやすいです。

static void apply_damage_to_enemy(Enemy* e, int dmg) {
    dmg = clamp_min(dmg, 1);

    if (e->defending) {
        dmg /= 2;
        e->defending = 0;
    }

    e->hp -= dmg;
    if (e->hp < 0) e->hp = 0;

    printf("%s takes %d damage!\n", e->name, dmg);
    sfx_beep(1);

    if (e->hp == 0) {
        e->alive = 0;
    }
}

static void apply_damage_to_char(Character* c, int dmg) {
    dmg = clamp_min(dmg, 1);

    if (c->defending) {
        dmg /= 2;
        c->defending = 0;
    }

    c->hp -= dmg;
    if (c->hp < 0) c->hp = 0;

    printf("%s takes %d damage!\n", c->name, dmg);
    sfx_beep(1);

    if (c->hp == 0) {
        c->alive = 0;
    }
}

static void party_attack_enemy(Character* actor, Enemy* enemy) {
    int base = actor->atk - enemy->base_def;
    base = clamp_min(base, 1);
    int dmg = apply_variance(base);

    printf("%s attacks!\n", actor->name);
    apply_damage_to_enemy(enemy, dmg);
}

static int pick_random_alive_index(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];
}

static void enemy_attack_party(Enemy* enemy, Character party[PARTY_SIZE]) {
    int t = pick_random_alive_index(party);
    if (t < 0) return;

    int base = enemy->base_atk - party[t].def;
    base = clamp_min(base, 1);
    int dmg = apply_variance(base);

    printf("%s attacks!\n", enemy->name);
    apply_damage_to_char(&party[t], dmg);
}

味方魔法(STEP09の仕組みを battle に接続)

ここはプロジェクト側の Spell 定義に合わせて調整してください。
「魔法は battle が最終適用する」のがポイントです。

static int cast_spell_by_party(Character* actor, Enemy* enemy, Character party[PARTY_SIZE], int spell_index) {
    Spell sp = actor->spells[spell_index];

    if (sp.mp_cost <= 0) {
        printf("No spell.\n");
        return 0;
    }
    if (actor->mp < sp.mp_cost) {
        printf("Not enough MP!\n");
        sfx_beep(4);
        return 0;
    }

    actor->mp -= sp.mp_cost;

    if (sp.type == SPELL_DMG) {
        int base = actor->mag * sp.power - enemy->base_def;
        base = clamp_min(base, 1);
        int dmg = apply_variance(base);

        printf("%s casts %s!\n", actor->name, sp.name);
        apply_damage_to_enemy(enemy, dmg);
        return 1;
    }

    if (sp.type == SPELL_HEAL) {
        /* 回復対象:生存メンバーから選ぶ(簡易版) */
        int idx = ui_select_party_member(party); /* ui側に選択UIが無いなら input で代用してOK */
        if (idx < 0 || idx >= PARTY_SIZE || !party[idx].alive) idx = 0;

        int before = party[idx].hp;
        party[idx].hp += sp.power;
        if (party[idx].hp > party[idx].max_hp) party[idx].hp = party[idx].max_hp;

        printf("%s casts %s on %s! HP +%d\n", actor->name, sp.name, party[idx].name, party[idx].hp - before);
        sfx_beep(2);
        return 1;
    }

    /* BUFF:とりあえず actor を defending 状態にする */
    actor->defending = 1;
    printf("%s casts %s! Defending!\n", actor->name, sp.name);
    sfx_beep(0);
    return 1;
}

補足

  • ui_select_party_member() がまだ無いなら、STEP04の input/UI を使って簡易に選ばせる関数を1つ追加すると良いです
  • 「強化魔法の種類別処理」は後続STEPで拡張しやすいよう、battle でまとめて扱うのがおすすめ

アイテム使用(消費アイテムだけ対応)

ここも items層の設計に依存します。
最低限「回復ポーション」だけでも戦闘が成立します。

static int use_item_in_battle(Character* actor, Character party[PARTY_SIZE], Inventory* inv) {
    int item_id = items_select_consumable(inv); /* ない場合は input で番号選択 */
    if (item_id < 0) return 0;

    Item it = items_get(item_id);

    if (!items_can_use_in_battle(&it)) {
        printf("You can't use that now.\n");
        sfx_beep(4);
        return 0;
    }

    int target = ui_select_party_member(party);
    if (target < 0 || target >= PARTY_SIZE || !party[target].alive) target = 0;

    if (!items_consume(inv, item_id, 1)) {
        printf("Out of stock.\n");
        sfx_beep(4);
        return 0;
    }

    /* 例:回復 */
    if (it.type == ITEM_HEAL) {
        int before = party[target].hp;
        party[target].hp += it.power;
        if (party[target].hp > party[target].max_hp) party[target].hp = party[target].max_hp;

        printf("%s uses %s on %s! HP +%d\n", actor->name, it.name, party[target].name, party[target].hp - before);
        sfx_beep(2);
        return 1;
    }

    /* 拡張用 */
    printf("%s uses %s.\n", actor->name, it.name);
    sfx_beep(0);
    return 1;
}

行動選択(味方ターンの主役)

味方1人ごとに「行動メニュー」を出します。

選択内容
1Attack
2Magic
3Item
4Defend
5Run
static int select_spell_index(Character* actor) {
    /* 表示は ui層に任せるのが理想。ここは簡易に番号入力。 */
    printf("Select spell:\n");
    for (int i = 0; i < 4; i++) {
        if (actor->spells[i].mp_cost <= 0) continue;
        printf("%d) %s (MP %d)\n", i + 1, actor->spells[i].name, actor->spells[i].mp_cost);
    }
    int v = input_read_int_range(1, 4);
    return v - 1;
}

static int try_run_away(Character party[PARTY_SIZE], Enemy* enemy) {
    /* 基本40%、生存人数が少ないほどちょい上げ */
    int alive = party_alive_count(party);
    int rate = 40 + (PARTY_SIZE - alive) * 10;
    if (rate > 80) rate = 80;

    int r = rand() % 100;
    if (r < rate) {
        printf("You successfully ran away!\n");
        sfx_beep(0);
        return 1;
    }

    printf("Couldn't escape!\n");
    sfx_beep(4);
    return 0;
}

static int party_take_action(Character party[PARTY_SIZE], int actor_idx, Enemy* enemy, Inventory* inv) {
    Character* actor = &party[actor_idx];

    if (!actor->alive) return 0;

    printf("\n%s's turn\n", actor->name);

    printf("1) Attack  2) Magic  3) Item  4) Defend  5) Run\n");
    int cmd = input_read_int_range(1, 5);

    if (cmd == 1) {
        party_attack_enemy(actor, enemy);
        return 0;
    }

    if (cmd == 2) {
        int si = select_spell_index(actor);
        cast_spell_by_party(actor, enemy, party, si);
        return 0;
    }

    if (cmd == 3) {
        use_item_in_battle(actor, party, inv);
        return 0;
    }

    if (cmd == 4) {
        actor->defending = 1;
        printf("%s is defending!\n", actor->name);
        sfx_beep(0);
        return 0;
    }

    if (cmd == 5) {
        if (try_run_away(party, enemy)) return 1; /* 逃走成功=戦闘終了 */
        return 0;
    }

    return 0;
}

勝利処理(EXP・Gold・レベルアップ)

勝利時にやることは意外と少ないです。

  1. 敵の exp/gold を取得
  2. 生存メンバーへ exp を配る。
  3. 所持金に gold を加算
  4. レベルアップ処理(characters層に任せる)
static void on_victory(Character party[PARTY_SIZE], Enemy* enemy) {
    printf("\nVictory! You defeated %s!\n", enemy->name);
    sfx_beep(3);

    int alive = party_alive_count(party);
    if (alive <= 0) alive = 1;

    int exp_each = enemy->exp / alive;

    printf("EXP %d, GOLD %d\n", enemy->exp, enemy->gold);

    for (int i = 0; i < PARTY_SIZE; i++) {
        if (!party[i].alive) continue;

        characters_add_gold(&party[i], enemy->gold / alive);  /* パーティ財布なら別関数に */
        characters_gain_exp(&party[i], exp_each);            /* レベルアップはこの中で処理 */
    }
}

設計メモ

  • お金を「パーティ共通」にする場合は GameState.gold に加算する形が自然です。
  • ここでは例として、各キャラに配る関数を書いています(あなたの設計に合わせて置き換えてOK)

battle_run 本体(完成形)

BattleResult battle_run(Character party[PARTY_SIZE], Enemy* enemy, Inventory* inv) {
    /* 戦闘開始時の状態整理 */
    for (int i = 0; i < PARTY_SIZE; i++) {
        party[i].defending = 0;
    }
    enemy->defending = 0;
    enemy->alive = 1;

    printf("\nA wild %s appears!\n", enemy->name);
    sfx_beep(0);

    int turn = 1;

    while (1) {
        printf("\nTurn %d\n", turn);

        ui_show_battle_status(party, enemy); /* STEP04で作ったUI表示関数を想定 */

        /* 味方ターン */
        for (int i = 0; i < PARTY_SIZE; i++) {
            if (!party[i].alive) continue;

            /* 敵が死んでたら即終了 */
            if (!enemy->alive || enemy->hp == 0) {
                on_victory(party, enemy);
                return BATTLE_WIN;
            }

            int escaped = party_take_action(party, i, enemy, inv);
            if (escaped) return BATTLE_RUN;
        }

        /* 勝利判定 */
        if (!enemy->alive || enemy->hp == 0) {
            on_victory(party, enemy);
            return BATTLE_WIN;
        }

        /* 敵ターン */
        printf("\nEnemy turn\n");

        if (enemies_will_cast_spell(enemy)) {
            if (!enemies_cast_spell(enemy, party)) {
                enemy_attack_party(enemy, party);
            }
        } else {
            enemy_attack_party(enemy, party);
        }

        /* 敗北判定 */
        if (party_alive_count(party) == 0) {
            printf("\nDefeated...\n");
            sfx_beep(4);
            return BATTLE_LOSE;
        }

        turn++;
    }
}

動作確認チェック(ここまでできたら強い)

チェック期待
ターン順味方全員→敵 の順で進む。
防御defending中は半減し、被弾で解除される。
魔法MPMP不足で不発になり、行動が潰れても落ちない。
アイテム在庫不足や使用不可で落ちない。
逃走成功したら即終了、失敗したら敵ターンへ。
勝利処理EXP/GOLDが加算され、レベルアップが反映される。
敗北味方全滅で BATTLE_LOSE を返す。

演習問題+模範解答例

演習:勝利時に「ランダムドロップ」を追加する

問題
勝利時に 25% の確率で Potion を1つ入手する処理を追加してください(invに追加)。

模範解答例(on_victory の最後に追加)

if ((rand() % 100) < 25) {
    int potion_id = ITEM_POTION; /* あなたのID定義に合わせる */
    items_add(inv, potion_id, 1);
    printf("Dropped: Potion x1\n");
    sfx_beep(0);
}

STEP11のまとめ

  • battle層は「戦闘進行・最終適用・勝利処理」を一手に引き受ける。
  • 味方の行動選択を battle に置くと全体の見通しが良くなる。
  • 敵の判断(魔法を使うか)は enemies、適用は battle に分けるとキレイ
  • 勝利処理は短くまとめて、レベルアップ詳細は characters 層へ委譲する。