
STEP09:味方魔法の実装(攻撃・回復・強化・防御の仕組み)
STEP09では、味方キャラクターが使う 魔法システム を実装します。
このゲームでは「魔法」をただの演出にせず、ちゃんと 攻撃・回復・強化・防御 をゲーム進行の軸にします。
このSTEPで作るものは大きく2つです。
- 各職業が持つ魔法(4つ)を定義する(勇者・戦士・魔法使い)
- cast_spell 関数で魔法効果を実行する(MP消費、ダメージ、回復、バフ、防御)
味方魔法の設計(データ構造)
味方魔法は common.h にある次の型で表現します。
SpellType(魔法の種類)
| 種類 | 意味 | 代表例 |
|---|---|---|
| SPELL_DMG | ダメージ魔法 | ファイア、スマイト |
| SPELL_HEAL | 回復魔法 | ヒール、リカバー |
| SPELL_BUFF | 強化・防御系 | ウォークライ、ガードオーラ |
Spell(魔法定義)
| フィールド | 型 | 意味 |
|---|---|---|
| name | char配列 | 表示名 |
| type | SpellType | ダメージ/回復/強化 |
| mp_cost | int | 消費MP |
| power | int | 効果量(倍率や回復量など) |
職業ごとの魔法セット(init_spells)
味方は1人につき4つの魔法を持ちます(SPELLS_PER_CHAR が 4)。
職業によって中身が変わります。
characters.c の魔法初期化はこの部分です。
static void init_spells(Character* c) {
if (c->job == HERO) {
c->spells[0] = (Spell){ "ヒール", SPELL_HEAL, 4, 25 };
c->spells[1] = (Spell){ "ホーリーブレード", SPELL_DMG, 5, 2 };
c->spells[2] = (Spell){ "ガードオーラ", SPELL_BUFF, 4, 1 };
c->spells[3] = (Spell){ "スマイト", SPELL_DMG, 7, 3 };
}
else if (c->job == WARRIOR) {
c->spells[0] = (Spell){ "パワーストライク", SPELL_DMG, 4, 2 };
c->spells[1] = (Spell){ "ウォークライ", SPELL_BUFF, 4, 2 };
c->spells[2] = (Spell){ "アイアンウォール", SPELL_BUFF, 5, 2 };
c->spells[3] = (Spell){ "スラッシュウェブ", SPELL_DMG, 7, 3 };
}
else {
c->spells[0] = (Spell){ "ファイア", SPELL_DMG, 4, 2 };
c->spells[1] = (Spell){ "アイス", SPELL_DMG, 4, 2 };
c->spells[2] = (Spell){ "サンダー", SPELL_DMG, 6, 3 };
c->spells[3] = (Spell){ "リカバー", SPELL_HEAL, 5, 35 };
}
}魔法一覧
| 職業 | 魔法名 | 種類 | 消費MP | power | 効果の意味 |
|---|---|---|---|---|---|
| 勇者 | ヒール | 回復 | 4 | 25 | HPを25回復 |
| 勇者 | ホーリーブレード | 攻撃 | 5 | 2 | MAG×2 ダメージ |
| 勇者 | ガードオーラ | 強化 | 4 | 1 | defending を 1 にして防御状態 |
| 勇者 | スマイト | 攻撃 | 7 | 3 | MAG×3 ダメージ |
| 戦士 | パワーストライク | 攻撃 | 4 | 2 | MAG×2 ダメージ(戦士でも魔法扱い) |
| 戦士 | ウォークライ | 強化 | 4 | 2 | base_atk に +2(恒久強化) |
| 戦士 | アイアンウォール | 強化 | 5 | 2 | defending を 1 にして防御状態 |
| 戦士 | スラッシュウェブ | 攻撃 | 7 | 3 | MAG×3 ダメージ |
| 魔法使い | ファイア | 攻撃 | 4 | 2 | MAG×2 ダメージ |
| 魔法使い | アイス | 攻撃 | 4 | 2 | MAG×2 ダメージ |
| 魔法使い | サンダー | 攻撃 | 6 | 3 | MAG×3 ダメージ |
| 魔法使い | リカバー | 回復 | 5 | 35 | HPを35回復 |
※このゲームでは「攻撃魔法の威力」が キャラの魔力 char_mag × power で決まるので、魔法使いは当然強いです。勇者もそこそこ戦える、戦士は殴りが本命…という役割分担になります。
魔法実行の心臓部:cast_spell
魔法を実際に発動するのが characters.c の cast_spell です。
battle.c から呼ばれて、MP消費・対象選択・ダメージ/回復/強化を全部ここでやります。
int cast_spell(Character* caster, Character party[PARTY_SIZE], Enemy* enemy, int spell_index) {
if (spell_index < 0 || spell_index >= SPELLS_PER_CHAR) return 0;
Spell sp = caster->spells[spell_index];
if (caster->mp < sp.mp_cost) {
printf(RED "MPが足りない!\n" RESET);
sfx_beep(4);
return 0;
}
caster->mp -= sp.mp_cost;
if (sp.type == SPELL_DMG) {
int dmg = char_mag(caster) * sp.power;
if (enemy->defending) {
dmg /= 2;
enemy->defending = 0;
}
enemy->hp -= dmg;
if (enemy->hp < 0) enemy->hp = 0;
sfx_beep(1);
printf(YELLOW "%s は %s! %d ダメージ!\n" RESET, caster->name, sp.name, dmg);
return 1;
}
else if (sp.type == SPELL_HEAL) {
printf("回復対象を選んでください (1-%d): ", PARTY_SIZE);
int t = 0;
if (!read_int_safely(&t)) return 0;
if (t < 1 || t > PARTY_SIZE) return 0;
Character* target = &party[t - 1];
if (!target->alive) {
printf(RED "その仲間は倒れている!\n" RESET);
return 0;
}
target->hp += sp.power;
if (target->hp > target->max_hp) target->hp = target->max_hp;
sfx_beep(2);
printf(GREEN "%s は %s! %s のHPが回復した!(+%d)\n" RESET,
caster->name, sp.name, target->name, sp.power);
return 1;
}
else {
sfx_beep(0);
if (strcmp(sp.name, "ウォークライ") == 0) {
caster->base_atk += sp.power;
printf(YELLOW "%s は %s! ATKが上がった!(+%d)\n" RESET, caster->name, sp.name, sp.power);
}
else {
caster->defending = 1;
printf(YELLOW "%s は %s! 次の被ダメージを軽減!\n" RESET, caster->name, sp.name);
}
return 1;
}
}cast_spell の処理手順(流れを整理)
➀ 魔法番号の範囲チェック
| チェック | 意味 |
|---|---|
| spell_index < 0 | マイナス指定は不正 |
| spell_index >= SPELLS_PER_CHAR | 4以上は存在しない |
不正なら return 0(失敗)で battle 側に戻します。
➁ MPチェック → MP消費
| 処理 | 目的 |
|---|---|
| caster->mp < sp.mp_cost | MP不足なら発動不可 |
| caster->mp -= sp.mp_cost | 発動したらMPを減らす |
ここを最初に固めると、魔法の種類が増えても「MP管理が崩れない」のが強いです。
攻撃魔法(SPELL_DMG)の仕組み
ダメージ計算
int dmg = char_mag(caster) * sp.power;- char_mag はキャラの魔力(杖補正込み)を返す関数
- power は倍率(2倍、3倍など)
敵が防御中なら半減
if (enemy->defending) {
dmg /= 2;
enemy->defending = 0;
}ここがポイントで、防御は1回限りです。
半減が適用されたら defending を 0 に戻すので、次の攻撃では通常ダメージになります。
敵HPを減らし、0未満にならないよう丸める
enemy->hp -= dmg;
if (enemy->hp < 0) enemy->hp = 0;RPGでは定番の「下限0丸め」です。HPがマイナスに落ちると表示や判定が崩れやすいので、ここで止めます。
回復魔法(SPELL_HEAL)の仕組み
回復は「誰を回復するか」を選ぶ必要があるので、入力が入ります。
対象選択(安全入力)
printf("回復対象を選んでください (1-%d): ", PARTY_SIZE);
int t = 0;
if (!read_int_safely(&t)) return 0;
if (t < 1 || t > PARTY_SIZE) return 0;- read_int_safely は STEP03 で作った「安全に数値を読む」関数
- 範囲外なら失敗扱いで return 0
倒れている仲間は回復できない
if (!target->alive) {
printf(RED "その仲間は倒れている!\n" RESET);
return 0;
}ここはゲーム設計として大事で、
「戦闘不能の復活」は教会や特別手段に任せる、という役割分担になります。
回復と上限丸め
target->hp += sp.power;
if (target->hp > target->max_hp) target->hp = target->max_hp;上限を超えないように max_hp で止めるのも、表示崩れ防止の基本です。
強化・防御(SPELL_BUFF)の仕組み
このゲームの SPELL_BUFF は2系統に分かれます。
- ウォークライ:攻撃力アップ(base_atkを増やす)
- それ以外:防御状態(defending = 1)
分岐が strcmp になっている理由
if (strcmp(sp.name, "ウォークライ") == 0) {
caster->base_atk += sp.power;
} else {
caster->defending = 1;
}今の設計では、「BUFFの中でも特殊なウォークライだけ挙動が違う」ので、名前で判定しています。
ここで覚えておくと良い仕様ポイント
| 魔法 | 効果 | 持続 |
|---|---|---|
| ウォークライ | base_atk が増える | 基本的にずっと(戻す処理がない限り恒久) |
| ガードオーラ/アイアンウォール | defending=1 | 次に受ける攻撃で半減し、その後解除 |
ウォークライが恒久強化なのは、わりと気持ちいい反面「積み上げが強すぎる」可能性もあります。
もしバランス調整したければ、後で「戦闘終了で元に戻す」「一定ターンで解除」などに拡張できます。
battle.c からの呼び出し(魔法選択→cast_spell)
battle.c 側では「魔法コマンド」を選ぶと、一覧を表示して番号を入力させ、cast_spell を呼びます。
printf("魔法を選択してください\n");
for (int s = 0; s < SPELLS_PER_CHAR; s++) {
printf(" %d:%s (MP%d)\n", s + 1, c->spells[s].name, c->spells[s].mp_cost);
}
printf("選択: ");
int si = 0;
if (!read_int_safely(&si)) si = 0;
si--;
if (!cast_spell(c, gs->party, &enemy, si)) {
printf("行動失敗。通常攻撃に切り替えます。\n");
/* 通常攻撃へフォールバック */
}失敗時に通常攻撃へ切り替える理由
入力ミスやMP不足で「何も起きない」とテンポが悪いので、
このゲームでは「魔法が失敗したら通常攻撃に落とす」設計にしています。
プレイヤー体験としてかなりスムーズです。
defending(防御状態)の扱いを整理
このSTEPの魔法は defending を操作します。
defending は「次の被ダメージを半減するフラグ」です。
| 対象 | フラグ | 半減処理が入る場所 |
|---|---|---|
| 敵 | enemy.defending | cast_spell のダメージ魔法、通常攻撃のダメージ計算 |
| 味方 | party[i].defending | 敵の攻撃、敵の魔法ダメージ |
つまり「防御」は戦闘ロジック全体に関わるので、
ここをフラグ化しておく設計はかなり良い感じです。
STEP09で登場した主な関数・命令の意味
| 名前 | 種別 | 何をする? |
|---|---|---|
| strcmp | 関数 | 文字列が同じか比較する |
| read_int_safely | 関数 | 数字入力を安全に読み取る(不正入力に強い) |
| char_mag | 関数 | 魔力(杖補正込み)を返す |
| sfx_beep | 関数 | 効果音(決定・攻撃・回復など) |
| if / else if / else | 命令 | 魔法タイプによって処理を分岐する |
| return 0 / return 1 | 戻り値 | 失敗/成功を battle 側へ返す |
STEP09のまとめ
- 魔法は Spell 構造体(名前・種類・消費MP・power)で統一管理
- init_spells で職業ごとの魔法4つをセット
- cast_spell が MP管理・対象選択・効果適用の中心
- 回復は上限丸め、攻撃は防御半減、強化は base_atk と defending の2系統
- battle.c からは「番号を選んで cast_spell を呼ぶ」だけにできてスッキリ
次はいよいよ STEP10:enemies層(敵テンプレート・ランダム生成・敵専用魔法) に入ります。
味方魔法ができたので、次は「敵側も魔法を使う」「敵の種類が増える」ことでバトルが一気にRPGっぽくなっていきます。
