ターン制RPGの作り方#18|売店:どうぐと装備を買えるようにしよう

ロビーで「売店」を選ぶと、右に売り物の一覧が開く。ハーブティー 8G を買うと右上のお金が 50G から 42G に減り、ペーパーナイフ 40G を買うと 2G に。お金が足りなくなって一覧が全部灰色になり、Escでロビーに戻る
売店でどうぐと装備を買う

前回、まほう図書館のロビーを作って、バトルでもらったお金を持ち越せるようにしました。せっかくお金がたまっても、使い道がないとさみしいですね・・・。今回は、ロビーの「売店」を作って、どうぐや装備を買えるようにしたいと思います!

目次

今回作るもの

ロビーの右に売店の一覧が開いている。ハーブティー 8G・まほうのクッキー 20G・どくけしキャンディ 10G・ペーパーナイフ 40G・ブックカバー 30G。上のメッセージは「いらっしゃい! なにを かう?」、左下のメニューは灰色
ロビーの売店
  • ロビーの「売店」を押すと、売り物の一覧が開く。名前は左、値段は右にそろえて出す
  • 選ぶとお金を払って、どうぐは持ち物に、装備は「装備のふくろ」に入る
  • お金が足りないものは灰色になって選べない
  • 右上のお金は、パッと変わるのではなく、数字が動いてウインドが弾む
売り物値段効果
ハーブティー8 GHPを30回復
まほうのクッキー20 GMPを10回復
どくけしキャンディ10 Gどくを治す
ペーパーナイフ(ぶき)40 Gこうげき +3
ブックカバー(ぼうぐ)30 Gぼうぎょ +3
  • めざましどけいや、もっと強い装備は、#21で地下2階・地下3階に行けるようになったら並べます
  • 買った装備を付けるのは、次の#19(装備の画面)です。今回は買ってふくろに入れるところまでです
スクリプト役割付ける場所
IShopGoods売り物(名前・値段・買ったら持ち物に入れる)(付けない)
ItemDataどうぐに値段を付ける(付けない)
EquipmentData装備1つ分のデータ(ぶき・ぼうぐ、上がる能力)(付けない)
GameSession装備のふくろ-
CommandMenu文字だけでボタンを並べて、選んだ番号を返す-
Shop売店:一覧から選んで買うLobby の Shop
GoldCounterお金の数字を動かして、ウインドを弾ませるLobby の GoldWindow
LobbyMenu「売店」のボタン-

売り物のデータ

上に緑の箱「IShopGoods(売り物)」:GoodsName:名前、Price:値段、AddTo:買ったら持ち物に入れる。下の2つの箱「ItemData(どうぐ)」AddTo → Inventory に1つ足す、「EquipmentData(装備)」AddTo → 装備のふくろに入れる
どうぐも装備も「売り物」として扱う

この節では、売店に並べるもののデータを作ります。

売店には、どうぐ(ItemData)と装備(EquipmentData)が並びます。ただ、ItemDataはBattleCommandを受け継いでいて、装備はコマンドではありません。2つは別々の親を持つクラスです。そこで、「売り物」という共通の約束(インターフェース)を作って、どちらにも付けます。

  • 「IShopGoods」スクリプトを作る
// 売店に並べられるもの(どうぐ・装備)。どちらも、名前と値段を持っていて、買ったら持ち物に入る
public interface IShopGoods
{
    string GoodsName { get; } // 一覧に出す名前
    int Price { get; }        // 値段(ゴールド)

    // 買ったものを、session の持ち物に入れる
    void AddTo(GameSession session);
}
  • インターフェースは「このプロパティとメソッドを持っています」という約束です。中身は、付けたクラスのほうに書きます
  • クラスの親は1つしか持てませんが、インターフェースはいくつでも付けられます。ItemDataは「BattleCommandを受け継いでいて、売り物でもある」になります
  • 名前の頭にIを付けるのは、C#でインターフェースによく使う決まりです
  • インターフェースについては、9-7 UIをドラッグ&ドロップしようでも、マウスのイベントを受け取るのに使いました

どうぐに値段を付ける

Data/Items/HerbTeaのInspector。一番下にPriceの欄が増えて、8が入っている
どうぐのPrice

ItemDataに、IShopGoodsを付けます。BattleCommandの後ろに、,で区切って書きます。

