ポケモン剣盾の素早さ比較ツールをNuxt.jsで作成してNetlifyで公開するまで

[GameWith AdventCalendar 2019](https://qiita.com/advent-calendar/2019/gamewith) の記事です。
この内容はGameWithのコンテンツとは関係なく、個人的な趣味で作ったものです。

# はじめに
ポケモントレーナーの いのす(@inosy22)です！
ポケットモンスター ソード・シールド のランク戦を日々楽しんでいます。

日頃から、ポケモンを育成する際に、色々な企業や個人の皆様が作っていただいているサイトを利用させていただいています。
そんな中、ポケモンにおいて重要な要素である **素早さ** に対しては、自分にとって使いやすいと思えるツールが見当たらなかったので自作して公開しました！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ポケモン剣盾の素早さ比較ツールを公開しました！<br>『対戦中に即座に素早さを比較したい』<br>『仮想敵に対しての細かい素早さ調整をしたい』<br>という場合に使うことを想定して作ったのでよかったら使ってみてください！<a href="https://t.co/NioRCXgQTR">https://t.co/NioRCXgQTR</a></p>&mdash; いのす🌻 (@inosy22_) <a href="https://twitter.com/inosy22_/status/1203975042276544512?ref_src=twsrc%5Etfw">December 9, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

↓実際に作成して公開したものです
https://pokemon-tools.netlify.com/speed-checker/
![efbf2fc9c31c700cd1d7bd842dd9655a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/620f81bc-9131-81d5-d0f9-51373d4e5ee5.gif)

（↑ミミッキュはピカチュウ相手だと性格補正かかってないと素早さ負けちゃうことを確認しているgif）

こちらを作るにあたっての、企画 -> 実装 -> 公開 までの道のりを記載していきたいと思います。

# 前提

## 利用技術

コスト(お金も時間も)を控えめにするために以下の技術を採用しています。

 - [Nuxt.js](https://ja.nuxtjs.org/) (Vue.jsフレームワーク)
 - [Vuetify](https://vuetifyjs.com/ja/) (Vue.js用UIフレームワーク)
 - [Netlify](https://www.netlify.com/) (Hostingサービス)

Vuetify/Netlifyについては知識がなかったので勉強しながら、
Nuxt.jsは少し構築経験がありましたが、Vueに対しての知識は薄かったので、そこらへんも勉強しながらでした。

この状態で、企画->実装->公開 の流れまでで、12時間程度でできたと思います。


## ポケモンのドメイン知識
こちらについての詳細は他のサイトをご覧いただけるといいと思いますが、最低限本記事に関連しそうなものをざっくり説明をします。


 - `種族値`: ポケモン種類ごとに定められているベースとなる能力値 
 - `個体値`: ポケモン1匹ごとに与えられる能力値
    - (例外を除いて、最高値のポケモンを使うことが前提)
 - `努力値`: ポケモントレーナーが各種能力値を強化できる値
    - **★素早さ調整において努力値の調整は重要**
 - `性格補正`: ポケモンの性格によって上下する倍率 (1.1倍/1倍/0.9倍)
    - **★素早さ調整において性格補正の選択は重要**
 - `実数値`: レベル/種族値/努力値/個体値/性格補正から計算される値
    - (ポケモンのステータスを見るとこの結果が表示されています)
 - `ランク補正`: 「すばやさ が ぐーんと あがった」などの際の補正
    - **★素早さ調整においてランク補正前提の調整をすることもある**
 - `その他補正`: 持ち物/状態異常/場の状態/特性 などでも変化します
    - **★素早さ調整においてこれらの補正を前提に調整することもある**

★マークがついている部分は今回の素早さ比較ツールで操作可能な値です。

（ポケモンはLv.50、個体値は さいこう のみの前提で進めます）

# 企画

主な利用想定ユーザーは **自分** です！（笑）
素早さ計算するだけのツールはいろいろあるのですが、
比較しながら細かい調整をするツールがほしいという欲望がありました。

加えて、ポケモン剣盾から参入した新規ユーザーが、対戦中に素早さがわからないという話を、周りの人たち/実況動画/Twitter等で見かけました。

それを踏まえると、以下の方針が生まれました。

 - 対戦中に即座に素早さ比較をしやすい
 - 仮想敵に対して細かい素早さ調整がしやすい

これに従った、具体的な実装内容としては、以下の通りです。

 - 相手のポケモンと自分のポケモンの2つの計算を行って比較する
 - ポケモンを選ぶ際はインクリメンタルサーチで快適に
 - デフォルト値は最速育成状態にして、その他の補正系の値はなし
 - 努力値や補正値系はポチポチしてるだけで調整できる
 - 条件を変更した際の結果をリアルタイムで見ることができる（リアクティブ！)

# 実装

全ての実装方法を細かく解説はしきれないので、今回の特徴的な実装の部分にフォーカスして取り上げます！
細かい内容が気になる場合は、こちらのリポジトリを見ていただければと思います。
https://github.com/inosy22/pokemon-tools

今回は綺麗な実装というよりは素早く作り上げることを重視したので、コードは結構汚めです...。
最低限同じロジックを2度書かないようにする程度の調整はしてあります。


## 種族値データを用意する

GameWithのAdventCalendarなので、こちらの記事を参考にさせていただきます！
[【ポケモン剣盾】全ポケモン種族値ランキング【ポケモンソードシールド】 - GameWith](https://gamewith.jp/pokemon-sword-shield/article/show/176712)

**※記事内容の商用利用、サーバーに負荷をかけることは利用規約違反になるのでやめましょう！**

今回は商用利用でもなく、記事のコンテンツというよりは、ポケモンの普遍的なデータのみを抽出するだけです。
また、サーバーに負荷をかけずに取得します。

Webページで先ほどのページを開いて、GoogleChromeのConsoleからJavascriptでDOMの中身を抽出することで、
ポケモン名をキーにして、バリューをポケモンの情報を持つオブジェクト(現状は素早さ種族値(s)のみ)を生成します。

```js
var pokeJson = {}
$(".pokemonSS_table_scroll tr[data-col1]").each((key, val) => {
  pokeJson[$(val).attr('data-col1')] = {
    s: $(val).attr('data-col7')
  }
})
console.log(JSON.stringify(pokeJson))
```

<img width="1792" alt="スクリーンショット 2019-12-12 2.26.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/7bf1a53a-9fdd-ae8a-ba9b-e4b73973a671.png">


## Nuxt.jsプロジェクト構築

`yarn create nuxt-app` コマンドを用いてプロジェクトを作成しました。

```
create-nuxt-app v2.12.0
✨  Generating Nuxt.js project in pokemon-speed-checker
? Project name pokemon-speed-checker
? Project description Pokemon speed checker for sword-shield
? Author name inosy22
? Choose the package manager Yarn
? Choose UI framework Vuetify.js
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Choose linting tools ESLint, Prettier
? Choose test framework Jest
? Choose rendering mode Single Page App
? Choose development tools jsconfig.json (Recommended for VS Code)
```

注目すべきところは、`Vuetify` を入れたことぐらいです。

SPA(SinglePageApp) か SSR(Universal)にするかを選択しますが、今回は最終的に静的ファイルに落とし込む `nuxt generate` コマンドを利用するためどちらでもいいのかなと思います。

### CompositionAPIについて

また、今回利用しているVue.jsのバージョンは2系ですが、近い将来出るはずのVue.js3系のバージョンで標準装備されている `CompositionAPI` を勉強がてら利用してみます。

これを利用することにより、ClassベースのVueComponentではなく、Functionベースのコンポーネント作成が可能です。

Vue2系には標準装備されていないので、提供されているパッケージを追加します。

```sh
yarn add @vue/composition-api
```

参考：CompositionAPI紹介
https://vue-composition-api-rfc.netlify.com/#api-introduction

個人的には、CompositionAPIはまだRFCではあるものの、かなり使い勝手良かったです。
直感的に書くことができて、SFCのtemplateで利用できる変数は、CompositionAPIのCreateComponentでreturnしたオブジェクトに宣言したものだけというのが非常にシンプルでわかりやすい！


## 実数値計算ロジック

コード： https://github.com/inosy22/pokemon-tools/tree/master/src/lib/pokemon

こちらは、ES6のクラスベースでオブジェクト指向っぽく作成。
時短のためTypeScriptにしなかったので、privateプロパティっぽくしてますがなってないのはご愛嬌...。
Nuxt.jsの部分とはあまり関係なく、ポケモンのドメインロジックなので詳細説明は省きます。


## ページの作成

コード: https://github.com/inosy22/pokemon-tools/blob/master/src/pages/speed-checker.vue

Nuxt.jsは `pages` ディレクトリにVueファイルを作成すると、その通りにルーティングされるので、ファイルを作成するだけです。
最初はこのファイルに全てベタ書きしていましたが、冗長な部分があったので、VueComponentの分割を行いました。

全体のレイアウトなどは、Vuetifyの[グリッドシステム](https://vuetifyjs.com/ja/components/grids)を使うことで、簡単にレスポンシブデザインを実現します。

全てのパーツは、VuetifyのForm系のコンポーネントなどを利用して、
[v-model](https://jp.vuejs.org/v2/guide/forms.html) でVueComponentのstateを紐付けることで、リアクティブな処理を実現しています。


## Vueコンポーネント設計

コード： https://github.com/inosy22/pokemon-tools/tree/master/src/components/speed_checker

冗長な機能と見た目を持つものは以下の2種類があったので、ここだけ別Componentに分離しています。

 - CalculatorCard(赤枠)
 - PlusMinusButton(青枠)

<img width="902" alt="スクリーンショット 2019-12-12 3.07.02のコピー2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/8153cdbd-5ab3-05a6-cfd6-950ee28c462b.png">


ラジオボタンとチェックボックスの部分については、シンプルなVuetifyのコンポーネントを使っているだけなので、説明を省略します。


### ポケモンの選択
インクリメンタルサーチで簡単にポケモンを選べるようにします。
これは、Vuetifyの[ComboBoxコンポーネント](https://vuetifyjs.com/ja/components/combobox)を用いることで簡単に作成できます。


実際の利用例

```js
<v-combobox
  v-model="state.pokemonName" // Componentのstateと紐付け
  :items="pokemonsForSearch" // 検索候補の文字列の配列を渡す
  :error="compute.speed.value === '???'" // エラー条件(やばい判定方法なのを今回は見逃してくださいw）
  @click:clear="clearText" // 消すボタンが押された時に実行される関数
  clear-icon="mdi-close-circle" // 消すボタンのアイコンを決める
  clearable // ×ボタンで消せるようにする
  autocomplete="off" // ブラウザのオートコンプリートは消す
  label="ポケモン" // 表示名
  dense // デザイン調整
/>
```

動作はこのような形になります
![6f73a325d8727191b88671af31756b0e.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/6ae4db7a-ad6e-abc7-296d-e99499ac0c76.gif)

今回は、ひらがなでも検索に引っかかるように無理矢理ひらがなも表記するようにしていますが、itemsにオブジェクトの配列を渡して、`text/value` を入れ、フィルター関数などを自前で作ったものを設定すれば、うまくできそうな気がします。
しかし、今回は時間の関係で断念。

## 努力値の入力

努力値は 0 から 252 までの値を取りますが、実数値への影響は4の倍数ごとに発生します。
また、Lv.50の場合は、0 と 8n+4 (0 <= n <= 31, nは整数) の値以外は使うことがありません。

つまり、これもComboBoxコンポーネントで補助してあげることで、入力がしやすくなります。
また、努力値は一番細かく調整したい箇所なので、細かい調整をリアクティブに確認しながら行いやすいように、プラスマイナスボタンもつけることにします。

![c7ec1becacf8c85f4f86de9cc70b117c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/24fd99b4-36a0-569c-6842-2183dad8974b.gif)

プラスマイナスボタンについては、ボタンの有効無効フラグとクリック時に発火する関数を受け取るだけのコンポーネントになっており、同じ見た目で色々な加算減算処理に対応できるようにしています。

アイコンは [Iconコンポーネント](https://vuetifyjs.com/ja/components/icons) を利用していて、[MaterialDesignIcons](https://materialdesignicons.com/)から自由に使うことができます。

```PlusMinusButton.vue
<template>
  <div>
    <v-icon
      @click="props.onClickMinusButton()"
      large
      color="rgba(255, 255, 255, 0.7)"
      class="pt-1"
      style="font-size: 32px"
    >
      {{ props.enableMinusButton ? 'mdi-minus-box' : 'mdi-minus-box-outline' }}
    </v-icon>
    <v-icon
      @click="props.onClickPlusButton()"
      color="rgba(255, 255, 255, 0.7)"
      large
      class="pt-1"
      style="font-size: 32px"
    >
      {{ props.enablePlusButton ? 'mdi-plus-box' : 'mdi-plus-box-outline' }}
    </v-icon>
  </div>
</template>

<script>
import { createComponent } from '@vue/composition-api'

export default createComponent({
  props: {
    enableMinusButton: {
      type: Boolean,
      default: false
    },
    enablePlusButton: {
      type: Boolean,
      default: false
    },
    onClickMinusButton: {
      type: Function,
      default: () => {}
    },
    onClickPlusButton: {
      type: Function,
      default: () => {}
    }
  },
  setup(props, ctx) {
    return {
      props
    }
  }
})
</script>
```

### 補正ランクの選択
補正ランクは、 -6 から +6 までの値のみなので、セレクトボックスを使うことにします。
しかし、セレクトボックスを使うと、1ランクずつ変更して徐々に確かめたい場合に面倒なので、こちらにもプラスマイナスボタンを利用します。

![59fb376036b61c294d4d1c2ba1eb3a59.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/3674c98b-94a2-f618-db71-bac03c169427.gif)

### 素早さ比較結果の表示
親コンポーネントであるページで比較結果を表示します。
自分と相手のそれぞれの `CalculatorCard` で計算された実数値は、
それぞれのコンポーネントが管理しているので、
親コンポーネントに通知する必要があります。

そのため、親コンポーネントから、親のstateに計算結果を連携するコールバック関数を `CalculatorCard` にpropsとして渡してあげます。
`CalculatorCard` はその関数を、実数値の計算が終わった時に実行します。

```js:propsで渡すコールバック関数の宣言
const calculatedOwnSpeed = (speed) => {
  state.ownSpeed = speed
}
const calculatedOpponentSpeed = (speed) => {
  state.opponentSpeed = speed
}
```

```vue:CalculatorCardにコールバック関数を渡す
<v-flex xs12 sm6 class="card-container">
  <CalculatorCard
    :title="`自分のポケモン`"
    :calculated-speed="calculatedOwnSpeed"
  />
</v-flex>
<v-flex xs12 sm6 class="card-container">
  <CalculatorCard
    :title="`相手のポケモン`"
    :calculated-speed="calculatedOpponentSpeed"
  />
</v-flex>
```

`state.ownSpeed` と `state.opponentSpeed` に依存した算出プロパティを作成してあげれば、子コンポーネントの計算結果も親コンポーネントの比較結果にリアクティブに処理されます。

```js:算出プロパティ
const compute = {
  result: computed(() => {
    const result = {
      text: '計測不能',
      color: 'white',
      percentage: 0,
      competition: 0
    }
    if (
      isNaN(state.ownSpeed) ||
      state.ownSpeed === null ||
      isNaN(state.opponentSpeed) ||
      state.opponentSpeed === null
    ) {
      return result
    }
    const ownSpeed = Number(state.ownSpeed)
    const opponentSpeed = Number(state.opponentSpeed)
    if (ownSpeed > opponentSpeed) {
      result.text = '速い！'
      result.color = '#880000'
    } else if (opponentSpeed > ownSpeed) {
      result.text = '遅い...'
      result.color = 'white'
    } else {
      result.text = '同速'
      result.color = 'black'
    }
    result.percentage = Math.round(
      (ownSpeed / (ownSpeed + opponentSpeed)) * 100
    )
    result.competition = Math.floor((ownSpeed / opponentSpeed) * 100) / 100
    return result
  })
}
```

結果の表示部分は、[ProgressLinerコンポーネント](https://vuetifyjs.com/ja/components/progress-linear#slots)を利用しています。
Vueの [Slot](https://jp.vuejs.org/v2/guide/components-slots.html) を使うことで、プログレスバーの中に文字を入れることができるので、結果をそこに記載しています。


```vue:結果表示プログレスバー
    <!-- スマホの場合だけ、固定フッターにする -->
    <v-footer :fixed="$vuetify.breakpoint.xs" width="100%">
      <v-flex xs12 sm12>
        <v-progress-linear
          :value="compute.result.value.percentage"
          color="amber"
          height="40"
          reactive
        >
          <v-row algin="center" justify="space-between">
            <v-col
              style="text-align: left; margin-left: 10px; font-size: 1.2rem; color: black;"
            >
              <strong>{{ state.ownSpeed }}</strong>
            </v-col>
            <v-col style="text-align: center;" cols="6">
              <strong
                :style="
                  `color: ${compute.result.value.color}; font-size: 1.2rem;`
                "
              >
                {{ compute.result.value.text }} ({{
                  compute.result.value.competition
                }}倍)
              </strong>
            </v-col>
            <v-col
              style="text-align: right; margin-right: 10px; font-size: 1.2rem;"
            >
              <strong>{{ state.opponentSpeed }}</strong>
            </v-col>
          </v-row>
        </v-progress-linear>
      </v-flex>
    </v-footer>
```

スマホの場合は `CalculatorCard` が縦に並ぶようにしているため、
結果表示を単純に最下部にしてしまうと、ポチポチ値を変更した際にリアクティブに結果を見ることができなくなってしまいます。

そのため、スマホサイズの場合だけ、結果表示は固定フッターにするという形を取りました。

![0ed4246da55a73c04bf002150af9482d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/f39a8b42-ed35-6386-4d91-45a939bfa3bb.gif)

## OGPの設定
SEO対策やSNSなどでのシェアのためのOGP設定を行います。
`nuxt.config.js` の[headプロパティ](https://ja.nuxtjs.org/api/configuration-head/) を使うことで一括で全ページに入れることができます。

```js:nuxt.config.js
  head: {
    titleTemplate: '%s',
    title: 'ポケモン剣盾素早さ比較ツール',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        name: 'keywords',
        content:
          'ポケモンソードシールド,ポケモン剣盾,ソード,シールド,SW,SH,剣盾,けんたて,ポケモン,ぽけもん,素早さ,素早さ比較,素早さチェッカー'
      },
      {
        hid: 'description',
        name: 'description',
        content:
          '速攻計算！ポケモン剣盾(ポケモンソードシールド)素早さ比較ツール by @inosy22'
      },
      { hid: 'twitter:card', name: 'twitter:card', content: 'summary' },
      { hid: 'twitter:site', name: 'twitter:site', content: '@inosy22' },
      { hid: 'og:type', property: 'og:type', content: 'website' },
      {
        hid: 'og:title',
        property: 'og:title',
        content: 'ポケモン剣盾素早さ比較ツール'
      },
      {
        hid: 'og:url',
        property: 'og:url',
        content: 'https://pokemon-tools.netlify.com'
      },
      {
        hid: 'og:description',
        property: 'og:description',
        content: '速攻計算！ポケモン剣盾用素早さ比較ツール by @inosy22'
      },
      {
        hid: 'og:image',
        property: 'og:image',
        content: 'https://pokemon-tools.netlify.com/img/speed-ball.png'
      },
      {
        hid: 'og:site_name',
        name: 'og:site_name',
        content: 'ポケモン素早さ比較 for 剣盾'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }]
  }
```

※ ページごとに設定する場合は別の方法を使う必要があります
※ SSR(univarsal)や静的ジェネレートであればページごとに設定できますが、SPAモードは全ページ共通以外設定不可です


# 公開
netlifyを利用することで、Githubのmasterブランチに変更にフックしてデプロイが可能です。
https://www.netlify.com/

## Netlifyの利用
Nuxt.jsの公式ドキュメントのFAQにやり方が書いてあります。
https://ja.nuxtjs.org/faq/netlify-deployment/

今回は、静的に作られたサイトで問題ないので、
BuildCommandには `npm run generate` を指定し、PublishDirectoryに `dist` を入力します。

<img width="1158" alt="スクリーンショット 2019-12-12 4.56.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/1e0f1711-ac96-14ff-d3e6-ef1e07588e89.png">


## Domainの変更
`Domain Management` から `ChangeSiteName` を開くことで、
`****.netlify.com` のサブドメイン部分を自由に変更することができます。

<img width="1184" alt="スクリーンショット 2019-12-12 4.50.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/fe69c191-172f-bbff-8161-066d2c0e2889.png">


## GoogleAnaliticsをNetifyで利用する

こちらの記事を参考にさせていただきました！
https://protoout.studio/posts/netlify-snippet-ga

`Build & Deploy` の `Post processing` の設定で、`<head>` タグにGoogleAnaliticsのタグを挿入されるようにします。

<img width="1163" alt="スクリーンショット 2019-12-12 4.52.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44933/b1bc417d-f16f-1448-262e-2f927959d1c5.png">


# さいごに
**Nuxt.js** はアプリケーション構築を簡単に行うことができてかなり便利です！
また、**CompositionAPI** を使うことでシンプルな記述ができたのも実装速度向上につながりました！
一番時間がかかったのはUI周りでしたが、**Vuetify** のおかげでデザイン知識が無いの自分が、コンポーネント単位の細かいデザインを考えなくて良いため、かなりの時間節約になりました！
**Netlify** については設定が10分ぐらいで終わり、CDのことも全く考えなくて良い、さらには無料なのは最高ですね！
加えて、**Nuxt.js** と **Netlify** はかなり相性がいいように感じました！

趣味での簡単なアプリケーション作成であれば、**Nuxt.js + Vuetify + Netlify** の構成、手早く作るのに非常にオススメです！

ポケモンのランク戦やっている方は、ぜひこちらの素早さ比較ツール使ってみてください！
そして余裕があればissueやプルリクもお待ちしております！