
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);
#endifextern 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 | 名前 | type | value の意味 |
|---|---|---|---|
| 0 | ポーション | IT_CONSUME | HP回復 30 |
| 1 | ハイポーション | IT_CONSUME | HP回復 70 |
| 2 | エーテル | IT_CONSUME | MP回復 15 |
| 3 | エリクサー | IT_CONSUME | HP/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_WEAPON | HERO / WARRIOR | ATK | weapon_atk |
| IT_STAFF | MAGE | MAG | staff_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 の流れはざっくりこうです。
- インベントリが空なら使えない
- 一覧表示
- 入力された番号をチェック
- 消費なら「対象選択→個数減らす→効果」
- 装備なら「装備できるか判定→装備」
抜粋するとこのあたりです。
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層(職業・成長・レベルアップ・基礎ステータス)に入って、
戦闘やアイテムの効果が「成長に応じて強くなる」気持ちよさを作っていきます。
