【FF14】出現頻度が低い魚が釣れる時間帯を通知してくれるdiscordbotを作った【釣り】

:::note warn
2022年11月28日(月)にHeroku Dynosの無料プランが終了することに伴い、botの運用が停止する予定です。
:::
# FF14と釣り
ファイナルファンタジーXIV(FF14)とは、株式会社スクウェア・エニックスが開発、運営をしているMMORPGゲームです。
MMORPGとは、普通のゲームとは異なりサーバーに常にゲーム世界が存在し、たくさんのプレイヤーが1つの時間と空間を共有してプレイするオンラインゲームです。

そんなFF14には、漁師という職業があり、FF14の広大な世界で釣りをすることができます。
魚が生息している場所に赴き、適した釣り餌を用意して、いろいろな技を駆使して魚を釣り上げ、図鑑を埋めていく…
ちょっとテクニカルなどうぶつの森、みたいな感じです。

各地域には「ヌシ」「オオヌシ」と呼ばれる、特定の条件を満たした場合のみ釣ることができるレアな魚が存在します。
その中には、ゲームの中の天候に出現が依存し、月に数回しか獲得チャンスがない魚が何匹か存在します。漁師を極めようとする猛者たちは、スケジュールとにらめっこしながら世界を旅しています。

そんな魚の希少な出現タイミングを逃さないために、スケジュールを参照できる先人が作った素敵なwebサービスがいくつかあるのですが、実際にそのタイミングを通知してくれるツールは存在しませんでした。自分がプレイするにあたって、通知が欲しいなと感じたので、今回discordbotを使用して実現してみました。

discordbotを選んだ理由としては、以下になります。
・discordはゲームプレイヤーに最も親しまれている通話ツールであり、FF14との親和性が高いと感じた
・Pythonで開発がしたかった
・フロント側をdiscordに依存できるため楽に開発できる

構想→コーディング→デプロイ→記事作成まで、作業したのは約5日程度でした。


# 時間・天候の仕様について
FF14の世界「エオルゼア」にはエオルゼア時間(以下ET)という概念が存在し、ゲームの中の世界はエオルゼア時間で進んでいきます。

ETでの24時間は地球時間(以下LT)での70分に相当します。

| エオルゼア時間(ET)  |  地球時間(LT)|
| ---- | ---- |
| 24時間 |  70分  |
|  8時間  |  23分20秒  |
|  1時間  |  2分55秒|


天候はET8時間ごとに各エリアで別々に変化していきます。
また、ET8時間ごとに朝昼夜の区別があります。
朝：ET 00:00 - 07:59
昼：ET 08:00 - 15:59
夜：ET 16:00 - 23:59

天気予報サイト。ET-LT-天候-朝昼夜の関係性が分かりやすい。

https://eorzea-weather.info/

たとえば、レア魚「紅龍」を釣りたい場合。
紅玉海(The Ruby Sea)というエリアで、天候が夜…雷から朝…曇りになった際の朝に釣ることが可能。
参考：

https://eorzea-weather.info/zones/the-ruby-sea

# 魚が釣れる時刻のリストをつくる

現在のETや各エリアの天候を取得できるパッケージがあるので、これを用いて魚が釣れるLTを算出する

https://pypi.org/project/EorzeaEnv/

1.天候を計算するための基準時間を求める

現在時刻(ET)をもとに、天候が変化してからどれだけのLTが経ったのかを計算する
現在時刻(LT)からそれを引き算し、現在の天候に変化した時刻を基準時間とする
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
    #天候が変わってからLTでどれだけ経過したか
    elapsed_LT = int(Decimal(str((Eorzea_time.hour % 8 * 175 + Eorzea_time.minute * 2.917) / 60)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))

    #現在の天候に変化した時刻を基準時間とする
    base_time = datetime.datetime.now(pytz.timezone('Asia/Tokyo')) - datetime.timedelta(minutes=elapsed_LT)

    #秒以下を0埋め
    base_time = datetime.datetime(base_time.year, base_time.month, base_time.day, base_time.hour, base_time.minute)
```
</div></details>



2.朝・昼・夜のリストを作成する
上記パッケージでは朝・昼・夜の情報を取得できなかったため、現在時刻(ET)をもとに、ゲーム内の朝・昼・夜を判定し、リストを作成する
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
    if Eorzea_time.hour < 8:
        term = 'Morning'
        term_list = ['Morning', 'Day', 'Night']
    elif Eorzea_time.hour < 16:
        term = 'Day'
        term_list = ['Day', 'Night', 'Morning']
    else:
        term = 'Night'
        term_list = ['Night', 'Morning', 'Day']

    #朝昼夜のリスト作成
    term_list = term_list * int(step / 3)

    #天候の取得時、0番目は1つ前の天候が格納されるため、それに対応
    term_list.insert(0, 'term0')
```

</div></details>

3.釣れる時刻の計算
天候のリストを取得する
魚が釣れる条件をif節に記載し、現在の天候が始まったLTと、そこからの朝・昼・夜のリストを用いて釣れる時間を算出、リストに格納する
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
    #天候の取得
    t = tuple(EorzeaTime.weather_period(step=step))

    dates=[]

    #検索(紅龍)...i分後
    if fish == 'The Ruby Dragon':
        weather = EorzeaWeather.forecast('The Ruby Sea', t, strict=True)

        for i in range(step):
            if weather[i-1] == 'Thunder' and weather[i] == 'Clouds' and term_list[i] == 'Morning':

                d, h, m = calc_minute_after(i, term)

                date = base_time + datetime.timedelta(days=d,hours=h,minutes=m)

                dates.append(date)

        return dates
