見出し画像

【疑似3DダンジョンRPG開発日誌 05】探索中のスキル使用システムとUI統一化


疑似3DダンジョンRPG「グラヴェンの傷跡」の開発ログです。 今回はポーズメニューから回復魔法を使えるようにする機能を実装しました。


今回やったこと

  • ポーズメニューから探索中にスキルを使えるようにした

  • 使用時に回復エフェクトが出るようにした

  • システムメッセージの表示をウィンドウに統一した


1. 探索中スキル使用システムの実装

移動中のスキル表示に対応

設計方針

戦闘中と同じ3ステップの選択フローを探索中でも再現しました。

ポーズメニュー「スキル」
  ↓
① だれが使う?(メンバー選択)
  ↓
② スキル一覧(味方対象スキルのみ表示、MP不足はグレー)
  ↓
  ├─ self / all_allies → そのまま発動
  └─ one_ally → ③ だれに使う?(対象選択)→ 発動

探索中に使えるスキルは以下の種類に限定しています。

スキルタイプ 代表スキル heal ヒール(単体回復) heal_all アルデスの光(全体全回復) revive 団長の号令(戦闘不能を復活)



実装コード

PAUSE_COMMANDSに「スキル」を追加

PAUSE_COMMANDS = ["ステータス", "アイテム", "スキル", "そうび", "セーブ", "ロード", "まちへ"]

__init__に状態変数を追加

# ── 探索中スキル使用 ──
self.show_skills              = False      # スキルメニューを表示中か
self.skill_exp_phase          = "member"   # "member"→"skill"→"target" の順
self.skill_exp_member_cursor  = 0
self.skill_exp_cursor         = 0
self.skill_exp_target_cursor  = 0
self.skill_exp_selected       = ""
self.skill_exp_actor          = None

update()のポーズメニュー処理を修正

インデックスがずれるため全体を書き直します。

if self._btn_confirm():
    if self.pause_cursor == 0:    # ステータス
        self.scene = "status"
    elif self.pause_cursor == 1:  # アイテム
        self.show_items = True
        self.scene = "explore"
    elif self.pause_cursor == 2:  # スキル ← 新規追加
        self.show_skills         = True
        self.skill_exp_phase     = "member"
        self.skill_exp_member_cursor = 0
        self.scene = "explore"
    elif self.pause_cursor == 3:  # そうび
        self.scene = "equip"
    elif self.pause_cursor == 4:  # セーブ
        self.save_game()
        self.scene = "explore"
    elif self.pause_cursor == 5:  # ロード
        self.load_game()
        self.scene = "explore"
    elif self.pause_cursor == 6:  # まちへ
        self._load_town_image()
        self.scene = "town"

スキルメニューの入力処理(update内)

if self.show_items: ブロックの直後に追加します。

if self.show_skills:

    # ① メンバー選択
    if self.skill_exp_phase == "member":
        members = self.party.members
        if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W):
            self.skill_exp_member_cursor = (self.skill_exp_member_cursor - 1) % len(members)
        elif pyxel.btnp(pyxel.KEY_DOWN) or pyxel.btnp(pyxel.KEY_S):
            self.skill_exp_member_cursor = (self.skill_exp_member_cursor + 1) % len(members)
        elif self._btn_confirm():
            actor = members[self.skill_exp_member_cursor]
            if actor.hp <= 0:
                self.system_message       = "たおれているので使えない…"
                self.system_message_timer = 60
            else:
                self.skill_exp_actor  = actor
                self.skill_exp_phase  = "skill"
                self.skill_exp_cursor = 0
        elif self._btn_cancel():
            self.show_skills = False
        return

    # ② スキル選択
    if self.skill_exp_phase == "skill":
        actor      = self.skill_exp_actor
        ALLY_TYPES = {"heal", "heal_all", "revive",
                      "buff_atk", "buff_def", "buff_res"}
        usable = [s for s in actor.skills
                  if SKILL_DATA[s]["type"] in ALLY_TYPES]

        if usable:
            if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W):
                self.skill_exp_cursor = (self.skill_exp_cursor - 1) % len(usable)
            elif pyxel.btnp(pyxel.KEY_DOWN) or pyxel.btnp(pyxel.KEY_S):
                self.skill_exp_cursor = (self.skill_exp_cursor + 1) % len(usable)

        if self._btn_confirm() and usable:
            skill_name = usable[self.skill_exp_cursor]
            sdata      = SKILL_DATA[skill_name]
            if actor.mp < sdata.get("mp_cost", 0):
                self.system_message       = "MPが足りない…"
                self.system_message_timer = 60
            elif sdata["target"] in ("self", "all_allies"):
                self._use_skill_in_explore(actor, skill_name, actor)
            else:
                self.skill_exp_selected      = skill_name
                self.skill_exp_target_cursor = 0
                self.skill_exp_phase         = "target"
        elif self._btn_cancel():
            self.skill_exp_phase = "member"
        return

    # ③ 対象選択
    if self.skill_exp_phase == "target":
        members = self.party.members
        if pyxel.btnp(pyxel.KEY_UP) or pyxel.btnp(pyxel.KEY_W):
            self.skill_exp_target_cursor = (self.skill_exp_target_cursor - 1) % len(members)
        elif pyxel.btnp(pyxel.KEY_DOWN) or pyxel.btnp(pyxel.KEY_S):
            self.skill_exp_target_cursor = (self.skill_exp_target_cursor + 1) % len(members)
        elif self._btn_confirm():
            target = members[self.skill_exp_target_cursor]
            self._use_skill_in_explore(
                self.skill_exp_actor, self.skill_exp_selected, target)
        elif self._btn_cancel():
            self.skill_exp_phase = "skill"
        return

