
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人ごとに「行動メニュー」を出します。
| 選択 | 内容 |
|---|---|
| 1 | Attack |
| 2 | Magic |
| 3 | Item |
| 4 | Defend |
| 5 | Run |
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・レベルアップ)
勝利時にやることは意外と少ないです。
- 敵の exp/gold を取得
- 生存メンバーへ exp を配る。
- 所持金に gold を加算
- レベルアップ処理(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中は半減し、被弾で解除される。 |
| 魔法MP | MP不足で不発になり、行動が潰れても落ちない。 |
| アイテム | 在庫不足や使用不可で落ちない。 |
| 逃走 | 成功したら即終了、失敗したら敵ターンへ。 |
| 勝利処理 | 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 層へ委譲する。