public class ItemData : BattleCommand, IShopGoods

値段の欄と、約束したプロパティとメソッドを足します(effectPrefabの下)。

    [SerializeField] private int price;                   // 売店での値段

    public string GoodsName => CommandName;
    public int Price => price;

    // 買ったら、持ち物に1つ足す
    public void AddTo(GameSession session)
    {
        session.Inventory.Add(this, 1);
    }
  • 名前は、コマンドの名前(ハーブティーなど)をそのまま使います
  • 約束したもの(GoodsName・Price・AddTo)が1つでも足りないと、エラーになって教えてくれます
  • Data/ItemsのどうぐのPriceを、HerbTea 8、MagicCookie 20、AntidoteCandy 10、AlarmClock 60 にする

EquipmentData

PaperKnifeとBookCoverのInspectorが2つ。PaperKnifeはEquipment Nameにペーパーナイフ、SlotはWeapon、Price 40、BonusのAttackが3。BookCoverはブックカバー、SlotはArmor、Price 30、BonusのDefenseが3
装備のデータ

装備1つ分のデータを作ります。

  • 「EquipmentData」スクリプトを作る
using UnityEngine;

// 装備する場所
public enum EquipSlot
{
    Weapon, // ぶき
    Armor,  // ぼうぐ
}

// 装備1つ分のデータ(ぶき・ぼうぐ)。装備すると、bonus の分だけ能力が上がる
[CreateAssetMenu(fileName = "NewEquipment", menuName = "TurnBasedRpg/Equipment")]
public class EquipmentData : ScriptableObject, IShopGoods
{
    [SerializeField] private string equipmentName; // 名前
    [SerializeField] private EquipSlot slot;       // ぶき か ぼうぐ か
    [SerializeField] private int price;            // 値段
    [SerializeField] private Stats bonus;          // 上がる能力(こうげき +3 なら attack に 3)

    public string GoodsName => equipmentName;
    public int Price => price;
    public EquipSlot Slot => slot;
    public Stats Bonus => bonus;