スキル発動ヘルパーメソッド

def _use_skill_in_explore(self, actor, skill_name, target):
    """探索中にスキルを発動する(回復・バフ系のみ対応)"""
    sdata   = SKILL_DATA[skill_name]
    stype   = sdata["type"]
    mp_cost = sdata.get("mp_cost", 0)
    actor.mp -= mp_cost

    if stype == "heal":
        amount = int(sdata["power"])
        target.hp = min(target.hp + amount, target.hp_max)
        self._start_heal_effect(target=target, all_members=False)
        self.system_message = f"{actor.name}の{skill_name}!{target.name}のHPが{amount}回復した"

    elif stype == "heal_all":
        for m in self.party.members:
            m.hp = m.hp_max
            m.mp = m.mp_max
        self._start_heal_effect(target=None, all_members=True)
        self.system_message = f"{actor.name}の{skill_name}!全員のHP・MPが全回復した!"

    elif stype == "revive":
        if target.hp > 0:
            self.system_message = "すでに起きている!"
            actor.mp += mp_cost
        else:
            target.hp = max(1, int(target.hp_max * sdata["power"]))
            self._start_heal_effect(target=target, all_members=False)
            self.system_message = f"{actor.name}の{skill_name}!{target.name}が復活した!"

    else:
        self.system_message = f"{skill_name}は戦闘中しか使えない…"
        actor.mp += mp_cost

    self.system_message_timer = 90
    self.show_skills = False

draw()に描画処理を追加

if self.show_items: ブロックの直後に追加します。

if self.show_skills:

    if self.skill_exp_phase == "member":
        self.draw_window(60, 48, 136,
                         16 + len(self.party.members) * WIN_LINE_HEIGHT,
                         title="だれが使う?")
        for i, m in enumerate(self.party.members):
            cur = "≫" if i == self.skill_exp_member_cursor else " "
            col = 5 if m.hp <= 0 else WIN_TEXT_COL
            self.jtext(64, 66 + i * WIN_LINE_HEIGHT,
                       f"{cur}{m.name}  MP{m.mp}/{m.mp_max}", col)

    elif self.skill_exp_phase == "skill":
        actor      = self.skill_exp_actor
        ALLY_TYPES = {"heal", "heal_all", "revive",
                      "buff_atk", "buff_def", "buff_res"}
        usable = [s for s in actor.skills
                  if SKILL_DATA[s]["type"] in ALLY_TYPES]
        self.draw_window(60, 48, 136,
                         16 + max(1, len(usable)) * WIN_LINE_HEIGHT,
                         title=f"{actor.name}のスキル")
        if not usable:
            self.jtext(64, 66, "使えるスキルがない", 5)
        for i, name in enumerate(usable):
            cur = "≫" if i == self.skill_exp_cursor else " "
            mp  = SKILL_DATA[name]["mp_cost"]
            col = 5 if actor.mp < mp else WIN_TEXT_COL
            self.jtext(64, 66 + i * WIN_LINE_HEIGHT,
                       f"{cur}{name}  {mp}MP", col)

    elif self.skill_exp_phase == "target":
        self.draw_window(60, 48, 136,
                         16 + len(self.party.members) * WIN_LINE_HEIGHT,
                         title="だれに使う?")
        for i, m in enumerate(self.party.members):
            cur = "≫" if i == self.skill_exp_target_cursor else " "
            col = 14 if m.hp <= 0 else WIN_TEXT_COL
            self.jtext(64, 66 + i * WIN_LINE_HEIGHT,
                       f"{cur}{m.name}  {m.hp}/{m.hp_max}", col)

2. システムメッセージの表示統一

セーブ・ロード・回復・階段移動など、あちこちで jtext で直接描いていたメッセージを全て draw_window() に統一しました。

変更内容

全てのメッセージを同じウィンドウに表示するようにしました

draw() の _draw_hud() 呼び出しの直後に1箇所だけ追加します。

self._draw_minimap()
self._draw_hud()

# ── システムメッセージウィンドウ(追加)──
if self.system_message_timer > 0 and not self._msg_active:
    self.draw_window(28, 196, 200, 36, message=self.system_message)

not self._msg_active の条件で戦闘メッセージと重なるのを防いでいます。

これで統一されたメッセージ一覧:

メッセージ タイミング ぼうけんのきろくをきざんだ… セーブ時
きろくのきおくがよみがえる… ロード時
扉を開いた… 扉を開けたとき
B2Fへ降りた… 階段移動時 ゆっくりと休んだ…
宿屋利用時 ○○のHPが○○回復した
探索中 スキル使用時
ゴールドがたりない… 購入失敗時


まとめ

今回の実装ポイントは、戦闘中と探索中でスキルの選択フローを共通化したことです。 同じ「メンバー選択 → スキル選択 → 対象選択」の流れにすることで、プレイヤーが操作を覚えやすくなります。

また _use_skill_in_explore() をヘルパーメソッドとして独立させたことで、今後スキルを追加するときもここに処理を書き足すだけで済む設計になっています。

いいなと思ったら応援しよう!

しん もし記事が参考になったり、開発を応援してもいいと思ってくれた方がいたら、noteのサポートをいただけると次の記事を書く原動力になります。もちろん読んでくれるだけで十分嬉しいです。