休日なので、オンラインゲームのプレイ記録を残すためのツールをつくります。
全部つくることはできないと思うため、今回はアイデア出し、設計、ツールの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ファイルを作成する。
<!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>
ここから始めます。
今日はここまで
半日かかって、ここまでできました。
見た目を整えるのは、後で悩むことにしました。
まだ、手戻りがあるでしょうし。
内容を簡単に説明します。
機能
自動保存
Webブラウザのローカルストレージに入力データをJSON形式で定期的に自動保存するようにしました。
ブラウザをリロードした際は、ローカルストレージからデータをロードして表示します。
このローカルストレージを明示的に消去するボタンを設置しました。
フォームの一括消去
一から新しく入力したい時用に、消去ボタンを設置しました。
データ入力欄
トレジャーハントのパーティでは複数枚の地図を開くため、各地図情報を入力できるようにしました。
「地図追加」ボタンを押すと、入力欄が追加されます。
「Open/Close」ボタンを押すと、入力欄を閉じたり開いたりできるようにし、入力しやすいようにしました。
また、「Delete」ボタンはそれぞれの地図データセットを消すボタンです。
ですが、完全にデータを消去するのではなく、消去フラグを立てるだけにして、後で復活させられるようにしました。
出力欄
さしあたっては、データをJSON形式でコピーできるようにしました。
「Generate」ボタンを押すと、現時点での入力データが右側の欄へ表示され、コピーできます。
技術的な話
自動保存のローカルストレージ
ローカルストレージを使用したため、(プライベートモードでの表示でなければ)データは期限なしに保持されます。
そのため、ローカルストレージのデータを明示的に消去する機能を設けました。
保存するときの形式は、単純にアプリで扱っているデータをごそっとJSON.stringify()
しました。
巨大なデータではないため、そのままJSON文字列化して、データのロード/セーブを簡単にしています。
内部的なデータ構造
選択肢などフォームを構築するための情報を以下のように保持しています。
const maps = ref([17, 15, 14, 12, 10])
const areas = ref({
1700: "オルコ・パチャ",
1701: "コザマル・カ",
1702: "ヤクテル樹海",
1703: "シャーローニ荒野",
1704: "ヘリテージファウンド",
1500: "エルピス",
1400: "ラヴィリンソス",
1401: "サベネア島",
1402: "ガレマルド",
1403: "嘆きの海",
1404: "ウルティマ・トゥーレ",
});
さらに、入力データは以下の配列にオブジェクトを追加していきます。
const logs = ref([
{
open: true,
delete: false,
number: 1,
map: 17,
area: 1700,
position: 1,
portal: false,
itemsAbove: [],
}
]);
これらを組み合わせて、入力フォームを生成します。
<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には何番目の地図であるかを付与し、地図毎に一意のデータとして扱われるようにしています。
これを忘れると、すべての地図を通してデータがひとつとして扱われてしまいます。
最初、ここに気づかず、少し悩みました。
別記事書いていて思ったのですが、v-model
を書いたなら、name
属性は不要かもしれません。後ほど検証。
<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テキスト形式で出力はできるため、二段階目は後回しでもよいと思っています。
どちらかというと、今回は考慮していない、宝物庫(ダンジョン)に入ってからの記録もできるようにしたいところです。
完成するといいですね。