LoginSignup
8
2

More than 1 year has passed since last update.

原神のステータスを取得するDiscordBotを作った話

Posted at

こんにちは.初投稿です.
めっちゃわかりにくいとは思いますが、部分的にでも参考になれば幸いです.

原神っていろんなステータスがあるんですけど、
確認するときっていちいち原神を開かないといけないんですよね.

HoYoLabという公式ツールもあるのですが、
それで確認できるのはほんとに最小限という感じで、聖遺物のステータスなどは確認できません.

なのでDiscordから聖遺物を確認できるBotを作ってみました.
botを招待して確認したい方はこちらからどうぞ.
https://discord.com/api/oauth2/authorize?client_id=1014120722404220998&permissions=2147485696&scope=bot

image.png
こんな感じです.
image.png
キャラ情報も取得できます.
武器の詳細については現在作成中です.

今回作るbotの材料

原神のステータスを取得するためのAPIには、
ステータスを一発で画像にしてくれると有名な、
https://enka.network/
こちらのサイトを利用させていただきました.
画像などもこちらのサイトから取得できるので便利です.

botにはPycordを使います.

python3 -m pip install pycord

でインストールします.
一応バージョンは2.0.0を使用します.

コードを書く

とりあえずみんな大好き(?)なCogを使っていきます.

Cogというのは私もよくわかっていないのですが、
たくさんのコマンドをまとめられるやつだと思っています...
Cogsフォルダにコマンドを書いたpythonファイルを突っ込むと動きます.
(Cogについてよくわかれば追記します.)

まずはmainのやつから

main.py
from discord.ext import commands

bot = commands.Bot()
path = "./cogs"

@bot.event
async def on_application_command_error(ctx, error):
    if isinstance(error, commands.CommandOnCooldown):
        await ctx.respond(error)
    if isinstance(error, commands.MissingPermissions):
        await ctx.respond(content="BOT管理者限定コマンドです", ephemeral=True)

@bot.event
async def on_ready():
    print(f"Bot名:{bot.user} On ready!!")

bot.load_extension('cogs.genshin', store=False)

bot.run("TOKEN")

最後の「TOKEN」にはbotのトークンを貼り付けてください.
botの作成の仕方は今回は省略とさせていただきます。

こちらのmain.pyと同じディレクトリに「cogs」というフォルダを作成してください.
その中に、「genshin.py」というファイルを作成します.

ファイル名を変える場合には、main.pyの

bot.load_extension('cogs.genshin')

の部分を

bot.load_extension('cogs.任意のファイル名.py')

としてください.

genshin.py
import discord
from discord.ui import Select,View,Button
from discord.ext import commands
from discord import Option, SlashCommandGroup
import aiohttp
from lib.yamlutil import yaml
import lib.getStat as getStat
from typing import List

dataYaml = yaml(path='genshin_avater.yaml')
data = dataYaml.load_yaml()
charactersYaml = yaml(path='characters.yaml')
characters = charactersYaml.load_yaml()
genshinJpYaml = yaml(path='genshinJp.yaml')
genshinJp = genshinJpYaml.load_yaml()
l: list[discord.SelectOption] = []

class TicTacToeButton(discord.ui.Button["TicTacToe"]):
    def __init__(self, label: str, uid: str, dict):
        super().__init__(style=discord.ButtonStyle.secondary, label=label)
        self.dict = dict
        self.uid = uid

    async def callback(self, interaction: discord.Interaction):
        assert self.view is not None
        view: TicTacToe = self.view

        self.style = discord.ButtonStyle.success
        content = self.label
        #ラベル(名前)からIDを割り出す
        #多分「名前:iD」ってなってるはず
        id = self.dict[self.label]
        print(interaction.user.id)
        for child in self.view.children:
            child.style = discord.ButtonStyle.gray
        await interaction.response.edit_message(content=content, embed=await getStat.get(self.uid, id), view=None)

