STEP07:items層の実装(インベントリ・消費・装備・宝箱固定配置)

STEP07では、ゲームの「アイテムまわり」を丸ごと担当する items層 を作っていきます。
この層が入ると、mainやbattleの中がスッキリして「アイテムの仕様変更」が超やりやすくなります。

このSTEPのゴールはこんな感じです。

  • インベントリ(所持数)を inv[ITEM_COUNT] で管理できる。
  • 消費アイテム(回復系)を使える。
  • 装備(武器・杖)で ATK/MAG が上がる。
  • 宝箱は座標固定で、8個の宝箱から8種類のアイテムが1回ずつ取れる(固定配置)

items層が担当するもの

役割具体例
アイテムの定義名前・種類(消費/武器/杖)・効果量
インベントリ管理所持数を数える、一覧表示する
アイテム効果HP/MP回復、装備でステータス上昇
宝箱処理宝箱を開ける → アイテムが増える。

逆に items層は「入力待ち」や「画面の演出」には深入りしません(それは input/ui の担当)。

アイテム設計の全体像(データの持ち方)

インベントリは「個数配列」で管理

GameState に以下が入っています。

int inv[ITEM_COUNT];

この意味はシンプルで、

  • inv[ITEM_POTION] = 2 ならポーションを2個持っている。
  • inv[ITEM_STEEL_SWORD] = 1 ならスチールソードを1個持っている。

という感じです。

items.h の役割(公開APIと extern)

items.h では「他の層から呼ばれる関数」と「アイテム表」を公開します。

/* items.h */
#ifndef ITEMS_H
#define ITEMS_H

#include "common.h"

extern const ItemDef g_items[ITEM_COUNT];

int any_item_available(const int inv[ITEM_COUNT]);
void show_inventory(const int inv[ITEM_COUNT]);
void use_consumable_on(Character* c, ItemID id);
int equip_item(Character* c, ItemID id);

/* 宝箱 */
void open_treasure_fixed(GameState* gs, int x, int y);

#endif

extern const ItemDef g_items[ITEM_COUNT]; って何?

  • g_items の「実体」は items.c にあります
  • 他のファイル(battle.c など)からも g_items[item_id].name みたいに参照したい
  • だから items.h で extern 宣言して「別ファイルに本体があるよ」と知らせています

アイテム定義テーブル(g_items)

items.c に「全アイテムの仕様」を集約します。

/* items.c */
const ItemDef g_items[ITEM_COUNT] = {
    {"ポーション",       IT_CONSUME, 30},
    {"ハイポーション",   IT_CONSUME, 70},
    {"エーテル",         IT_CONSUME, 15},
    {"エリクサー",       IT_CONSUME, 999},
    {"ブロンズソード",   IT_WEAPON,  3},
    {"スチールソード",   IT_WEAPON,  6},
    {"見習いの杖",       IT_STAFF,   3},
    {"賢者の杖",         IT_STAFF,   6},
};

ItemDef の構造

フィールド意味
name表示名
type種類(消費 / 武器 / 杖)
value効果量(回復量や上昇量)

アイテム一覧(仕様が一目でわかる表)

ItemID名前typevalue の意味
0ポーションIT_CONSUMEHP回復 30
1ハイポーションIT_CONSUMEHP回復 70
2エーテルIT_CONSUMEMP回復 15
3エリクサーIT_CONSUMEHP/MP 全回復(999扱い)
4ブロンズソードIT_WEAPON武器ATK +3
5スチールソードIT_WEAPON武器ATK +6
6見習いの杖IT_STAFF杖MAG +3
7賢者の杖IT_STAFF杖MAG +6

インベントリ系関数(所持チェック・一覧表示)

所持アイテムがあるかチェック

int any_item_available(const int inv[ITEM_COUNT]) {
    for (int i = 0; i < ITEM_COUNT; i++) {
        if (inv[i] > 0) return 1;
    }
    return 0;
}

ポイント

  • 1つでも inv[i] > 0 があれば「何か持ってる」と判定
  • battle.c の「アイテムがない!」判定に使うのにちょうどいいです

一覧表示

void show_inventory(const int inv[ITEM_COUNT]) {
    printf(YELLOW "---- 所持アイテム ----\n" RESET);
    for (int i = 0; i < ITEM_COUNT; i++) {
        if (inv[i] > 0) printf("%d) %-16s x%d\n", i + 1, g_items[i].name, inv[i]);
    }
}

