STEP09:味方魔法の実装(攻撃・回復・強化・防御の仕組み)

STEP09では、味方キャラクターが使う 魔法システム を実装します。
このゲームでは「魔法」をただの演出にせず、ちゃんと 攻撃・回復・強化・防御 をゲーム進行の軸にします。

このSTEPで作るものは大きく2つです。

  • 各職業が持つ魔法(4つ)を定義する(勇者・戦士・魔法使い)
  • cast_spell 関数で魔法効果を実行する(MP消費、ダメージ、回復、バフ、防御)

味方魔法の設計(データ構造)

味方魔法は common.h にある次の型で表現します。

SpellType(魔法の種類)

種類意味代表例
SPELL_DMGダメージ魔法ファイア、スマイト
SPELL_HEAL回復魔法ヒール、リカバー
SPELL_BUFF強化・防御系ウォークライ、ガードオーラ

Spell(魔法定義)

フィールド意味
namechar配列表示名
typeSpellTypeダメージ/回復/強化
mp_costint消費MP
powerint効果量(倍率や回復量など)

職業ごとの魔法セット(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 };
    }
}

魔法一覧

職業魔法名種類消費MPpower効果の意味
勇者ヒール回復425HPを25回復
勇者ホーリーブレード攻撃52MAG×2 ダメージ
勇者ガードオーラ強化41defending を 1 にして防御状態
勇者スマイト攻撃73MAG×3 ダメージ
戦士パワーストライク攻撃42MAG×2 ダメージ(戦士でも魔法扱い)
戦士ウォークライ強化42base_atk に +2(恒久強化)
戦士アイアンウォール強化52defending を 1 にして防御状態
戦士スラッシュウェブ攻撃73MAG×3 ダメージ
魔法使いファイア攻撃42MAG×2 ダメージ
魔法使いアイス攻撃42MAG×2 ダメージ
魔法使いサンダー攻撃63MAG×3 ダメージ
魔法使いリカバー回復535HPを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_CHAR4以上は存在しない

不正なら return 0(失敗)で battle 側に戻します。

➁ MPチェック → MP消費

処理目的
caster->mp < sp.mp_costMP不足なら発動不可
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系統に分かれます。

  1. ウォークライ:攻撃力アップ(base_atkを増やす)
  2. それ以外:防御状態(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.defendingcast_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っぽくなっていきます。