class TicTacToe(discord.ui.View):
    children: List[TicTacToeButton]

    def __init__(self, data, uid):
        super().__init__(timeout=190)
        names = []
        dict = {}
        #入ってきたidを名前にしてリスト化
        for id in data:
            id = str(id)
            print(id)
            name = characters[id]["NameId"]
            print(name)
            name = genshinJp[name]
            print(name)
            names.append(name)
            dict.update({name: id})
            print(dict)
        #名前をラベル、ついでにdictとuidも送り付ける
        for v in names:
            self.add_item(TicTacToeButton(v,uid,dict))

class GenshinCog(commands.Cog):

    def __init__(self, bot):
        print('genshin初期化')
        self.bot = bot

    async def getApi(self,uid):
        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
        try:
            embed = discord.Embed( 
                                title=f"{resp['playerInfo']['nickname']}の原神ステータス",
                                color=0x1e90ff, 
                                description=f"uid: {uid}", 
                                url=url 
                                )
                                
            hoge = data[resp['playerInfo']['profilePicture']['avatarId']]['iconName']
            embed.set_thumbnail(url=f"https://enka.network/ui/{hoge}.png")
            try:
                embed.add_field(inline=False,name="ステータスメッセージ",value=resp['playerInfo']['signature'])
            except:
                print("hoge")
            embed.add_field(inline=False,name="冒険ランク",value=resp['playerInfo']['level'])
            embed.add_field(inline=False,name="世界ランク",value=resp['playerInfo']['worldLevel'])
            embed.add_field(inline=False,name="深境螺旋",value=f"{resp['playerInfo']['towerFloorIndex']}層 第{resp['playerInfo']['towerLevelIndex']}")
            return embed
        except:
            embed = discord.Embed( 
                    title=f"エラーが発生しました。APIを確認してからもう一度お試しください。\n{url}",
                    color=0x1e90ff, 
                    url=url 
                    )
            return embed

    genshin = SlashCommandGroup('genshinstat', 'test')

    @genshin.command(name="get", description="UIDからキャラ情報を取得します")
    async def genshin_get(
            self,
            ctx: discord.ApplicationContext,
            uid: Option(str, required=True, description='UIDを入力してください.'),
    ):
        await ctx.respond(content="アカウント情報読み込み中...", ephemeral=True)  
        embed = await GenshinCog.getApi(self,uid)
        await ctx.respond(content="キャラ情報読み込み中...", ephemeral=True)  

        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
                resalt = []
        for id in resp["playerInfo"]["showAvatarInfoList"]:
            resalt.append(id["avatarId"])
        await ctx.respond(content=None,embed=embed,view=TicTacToe(resalt,uid))

def setup(bot):
    bot.add_cog(GenshinCog(bot))

くっそ長いですね.
長すぎるので細かくは紹介しません.
多分ほんとはもっと省略できそうですが、私の技術力ではできませんでした...
簡単に解説していきます.

コマンド部分

    @genshin.command(name="get", description="UIDからキャラ情報を取得します")
    async def genshin_get(
            self,
            ctx: discord.ApplicationContext,
            uid: Option(str, required=True, description='UIDを入力してください.'),
    ):
        await ctx.respond(content="アカウント情報読み込み中...", ephemeral=True)  
        embed = await GenshinCog.getApi(self,uid)
        await ctx.respond(content="キャラ情報読み込み中...", ephemeral=True)  

        url = f"https://enka.network/u/{uid}/__data.json"
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                resp = await response.json()
                resalt = []
        for id in resp["playerInfo"]["showAvatarInfoList"]:
            resalt.append(id["avatarId"])
        await ctx.respond(content=None,embed=embed,view=TicTacToe(resalt,uid))

ここでスラッシュコマンドを定義しておきます.
やってることは単純で、
 ①まずrespond(返信)をする(読み込みが長くタイムアウトしてしまう可能性があるため)
 ②アカウントの情報(レベルなど)をembedとして取得
 ③そのアカウントの持っているキャラ情報の取得
