FF14の地図の記録を残すためのツールを作りかけた話

休日なので、オンラインゲームのプレイ記録を残すためのツールをつくります。

全部つくることはできないと思うため、今回はアイデア出し、設計、ツールのUI部分の作成となります。

その後の工程実施と記事にするかどうかは未定です。

# アイデア

オンラインゲームFF14のコンテンツである「トレジャーハント」（通称、地図）の記録を残すツールをつくる

# 残したい記録とは？

- 魔紋が開いたか（地上で宝箱を開けた後、ランダムで宝物庫への扉である魔紋が開く）
→開いたか開いていないかのどちらか
- ドロップしたアイテム（特に、パーティメンバーでダイス勝負して、いちばん大きな目を出した人が獲得するロット品を記録したい）
→アイテム名（アイテムは限られている）
- 宝箱の座標（マップエリアと詳細位置）
→エリア名と詳細座標（座標の選択肢は限られている）
- 地図の種類
→1から17まで、もしくは、S1からS3まで
- 実地日時
→日時

# 要件は？

- データベースに記録を残し、後で解析、図表化できるようにしたい
- お手軽に入力フォームを生成したいので、Webページで入力したい
- 各項目はできるだけ選択肢から選べるようにする
- フォーム入力中は自動保存したい
- ネット経由でどこからでも入力できるようにしたいが、アクセスは自分だけ

# 実現方法をあれこれ考える
- ネット経由で入力したいけど、セキュリティを厳密に考えるのはめんどくさい
- 自動保存はデータベースへ書きこむのではなく、Webブラウザのストレージでいいかな
- Webページの作成は、ある程度知識があるVue.jsでいいか
- Nuxtでもいいけど、複数ページがあるわけではないから、Vue.jsでお手軽にしよう
- 選択肢を選ぶのと、新しい選択肢を追加するのはシームレスにしたい
- アクセス制限の設定が簡単だった気がするし、Cloudflareにデプロイでいいかな
- となると、データベースはD1でいいか？
- でもそうなると、フロントエンドとサーバーサイドを両方とも開発する必要があるのか、めんどくさいな
- めんどくさいから、開発を二段階にわけるか
- 一段階目は、Webページから入力、テキストデータ（JSON）で出力する
- 二段階目で、サーバーサイドを経由して、データベースに出力する
- つまり、一段階目の開発では、ローカルでHTMLファイルをひとつつくればいいんだな、これは楽だ
- 選択肢があるから、選択肢のJSONデータを入力できるようにしておけばいいか
- ゼロ段階目が、選択肢のJSONデータを入力して、フォームを生成することだな
- ファイルひとつなら、別のパソコンで入力したい際も、コードを管理しているGitHubからダウンロードすればいいから楽だろう
- 一段階目の出力データはひとまずGitHubでソースコードと一緒に保存するようにしよう
- 選択肢のJSONデータもツールから出力できればいいけど、まだデータ構造が固まってないから、手戻りもありそうだし、ひとまず妥協して、JSONデータは手で書こう

# 開発の方向性の結論

## 開発を三段階にわける

- ゼロ段階目は、選択肢を記載したJSONデータを入力とし、ツールであるWebフォームを出力する仕組みの作成（フロントエンド）

- 一段階目は、トレジャーハント実施時にその記録を随時入力し、JSONデータとして出力する仕組みの作成（フロントエンド）

- 二段階目は、JSONデータを入力し、データベースへ出力する仕組みの作成（サーバーサイド）

## フロントエンドはCDNのVue.jsで構築する

ゼロ段階目の仕組みを作成してから、一段階目の仕組みを作成する順番でコツコツと作成し、必要に応じて段階を戻って修正していく。

ファイル選択・保存ではなく、フォームへテキストデータをコピペする形式、フォームからテキストデータをコピーする形式でお手軽にする（ファイルそのものは扱わない。やるなら後で）。

## サーバーサイドは未定

APIを簡単に構築できること、フロントエンドとの統合管理が簡単になる方法を採用する予定。

デプロイ先の有力候補がCloudflareだから（使用経験があるため）、Cloudflare Workersか何かが使えるのではないかと考えている（根拠のない妄想）。

# ひとまずベースを準備する

Vue.jsの公式ドキュメント「CDN の Vue を使用する」より、ベースのHTMLファイルを作成する。

https://ja.vuejs.org/guide/quick-start.html#using-vue-from-cdn

```html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>トレジャーハント・ログ・ツール</title>
</head>

<body>
    <div id="app">{{ message }}</div>
</body>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
    const { createApp, ref } = Vue

    createApp({
        setup() {
            const message = ref('Hello vue!')
            return {
                message
            }
        }
    }).mount('#app')
</script>

</html>
```