ポイント

  • 表示番号を i + 1 にしている(人間向けの表示)
  • 後で選択入力したら id-- して 0始まりに戻すのが王道です

消費アイテム処理(use_consumable_on)

void use_consumable_on(Character* c, ItemID id) {
    if (id == ITEM_POTION || id == ITEM_HIPOTION) {
        c->hp += g_items[id].value;
        if (c->hp > c->max_hp) c->hp = c->max_hp;
        sfx_beep(2);
    }
    else if (id == ITEM_ETHER) {
        c->mp += g_items[id].value;
        if (c->mp > c->max_mp) c->mp = c->max_mp;
        sfx_beep(2);
    }
    else if (id == ITEM_ELIXIR) {
        c->hp = c->max_hp;
        c->mp = c->max_mp;
        sfx_beep(2);
    }
}

ロジックの意図

分岐処理
ポーション/ハイポーションHP回復(最大HPを超えないように丸める)
エーテルMP回復(最大MPを超えないように丸める)
エリクサーHP/MP を最大にする。

地味に大事な部分

if (c->hp > c->max_hp) c->hp = c->max_hp;

この「上限丸め」を入れておくと、回復量の調整をしてもHPが溢れないので安全です。

装備処理(equip_item)

int equip_item(Character* c, ItemID id) {
    if (g_items[id].type == IT_WEAPON) {
        if (c->job == HERO || c->job == WARRIOR) {
            c->weapon_atk = g_items[id].value;
            sfx_beep(0);
            printf(YELLOW "%s は「%s」を装備した!(武器ATK+%d)\n" RESET,
                c->name, g_items[id].name, g_items[id].value);
            return 1;
        }
        return 0;
    }
    else if (g_items[id].type == IT_STAFF) {
        if (c->job == MAGE) {
            c->staff_magic = g_items[id].value;
            sfx_beep(0);
            printf(YELLOW "%s は「%s」を装備した!(杖MAG+%d)\n" RESET,
                c->name, g_items[id].name, g_items[id].value);
            return 1;
        }
        return 0;
    }
    return 0;
}

装備の仕様

装備種別装備できる職業上がる値反映先
IT_WEAPONHERO / WARRIORATKweapon_atk
IT_STAFFMAGEMAGstaff_magic

この設計にしておくと、ダメージ計算側は

  • char_atk = base_atk + weapon_atk
  • char_mag = base_magic + staff_magic

みたいに「足すだけ」で済みます。シンプルで強いです。

宝箱固定配置(TreasureSpot と open_treasure_fixed)

固定配置のテーブル

typedef struct { int x, y; ItemID item; } TreasureSpot;

static const TreasureSpot g_treasures[ITEM_COUNT] = {
    { 1, 12, ITEM_POTION },
    { 5, 10, ITEM_HIPOTION },
    { 7, 18, ITEM_ETHER },
    { 11, 12, ITEM_ELIXIR },
    { 15, 6, ITEM_BRONZE_SWORD },
    { 17, 13, ITEM_STEEL_SWORD },
    { 19, 4, ITEM_APPRENTICE_STAFF },
    { 23, 5, ITEM_SAGE_STAFF },
};

ここが「宝箱8個 = 8アイテムを必ず1回ずつ取れる」設計の心臓部です。

  • 座標 (x, y) を固定
  • そこを開けると必ず特定アイテムが出る

座標から宝箱IDを引く

static int treasure_index_at(int x, int y) {
    for (int i = 0; i < ITEM_COUNT; i++) {
        if (g_treasures[i].x == x && g_treasures[i].y == y) return i;
    }
    return -1;
}

宝箱を開ける本体

void open_treasure_fixed(GameState* gs, int x, int y) {
    int idx = treasure_index_at(x, y);

    if (idx < 0) {
        int item = rand() % ITEM_COUNT;
        gs->inv[item]++;
        sfx_beep(0);
        printf(YELLOW "宝箱を開けた!「%s」を手に入れた!(所持:%d)\n" RESET,
            g_items[item].name, gs->inv[item]);
        return;
    }

    ItemID item_id = g_treasures[idx].item;
    gs->inv[item_id]++;

    sfx_beep(0);
    printf(YELLOW "宝箱を開けた!「%s」を手に入れた!(所持:%d)\n" RESET,
        g_items[item_id].name, gs->inv[item_id]);
}

ポイント

  • 見つかった座標なら固定アイテム
  • 見つからない座標なら保険としてランダム(設計上、基本は idx >= 0 になる想定)