です.
キャラ情報については、キャラのIDをリストとして、ボタンの処理について書かれたクラスにUIDと一緒に渡します.

class TicTacToe(discord.ui.View):
    children: List[TicTacToeButton]

    def __init__(self, data, uid):
        super().__init__(timeout=190)
        names = []
        dict = {}
        #入ってきたidを名前にしてリスト化
        for id in data:
            id = str(id)
            name = genshinJp[characters[str(id)]["NameId"]]
            names.append(name)
            dict.update({name: id})
        #名前をラベル、ついでにdictとuidも送り付ける
        for v in names:
            self.add_item(TicTacToeButton(v,uid,dict))

こちらはボタン部分です.
Discordで見るとこのようになっていますね.
image.png
ここから見たいキャラを選ぶ感じです.

ここにはリストになったキャラのidが突っ込まれてくるので、
idからキャラ名を取得
日本語化して表示しています.
また、キャラ詳細でidが必要なので、一緒に辞書型としてボタンの処理部分に丸投げします.

class TicTacToeButton(discord.ui.Button["TicTacToe"]):
    def __init__(self, label: str, uid: str, dict):
        super().__init__(style=discord.ButtonStyle.secondary, label=label)
        self.dict = dict
        self.uid = uid

    async def callback(self, interaction: discord.Interaction):
        assert self.view is not None
        view: TicTacToe = self.view

        self.style = discord.ButtonStyle.success
        content = self.label
        #ラベル(名前)からIDを割り出す
        #多分「名前:iD」ってなってるはず
        id = self.dict[self.label]
        print(interaction.user.id)
        for child in self.view.children:
            child.style = discord.ButtonStyle.gray
        await interaction.response.edit_message(content=content, embed=await getStat.get(self.uid, id), view=None)

ボタンの処理部分です.
さっきのボタンの部分にidがくっついているので、押されたボタンのラベル(キャラ名)に関連したidを、キャラ詳細を表示するbotにuidと一緒に丸投げします.