ここから始めます。

# 今日はここまで

半日かかって、ここまでできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3821654/08ee637d-0b0f-ecf3-bf75-93a62fddcc02.png)

見た目を整えるのは、後で悩むことにしました。
まだ、手戻りがあるでしょうし。

内容を簡単に説明します。

## 機能

### 自動保存

Webブラウザのローカルストレージに入力データをJSON形式で定期的に自動保存するようにしました。
ブラウザをリロードした際は、ローカルストレージからデータをロードして表示します。

このローカルストレージを明示的に消去するボタンを設置しました。

### フォームの一括消去

一から新しく入力したい時用に、消去ボタンを設置しました。

### データ入力欄

トレジャーハントのパーティでは複数枚の地図を開くため、各地図情報を入力できるようにしました。
「地図追加」ボタンを押すと、入力欄が追加されます。

「Open/Close」ボタンを押すと、入力欄を閉じたり開いたりできるようにし、入力しやすいようにしました。

また、「Delete」ボタンはそれぞれの地図データセットを消すボタンです。
ですが、完全にデータを消去するのではなく、消去フラグを立てるだけにして、後で復活させられるようにしました。

### 出力欄

さしあたっては、データをJSON形式でコピーできるようにしました。
「Generate」ボタンを押すと、現時点での入力データが右側の欄へ表示され、コピーできます。


## 技術的な話

### 自動保存のローカルストレージ

https://developer.mozilla.org/ja/docs/Web/API/Window/localStorage

ローカルストレージを使用したため、（プライベートモードでの表示でなければ）データは期限なしに保持されます。
そのため、ローカルストレージのデータを明示的に消去する機能を設けました。

保存するときの形式は、単純にアプリで扱っているデータをごそっと`JSON.stringify()`しました。
巨大なデータではないため、そのままJSON文字列化して、データのロード/セーブを簡単にしています。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify

### 内部的なデータ構造

選択肢などフォームを構築するための情報を以下のように保持しています。
```js
const maps = ref([17, 15, 14, 12, 10])
const areas = ref({
    1700: "オルコ・パチャ",
    1701: "コザマル・カ",
    1702: "ヤクテル樹海",
    1703: "シャーローニ荒野",
    1704: "ヘリテージファウンド",
    1500: "エルピス",
    1400: "ラヴィリンソス",
    1401: "サベネア島",
    1402: "ガレマルド",
    1403: "嘆きの海",
    1404: "ウルティマ・トゥーレ",
});
```

さらに、入力データは以下の配列にオブジェクトを追加していきます。

```js
const logs = ref([
    {
        open: true,
        delete: false,
        number: 1,
        map: 17,
        area: 1700,
        position: 1,
        portal: false,
        itemsAbove: [],
    }
]);
```

これらを組み合わせて、入力フォームを生成します。

```html
<div v-for="perMap in logs" :key="perMap.number" class="perMap">
    <h2>{{perMap.number}}枚目の地図 </h2>
    /* 略 */
</div>
```

これが一番外側のfor文です。

この内部では、`maps`や`areas`を表示しながら、`v-model`で`perMap`のデータをバインディングしていきます。

### 入力欄の開閉

入力欄の開閉は`v-show`を使用しています。

### フォームのname

inputタグには`name`を記載していますが、このnameには何番目の地図であるかを付与し、地図毎に一意のデータとして扱われるようにしています。
これを忘れると、すべての地図を通してデータがひとつとして扱われてしまいます。
最初、ここに気づかず、少し悩みました。

:::note
別記事書いていて思ったのですが、`v-model`を書いたなら、`name`属性は不要かもしれません。後ほど検証。
:::

```html
<div class="positions">
    <h3>Position</h3>
    <template v-for="area in Object.keys(areas)">
        <div v-if="String(area) === String(perMap.area)">
            <label v-for="position in positions[area] ">
                <input type="radio" :name="'position'+perMap.number" :value="position"
                    v-model="perMap.position">
                <img style="width:150px; height:120px; background-color:red;">
            </label>
        </div>
    </template>
</div>
```

# 終わりに

目標としていたUI部分までは大体できましたが、結局ゼロ段階目は作成していません。
ひとまず目に見える部分である一段階目を構築しました。

最初から計画倒れですが、ひとまず適切なデータ構造は思いつけたような気がします。

ここから、

- ゼロ段階目に必要なデータを収集する
- ゼロ段階目のデータを入力し、フォームを生成する仕組みを作成する

と、ゼロ段階目を実行するのがよさそうです。

JSONテキスト形式で出力はできるため、二段階目は後回しでもよいと思っています。

どちらかというと、今回は考慮していない、宝物庫（ダンジョン）に入ってからの記録もできるようにしたいところです。

完成するといいですね。