つまり、マップ上の T の座標g_treasures の座標が一致していれば、8種が1回ずつ取れます。

main.c での宝箱イベント接続(Tを踏んだら items層へ)

main.c のここで items層を呼びます。

if (cell == 'T') {
    clear_screen();
    open_treasure_fixed(&gs, gs.px, gs.py);
    gs.map[gs.px][gs.py] = ' ';
    printf("Enterで続行...");
    read_line(buf, sizeof(buf));
}

ここでやっていること

意味
open_treasure_fixed(...)宝箱処理(所持数が増える)
gs.map[px][py] = ' '宝箱を取り切ったのでマップから消す(再取得防止)

この「宝箱を消す」のが大事で、これがあるから 同じ宝箱から何度も取れない ようになっています。

battle.c でのアイテム使用フロー(選ぶ→判定→効果)

battle.c の流れはざっくりこうです。

  1. インベントリが空なら使えない
  2. 一覧表示
  3. 入力された番号をチェック
  4. 消費なら「対象選択→個数減らす→効果」
  5. 装備なら「装備できるか判定→装備」

抜粋するとこのあたりです。

if (!any_item_available(gs->inv)) {
    printf(RED "アイテムがない!\n" RESET);
    continue;
}

show_inventory(gs->inv);
printf("使用/装備するアイテム番号: ");
int id = 0;
if (!read_int_safely(&id)) continue;
id--;

if (id < 0 || id >= ITEM_COUNT || gs->inv[id] <= 0) {
    printf(RED "選択が不正です。\n" RESET);
    continue;
}

ItemID item_id = (ItemID)id;

if (g_items[item_id].type == IT_CONSUME) {
    printf("対象を選んでください (1-%d): ", PARTY_SIZE);
    int t = 0;
    if (!read_int_safely(&t)) continue;
    if (t < 1 || t > PARTY_SIZE) continue;

    Character* target = &gs->party[t - 1];
    if (!target->alive) {
        printf(RED "その仲間は倒れている!\n" RESET);
        continue;
    }

    gs->inv[item_id]--;
    use_consumable_on(target, item_id);
    printf(GREEN "%s は %s を使った!\n" RESET, c->name, g_items[item_id].name);
}
else {
    if (!equip_item(c, item_id)) {
        printf(RED "その職業では装備できない!\n" RESET);
        sfx_beep(4);
    }
}

重要チェック(事故を防ぐガード)

ガード目的
`id < 0
gs->inv[id] <= 0個数0を使えないようにする
!target->alive戦闘不能者に回復を使う仕様を禁止(ここは好みで変更OK)

よくある落とし穴と改善ポイント(実装が一段よくなる)

落とし穴:装備アイテムを複数人が同時装備できてしまう

現状の battle.c は、装備をしても inv を減らしていません。
その結果、極端な話ですが「スチールソード1本」を勇者も戦士も装備できてしまいます(共有装備扱いになっている)。

選択肢は2つです。

選択肢A:装備したら inv を減らす(わかりやすい方式)

  • 装備成功時に inv を 1 減らす
  • もし既に装備していた武器があるなら、それを inv に戻す(今後の拡張)

最低限の簡易修正だけなら battle.c の装備成功時にこうできます。

if (equip_item(c, item_id)) {
    gs->inv[item_id]--; /* 装備成功したら所持を減らす(簡易版) */
} else {
    printf(RED "その職業では装備できない!\n" RESET);
    sfx_beep(4);
}

選択肢B:装備は「所持」扱いで inv は減らさない(共有/付け替え方式)

  • その代わり「今誰が装備しているか」を管理して、同時装備を防ぐ
  • 例えば equipped_weapon_id をキャラに持たせる、など

どっちでもOKですが、記事としてはAが説明しやすく、バグになりにくいです。

STEP07のまとめ

  • アイテム仕様(名前・タイプ・効果)を g_items に集約できた。
  • インベントリは inv[ITEM_COUNT] の個数管理でシンプルに運用できた。
  • 消費アイテムは回復処理、装備アイテムは職業制限+能力値上昇を実装できた。
  • 宝箱は座標固定テーブル g_treasures で「確定ドロップ」を実現できた。

次のSTEP08では、characters層(職業・成長・レベルアップ・基礎ステータス)に入って、
戦闘やアイテムの効果が「成長に応じて強くなる」気持ちよさを作っていきます。