    // 買ったら、装備のふくろに入れる(装備するのは #19 の装備の画面)
    public void AddTo(GameSession session)
    {
        session.Equipments.Add(this);
    }
}
  • slot:ぶき か ぼうぐ か。6-15 選択肢に名前を付けよう:enum(列挙型)とswitch文のenumです。1人がぶきとぼうぐを1つずつ装備できるようにします(#19)
  • bonus:装備したときに上がる能力です。#2で作ったStatsをそのまま使うので、HPやすばやさが上がる装備も作れます
  • AddTo:装備は、持ち物(Inventory)ではなく、次に作る「装備のふくろ」に入れます

GameSessionに、装備のふくろを足します(Inventoryの下)。

    public List<EquipmentData> Equipments { get; } = new List<EquipmentData>(); // 装備のふくろ(買って、まだ装備していないもの)
  • 同じ装備を2つ買ったら、2つ入ります(2人に同じペーパーナイフを持たせられるように)

データを作ります。

  • Dataフォルダの中に「Equipments」フォルダを作る
  • 右クリック>Create>TurnBasedRpg>Equipmentで、次の6つを作る
ファイルEquipment NameSlotPriceBonus
PaperKnifeペーパーナイフWeapon40Attack 3
PaperweightぶんちんWeapon120Attack 7
MagicPenまほうのペンWeapon300Attack 12・Magic 4
BookCoverブックカバーArmor30Defense 3
LibrarianApron司書のエプロンArmor100Defense 6
MagicCapeまほうのマントArmor260Defense 10・Magic 2
  • 今回売店に並べるのは、ペーパーナイフとブックカバーだけです。ほかの4つは、#21で地下2階・地下3階に行けたら並べます

一覧を文字で並べられるようにしよう

売店の一覧のアップ。ハーブティー 8 G、まほうのクッキー 20 G、どくけしキャンディ 10 G、ペーパーナイフ 40 G。名前は左にそろい、値段は右はしで「8」と「20」の一の位が縦にそろっている
名前は左、値段は右

この節では、#16で作ったCommandMenu(まほう・どうぐの一覧)を、売店でも使えるようにします。

今のCommandMenuは、BattleCommandの配列を受け取ってボタンを作ります。でも、装備はBattleCommandではありません。そこで、ボタンに出す文字と、押せるかどうかだけを受け取って、押されたボタンの番号を返すメソッドを作ります。今までのWaitForCommandAsyncは、そのメソッドを使う形に書き直します。

選ばれたものを入れる箱を、コマンドから番号に変えます。

    // 押されたボタンの番号が入る箱(キャンセルなら -1 が入る)
    private readonly AwaitableCompletionSource<int> choiceSource = new AwaitableCompletionSource<int>();

Updateのキャンセルのところも、番号の-1にします。

            Choose(-1);

WaitForCommandAsyncとCreateButtonsを、次のように書き換えます。

    // user のコマンドでボタンを作り、どれかが押されるまで待つ(キャンセルなら null)
    // values を渡すと、ボタンの右はしにそれを出す(どうぐの個数、まほうの MP)
    public async Awaitable<BattleCommand> WaitForCommandAsync(BattleCommand[] commands, BattleUnit user, string[] values = null)
    {
        string[] labels = new string[commands.Length];
        bool[] usables = new bool[commands.Length];
        for (int i = 0; i < commands.Length; i++)
        {
            BattleCommand command = commands[i];
            labels[i] = command.MenuLabel;
            usables[i] = command.CanUse(user); // user が使えないコマンドは灰色
        }

        int index = await WaitForChoiceAsync(labels, values, usables);
        if (index < 0)
        {
            return null;
        }
        return commands[index];
    }

    // labels の文字でボタンを作り、どれかが押されるまで待つ。押されたボタンの番号を返す(キャンセルなら -1)
    // values は右はしに出す文字(無ければ null)、usables が false のボタンは灰色
    // 最初は firstIndex のボタンを選ぶ(灰色なら、使えるボタンの一番上)
    public async Awaitable<int> WaitForChoiceAsync(string[] labels, string[] values, bool[] usables, int firstIndex = 0)
    {
        // 前の人の決定(Enter など)が、まだこのフレームに残っていることがあるので、1フレーム待ってから始める
        await Awaitable.NextFrameAsync(destroyCancellationToken);
        CreateButtons(labels, values, usables);
        choiceSource.Reset();
        isWaiting = true;

        lastSelected = buttons[0].gameObject;
        if (firstIndex < buttons.Count && buttons[firstIndex].interactable)
        {
            lastSelected = buttons[firstIndex].gameObject;
        }
        else
        {
            foreach (Button button in buttons)
            {
                if (button.interactable)
                {
                    lastSelected = button.gameObject;
                    break;
                }
            }
        }
        EventSystem.current.SetSelectedGameObject(lastSelected);

        int index = await choiceSource.Awaitable;

        isWaiting = false;
        return index;
    }

    // labels の数だけボタンを作る(前のボタンは消す)
    private void CreateButtons(string[] labels, string[] values, bool[] usables)
    {
        foreach (Button oldButton in buttons)
        {
            // Destroy はフレームの最後に消えるので、先に隠して、並びから外しておく
            oldButton.gameObject.SetActive(false);
            Destroy(oldButton.gameObject);
        }
        buttons.Clear();

        // スクロールする一覧は Content の子に、そうでなければ自分の子にボタンを作る
        Transform parent = transform;
        if (scrollRect != null)
        {
            parent = scrollRect.content;
            scrollRect.content.anchoredPosition = Vector2.zero; // 一覧の一番上から見せる
        }

        for (int i = 0; i < labels.Length; i++)
        {
            int index = i; // ボタンが押されたときに使うので、ボタンごとに別の変数にしておく
            Button button = Instantiate(buttonPrefab, parent);
            button.GetComponentInChildren<TMP_Text>().text = labels[i];
            if (values != null)
            {
                // 一覧のボタン(ListButton)は、右はしの ValueText に出す
                button.transform.Find("ValueText").GetComponent<TMP_Text>().text = values[i];
            }
            button.interactable = usables[i];
            button.onClick.AddListener(() => Choose(index));
            buttons.Add(button);
        }
    }

最後に、Chooseも番号を受け取るようにします。

    private void Choose(int index)
    {
        // 待っていないときに押されても、何もしない
        if (isWaiting)
        {
            choiceSource.TrySetResult(index);
        }
    }
  • WaitForChoiceAsync:ボタンの文字(labels)・右はしの文字(values)・押せるか(usables)を受け取って、押されたボタンの番号を返します。キャンセルなら-1です
  • firstIndex:最初に選んでおくボタンです。売店で1つ買ったあとも、同じものを続けて選べるようにします
  • WaitForCommandAsync:コマンドから文字と「使えるか」を作って、WaitForChoiceAsyncに渡します。バトルのほうは、今までどおり動きます
  • int index = i;:ボタンが押されたときのChoose(index)は、ボタンを作り終わったずっとあとで動きます。Choose(i)と書くと、そのときのi(最後まで数え終わった数)が使われてしまうので、ボタンごとに別の変数に入れておきます

売店を作ろう

売店の一覧が2つ。左の「42 G のとき」は、全部白くて選べる。右の「2 G のとき(どれも買えない)」は、全部灰色
お金が足りないものは灰色

この節では、ロビーに売店の一覧を置いて、買えるようにします。

ShopWindow

LobbyのHierarchyで、GoldWindowの下に灰色のShopWindow(Viewport・Scrollbar Vertical)。InspectorはRect TransformのPos X 1450、Pos Y 560、Width 560、Height 448、ImageにUI_window、Scroll Rect
BattleのItemWindowを複製

一覧のウインドは、BattleのItemWindow(#16で作ったScroll View)をそのまま使います。

  • Battleシーンを開き、HierarchyのItemWindowを選んでCtrl+C
  • Lobbyシーンを開き、Canvasを右クリック>Paste As Child
  • 名前を「ShopWindow」にして、GoldWindowのすぐ下に置く(ScreenFade・SceneFadeより上)
  • Pos Xを 1450、Pos Yを 560 にする(右の本棚の前)
  • 一番上のチェックを外して、隠しておく(Battleで隠していたので、はじめから外れています)

Shop

LobbyのShopオブジェクトのInspector。Shop (Script)のItemsにHerbTea・MagicCookie・AntidoteCandy、EquipmentsにPaperKnife・BookCover、Goods MenuにShopWindow (Command Menu)、Message WindowにMessageWindow、Message TextにMessageText、Gold CounterにGoldWindow (Gold Counter)
売っているもの

売店のスクリプトを作ります。

  • 「Shop」スクリプトを作る
using System.Collections.Generic;
using TMPro;
using UnityEngine;

// 売店:売り物の一覧から選んで、お金を払って持ち物に入れる
public class Shop : MonoBehaviour
{
    [SerializeField] private ItemData[] items;           // 売っている どうぐ
    [SerializeField] private EquipmentData[] equipments; // 売っている 装備
    [SerializeField] private CommandMenu goodsMenu;      // 売り物の一覧(ShopWindow)
    [SerializeField] private MessageWindow messageWindow;
    [SerializeField] private TMP_Text messageText;       // 上のメッセージ(「なにを かう?」をすぐ出す)
    [SerializeField] private GoldCounter goldCounter;

    // 売店を開いて、キャンセルされるまで買い物をする
    public async Awaitable OpenAsync()
    {
        GameSession session = GameSession.Instance;
        List<IShopGoods> goodsList = new List<IShopGoods>();
        goodsList.AddRange(items);
        goodsList.AddRange(equipments);

        goodsMenu.gameObject.SetActive(true);
        int index = 0; // 前に選んでいたもの(買ったあとも、同じところから選べるように)
        while (true)
        {
            // 名前・値段と、お金が足りるか(足りないものは灰色)
            string[] names = new string[goodsList.Count];
            string[] prices = new string[goodsList.Count];
            bool[] canBuy = new bool[goodsList.Count];
            for (int i = 0; i < goodsList.Count; i++)
            {
                IShopGoods product = goodsList[i];
                names[i] = product.GoodsName;
                prices[i] = $"<mspace=0.6em>{product.Price,3}</mspace> G";
                canBuy[i] = session.Gold >= product.Price;
            }

            messageText.text = "いらっしゃい! なにを かう?";
            index = await goodsMenu.WaitForChoiceAsync(names, prices, canBuy, index);
            if (index < 0)
            {
                break; // キャンセル:売店を出る
            }

            IShopGoods goods = goodsList[index];
            session.Gold -= goods.Price;
            goods.AddTo(session);
            Awaitable count = goldCounter.CountToAsync(session.Gold);
            await messageWindow.ShowAsync($"{goods.GoodsName}を かった!");
            await count;
        }
        goodsMenu.gameObject.SetActive(false);
    }
}
  • goodsList:どうぐと装備を、IShopGoodsのListにまとめます。どちらも「売り物」なので、1つのListに入れられます
  • 一覧を出すたびに、今のお金で買えるかを調べます。1つ買ってお金が減ると、足りなくなったものが灰色になります
  • 値段は、#16の個数と同じく<mspace>で数字の幅をそろえ、3けたの幅で右はしにそろえます
  • AddTo:どうぐか装備かを調べなくても、それぞれのクラスに書いたAddToが呼ばれて、持ち物かふくろに入ります。インターフェースを使うと、こういう「種類ごとにちがう動き」をすっきり書けます
  • お金の数字を動かすGoldCounterは、次の節で作ります(ここではまだエラーになります)
  • Hierarchyで、Canvasの外に空のオブジェクトを作り、名前を「Shop」にして、「Shop」スクリプトを付ける
    • Itemsに HerbTea・MagicCookie・AntidoteCandy、Equipmentsに PaperKnife・BookCover
    • Goods Menuに ShopWindow、Message Windowに MessageWindow、Message Textに MessageWindow>MessageText、Gold Counterに GoldWindow(次の節で付けます)

LobbyMenu

LobbyのLobbyオブジェクトのInspector。Lobby Menu (Script)にGold Counter・Shop Button・Shop・Menu Group・Message Textの欄が増えて、GoldWindow (Gold Counter)・ShopButton (Button)・Shop (Shop)・LobbyWindow (Canvas Group)・MessageText が入っている
LobbyMenuに売店をつなぐ

LobbyMenuに、売店のボタンをつなぎます。

右上のお金は、文字を直接書きかえるのではなく、次の節のGoldCounterに任せます。goldTextの欄を、次のように書き換えます。

    [SerializeField] private GoldCounter goldCounter; // 右上のお金

restButtonの下に、欄を足します。

    [SerializeField] private Button shopButton;       // 売店
    [SerializeField] private Shop shop;
    [SerializeField] private CanvasGroup menuGroup;   // メニューのボタン(売店にいる間は、押せないようにする)
    [SerializeField] private TMP_Text messageText;    // 上のメッセージ(売店から戻ったら、元の文に)

Startで、ボタンが押されたときのメソッドを登録します(restButtonの下)。

        shopButton.onClick.AddListener(OnShopClicked);

ShowPartyの最後の、お金を出す行を書き換えます。

        goldCounter.ShowInstant(session.Gold);

RestAsyncで休憩室のお金を払うところも、GoldCounterで数字を動かします。

        session.Gold -= restPrice;
        Awaitable count = goldCounter.CountToAsync(session.Gold);
        await messageWindow.ShowAsync($"{restPrice}ゴールドで ひとやすみ。");
        await count;

OnRestClickedの下に、売店のボタンのメソッドを足します。

    private async void OnShopClicked()
    {
        if (isBusy)
        {
            return;
        }
        isBusy = true;
        menuGroup.interactable = false; // 売店の一覧から、メニューのボタンへ移れないように
        try
        {
            await shop.OpenAsync();
        }
        catch (System.OperationCanceledException)
        {
            return;
        }
        menuGroup.interactable = true;
        messageText.text = "ここは まほう図書館の ロビー。";
        EventSystem.current.SetSelectedGameObject(shopButton.gameObject);
        isBusy = false;
    }
  • menuGroup:キーボードやゲームパッドで一覧を上下に動かしているうちに、左下のメニューのボタンに移ってしまわないように、売店にいる間はメニューのボタンを押せなくします
  • Canvas Groupは、子のボタンをまとめて押せなくできるコンポーネントです。interactableをfalseにすると、子のボタンが全部灰色になります
  • 売店から出たら、メッセージを元に戻して、「売店」のボタンを選び直します
  • LobbyのLobbyWindowに、Add Component>Canvas Groupを付ける
  • ShopButtonのButtonのInteractableにチェックを入れる(#17で外していました)
  • Lobbyオブジェクトの Lobby Menu に、Gold Counter(GoldWindow)・Shop Button(ShopButton)・Shop(Shop)・Menu Group(LobbyWindow)・Message Text(MessageText)を入れる

お金が減るのを見せよう

右上のお金のウインドのアップ。ペーパーナイフを買うと、42 G から数字がだんだん減って 2 G になり、ウインドが一瞬大きくなって戻る
数字が動いて、ウインドが弾む

この節では、お金が減ったことがひと目で分かるようにします。数字がパッと変わるだけだと、買ったことに気づきにくいですね。数字を少しずつ動かして、ウインドを弾ませます!

  • 「GoldCounter」スクリプトを作る
using TMPro;
using UnityEngine;

// 右上のお金。お金が変わったら、数字を少しずつ動かして、ウインドを弾ませる
public class GoldCounter : MonoBehaviour
{
    [SerializeField] private TMP_Text goldText;
    [SerializeField] private float countSeconds = 0.4f; // 数字を動かす時間
    [SerializeField] private float bumpScale = 1.15f;   // 弾んだときの大きさ

    private int shownGold; // 今、表示している数

    // すぐその数にする(シーンの始め)
    public void ShowInstant(int gold)
    {
        shownGold = gold;
        goldText.text = $"{gold} G";
    }

    // 今の数から gold まで、countSeconds 秒かけて数字を動かす。始めに少し大きくして、元の大きさに戻す
    public async Awaitable CountToAsync(int gold)
    {
        int startGold = shownGold;
        shownGold = gold;
        float time = 0f;
        while (time < countSeconds)
        {
            time += Time.deltaTime;
            float rate = Mathf.Clamp01(time / countSeconds);
            goldText.text = $"{Mathf.RoundToInt(Mathf.Lerp(startGold, gold, rate))} G";
            transform.localScale = Vector3.one * Mathf.Lerp(bumpScale, 1f, rate);
            await Awaitable.NextFrameAsync(destroyCancellationToken);
        }
        goldText.text = $"{gold} G";
        transform.localScale = Vector3.one;
    }
}
  • ShowInstant:シーンの始めは、数字を動かさずにそのまま出します(#17のゲージと同じ考え方です)
  • CountToAsync:Mathf.Lerpで、始めの数から新しい数までを、時間に合わせて動かします。大きさも、1.15倍から1倍へ戻していきます
  • shownGoldは、数字を動かし始めるときに新しい数にしておきます。動いている途中でもう一度呼ばれても、正しい数から始められます
LobbyのGoldWindowのInspector。一番下にGold Counter (Script)が付いて、Gold TextにGoldText、Count Secondsが0.4
GoldWindowにGoldCounter
  • LobbyのGoldWindowに「GoldCounter」スクリプトを付けて、Gold Textに GoldText を入れる
  • GoldWindowの基準点(Pivot)は右上なので、右上を中心に大きくなります
スライム

数字がチャラララって減ると、「買った!」って感じがしますね・・・

フェアリー

あとで、お金の音も付けたいわね!

動かして確認!

売店でハーブティーを選ぶと「ハーブティーを かった!」。右上のお金が 50 G から 42 G に減っていく
1つ買ったところ
  • Lobbyシーンを開いて再生し、「売店」を選ぶ
  • ハーブティーを買うと、お金が8ゴールド減る。続けて、ペーパーナイフを買う
  • お金が足りなくなったものは、灰色になって選べない
  • Escで売店を出て、「地下へ」でバトルに行き、「どうぐ」にハーブティーが1つ増えていることを確かめる

#18の動作確認

  • 「売店」で、右に売り物の一覧が開く。名前は左、値段は右はしにそろって出る
  • 売店にいる間は、左下のメニューが灰色になり、キーボードで一覧からメニューに移らない
  • 買うと「〇〇を かった!」と出て、右上のお金の数字が動いて減り、ウインドが弾む
  • 1つ買ったあとも、同じものが選ばれたままで、続けて買える
  • お金が足りないものは灰色になる。全部足りなくなっても、Escで売店を出られる
  • 買ったどうぐは、バトルの「どうぐ」に増えている
  • 休憩室でも、お金の数字が動いて減る
  • バトルの「まほう」「どうぐ」の一覧は、今までどおり動く

あとがき

今回は、ロビーに売店を作って、どうぐと装備を買えるようにしました。どうぐと装備は親のちがうクラスですが、IShopGoodsという約束を付けたことで、売店では同じ「売り物」として1つの一覧に並べられましたね。次回は、買った装備を仲間に付けて、こうげきやぼうぎょが上がるようにしたいと思います!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメント一覧 (1件)

コメントする

目次