async def get(uid,id):
    url = f"https://enka.network/u/{uid}/__data.json"
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            resp = await response.json()
    name = characters[id]["NameId"]
    name = genshinJp[name]
    print(id)
    id = int(id)
    try:
        for n in resp['avatarInfoList']:
            if n["avatarId"] == id:
                chara = n
                print("hogehogheogheohgpoihvgp;ogiazwqp;oabwo")
                break
            else:
                continue
        for n in resp['playerInfo']["showAvatarInfoList"]:
            print(n["avatarId"])
            if n["avatarId"] == id:
                level = n["level"]
                print("hogehogehoge")
                break
            else:
                continue
    except:
        embed = discord.Embed( 
            title="エラー",
            color=0x1e90ff,
            description=f"キャラ詳細が非公開です。原神の設定で公開設定にしてください。", 
            url=url 
        )
        return embed
    try:
        embed = discord.Embed( 
                    title=name,
                    color=0x1e90ff, 
                    description=f"{level}lv", 
                    url=url 
                    )
        hoge = characters[str(id)]["sideIconName"]
        embed.set_thumbnail(url=f"https://enka.network/ui/{hoge}.png")
        embed.add_field(inline=True,name="キャラレベル",value=f"{level}lv")
        embed.add_field(inline=True,name="キャラ突破レベル",value=str(chara["propMap"]["1002"]["ival"]))
        embed.add_field(inline=True,name="HP",
            value=f'{str(round(chara["fightPropMap"]["1"]))} + {str(round(chara["fightPropMap"]["2000"]) - round(chara["fightPropMap"]["1"]))} = {str(round(chara["fightPropMap"]["2000"]))}'
        )
        embed.add_field(inline=True,name="攻撃力",
            value=f'{str(round(chara["fightPropMap"]["4"]))} + {str(round(chara["fightPropMap"]["2001"]) - round(chara["fightPropMap"]["4"]))} = {str(round(chara["fightPropMap"]["2001"]))}'
        )
        embed.add_field(inline=True,name="防御力",
            value=f'{str(round(chara["fightPropMap"]["7"]))} + {str(round(chara["fightPropMap"]["2002"]) - round(chara["fightPropMap"]["7"]))} = {str(round(chara["fightPropMap"]["2002"]))}'
        )
        embed.add_field(inline=True,name="会心率",
            value=f'{str(round(chara["fightPropMap"]["20"] *100))}%'
        )
        embed.add_field(inline=True,name="会心ダメージ",
            value=f'{str(round(chara["fightPropMap"]["22"]*100))}%'
        )
        embed.add_field(inline=True,name="元素チャージ効率",
            value=f'{str(round(chara["fightPropMap"]["23"]*100))}%'
        )
        embed.add_field(inline=True,name="元素熟知",
            value=f'{str(round(chara["fightPropMap"]["28"]))}'
        )
        buf = 1
        if round(chara["fightPropMap"]["30"]*100) > 0:
            embed.add_field(inline=True,name="物理ダメージ",
                value=f'{str(round(chara["fightPropMap"]["30"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["30"])
        elif round(chara["fightPropMap"]["40"]*100) > 0:
            embed.add_field(inline=True,name="炎元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["40"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["40"])
        elif round(chara["fightPropMap"]["41"]*100) > 0:
            embed.add_field(inline=True,name="雷元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["41"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["41"])
        elif round(chara["fightPropMap"]["42"]*100) > 0:
            embed.add_field(inline=True,name="水元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["42"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["42"])
        elif round(chara["fightPropMap"]["43"]*100) > 0:
            embed.add_field(inline=True,name="草元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["43"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["42"])
        elif round(chara["fightPropMap"]["44"]*100) > 0:
            embed.add_field(inline=True,name="風元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["44"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["44"])
        elif round(chara["fightPropMap"]["45"]*100) > 0:
            embed.add_field(inline=True,name="岩元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["45"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["45"])
        elif round(chara["fightPropMap"]["46"]*100) > 0:
            embed.add_field(inline=True,name="氷元素ダメージ",
                value=f'{str(round(chara["fightPropMap"]["46"]*100))}%'
            )
            buf += round(chara["fightPropMap"]["46"])
        temp = []
        for myvalue in chara["skillLevelMap"].values():
            temp.append(f"{myvalue}")
        embed.add_field(inline=False,name="天賦レベル",
            value="\n".join(temp)
        )
        for n in chara["equipList"]:
            name = genshinJp[n["flat"]["setNameTextMapHash"]]
            equip = genshinJp[n["flat"]["equipType"]]
            main = genshinJp[n["flat"]["reliquaryMainstat"]["mainPropId"]]
            hoge = []
            for b in n["flat"]["reliquarySubstats"]:
                name_ = genshinJp[b["appendPropId"]]
                value_ = b["statValue"]
                hoge.append(f"{name_}{value_}")
            embed.add_field(inline=True,name=f'聖遺物:{equip}\n{name}\n{main}{n["flat"]["reliquaryMainstat"]["statValue"]}\n{n["reliquary"]["level"]-1}lv\n',
                value="\n".join(hoge)
            )
        return embed
    except KeyError:
        return embed

キャラ詳細表示部分です.
main.pyのディレクトリlibというフォルダを作り、その中にgetStat.pyというファイルを入れています.

こちらは長いだけなので特に複雑な処理はしていません.
丸投げされたUIDとキャラIDから、その人のキャラ詳細のJsonが帰ってくるので
それを分解して表示するだけです.

武器なども表示できると思いますが、時間がなくてまだできていません.
今後追記すると思います.

おわりに

以上となります。
多分これから修正点がたくさん見つかると思いますが、随時追記していきます.
Qiitaを書くのは初めてなのでわかりずらいとは思いますが、少しでも参考になれば幸いです.
また、手伝っていただいたにかわみかんさん、ありがとうございました。

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2