```
```python:schedule_calc.py
def calc_minute_after(i, term):
    #ET8時間は23分20秒だが、簡略化するために朝23/昼23/夜24で計算する
    calc_minutes = (i - 1) * 23 + (i - 1) // 3 

    #計算スタートが昼・夜だった場合1分調整
    if term == 'Day' or term == 'Night':
        calc_minutes += 1

    d = calc_minutes // 60 // 24
    h = calc_minutes // 60 % 24
    m = calc_minutes % 60
    return d, h, m
```

</div></details>


# discordbotに組み込む

今回実装した機能は3つです。
①通知機能…メインの機能です。魚が釣れるタイミングの30分前/15分前/5分前に通知してくれます。
![通知.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128908/329fd3fd-c751-0e8d-36b7-31d4f2528de0.png)

60秒に1度ループし、実装されている魚が次に釣れる時刻と現在時刻(LT)の30分前/15分前/5分前を突き合わせ、一致した場合、該当の魚のロールが付与されたユーザー向けにメッセージを送信します。
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
# 60秒に一回ループ
@tasks.loop(seconds=60)
async def loop():

    # 現在の時刻
    now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))
    #秒以下を0埋め
    now = datetime.datetime(now.year, now.month, now.day, now.hour, now.minute)

    #テスト用
    #now = datetime.datetime(2021, 10, 5, 10, 13)
    print(now)

    channel = client.get_channel(CHANNEL_ID_MENTION)

    async def send_Notice(fishing_date, role_ID, minutes_ago):
        date_ago = fishing_date - datetime.timedelta(minutes=minutes_ago)

        #現在時刻がx分前だった場合に通知
        if now == date_ago:
            await channel.send('<@&' + str(role_ID) + '> ' + str(minutes_ago) + '分前\n' + fishing_date.strftime('%Y/%m/%d (%a) %H:%M'))    


    for value in fish_dict.values():
        
        fishing_date = schedule_calc(value['name_EN'])[0]

        #30分前
        await send_Notice(fishing_date, value['role_ID'], 30)
        #15分前
        await send_Notice(fishing_date, value['role_ID'], 15)
        #5分前
        await send_Notice(fishing_date, value['role_ID'], 5)
```
</div></details>


②通知のON/OFF機能…プリフィックス(!) + 魚の名前で通知を有効化/無効化することができます。
![登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128908/f630e452-f17e-22a1-d883-17479cfe2dd9.png)
discordのロール機能を用いて、ロールの付与/剥奪を切り替えています。
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
# メッセージ受信時に動作する処理
@client.event
async def on_message(message):

    guild = client.get_guild(SERVER_ID)

    #登録
    if message.channel.id == CHANNEL_ID_REGISTER:

        for v in fish_dict.values():

            if message.content == '!' + v['name_JA']:
                role = guild.get_role(v['role_ID'])
                
                if role in message.author.roles:
                    await message.author.remove_roles(role)
                    await message.reply(v['name_JA'] + 'のタイマーをOFFにしました')
                else:
                    await message.author.add_roles(role)
                    await message.reply(v['name_JA'] + 'のタイマーをONにしました')
```
</div></details>

③スケジュール確認機能…プリフィックス(!) + 魚の名前で直近のスケジュールを確認することができます。
![スケジュール.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2128908/1e22406b-7708-2c27-4188-d193d984324a.png)
先述した魚が釣れる時刻のリストをメッセージとして出力しています。
<details><summary>ソースコード(クリックで展開)</summary><div>

```python:schedule_calc.py
# メッセージ受信時に動作する処理
@client.event
async def on_message(message):

    guild = client.get_guild(SERVER_ID)

    #スケジュール
    if message.channel.id == CHANNEL_ID_SCHEDULE:

        for v in fish_dict.values():

            if message.content == '!' + v['name_JA']:
                msg = v['name_JA'] + ' スケジュール\n'
                msg += '```'
                fishing_dates = schedule_calc(v['name_EN'])

                for fishing_date in fishing_dates:
                    #TODO Herokuに日本語のロケールがないので、曜日を日本語にするなら独自の関数を実行する必要がある
                    msg += fishing_date.strftime('%Y/%m/%d(%a) %H:%M') + '\n'
            
                msg += '```'
                await message.reply(msg)
```
</div></details>


# 今後の課題
使用方法の実装
スケジュールの曜日を日本語に(ロケールの関係でめんどくさい)
ヘルプ機能の実装(実際の魚の釣り方の簡易的なガイドメッセージを出したい)
出現頻度が低い魚を実装
プリフィックス(!) + 魚の名前以外のメッセージを即時削除する
一定時間経過でメッセージが消えるようにする

# ソースコード
https://github.com/6futjs9g9xi2r/xiv_fish_assistant/tree/master


#botが稼働しているdiscordサーバー
下記リンクからサーバーにJoinすることができます。

https://discord.gg/R3qmDYDzbM

#連絡先
なにかあれば下記連絡先までお気軽にお問い合わせください。
6futjs9g9xi2r(at)gmail.com
6futjs9g9xi2r#5579

# FF14プレイヤー向け
2021/10/7現在以下の3匹のタイマーが実装されています。
紅龍
イラッド・スカーン
サプライズエッグ

今後uptimeが1%以下の魚は順次実装していく予定です。

私自身がヌシ釣りの経験が浅いので、サーバーに参加して意見をいただけると幸いです。よろしくお願いいたします。

https://discord.gg/R3qmDYDzbM