From aa7a8271f398354ba887576507c25d3c29b0f34a Mon Sep 17 00:00:00 2001
From: HibiKier <775757368@qq.com>
Date: Sun, 28 Jul 2024 03:37:37 +0800
Subject: [PATCH] =?UTF-8?q?feat=E2=9C=A8:=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?=
=?UTF-8?q?=E6=88=8F=E6=8A=BD=E5=8D=A1=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
poetry.lock | 390 ++++++++++++++-
pyproject.toml | 3 +
zhenxun/plugins/draw_card/__init__.py | 289 +++++++++++
zhenxun/plugins/draw_card/config.py | 203 ++++++++
zhenxun/plugins/draw_card/count_manager.py | 149 ++++++
.../plugins/draw_card/handles/azur_handle.py | 307 ++++++++++++
.../plugins/draw_card/handles/ba_handle.py | 153 ++++++
.../plugins/draw_card/handles/base_handle.py | 296 +++++++++++
.../plugins/draw_card/handles/fgo_handle.py | 223 +++++++++
.../draw_card/handles/genshin_handle.py | 465 ++++++++++++++++++
.../draw_card/handles/guardian_handle.py | 399 +++++++++++++++
.../draw_card/handles/onmyoji_handle.py | 178 +++++++
.../plugins/draw_card/handles/pcr_handle.py | 149 ++++++
.../draw_card/handles/pretty_handle.py | 422 ++++++++++++++++
.../plugins/draw_card/handles/prts_handle.py | 343 +++++++++++++
zhenxun/plugins/draw_card/rule.py | 10 +
zhenxun/plugins/draw_card/util.py | 61 +++
17 files changed, 4039 insertions(+), 1 deletion(-)
create mode 100644 zhenxun/plugins/draw_card/__init__.py
create mode 100644 zhenxun/plugins/draw_card/config.py
create mode 100644 zhenxun/plugins/draw_card/count_manager.py
create mode 100644 zhenxun/plugins/draw_card/handles/azur_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/ba_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/base_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/fgo_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/genshin_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/guardian_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/onmyoji_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/pcr_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/pretty_handle.py
create mode 100644 zhenxun/plugins/draw_card/handles/prts_handle.py
create mode 100644 zhenxun/plugins/draw_card/rule.py
create mode 100644 zhenxun/plugins/draw_card/util.py
diff --git a/poetry.lock b/poetry.lock
index 4c1c1694..c526dd27 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -16,6 +16,126 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "aiohttp"
+version = "3.9.5"
+description = "Async http client/server framework (asyncio)"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcde4c397f673fdec23e6b05ebf8d4751314fa7c24f93334bf1f1364c1c69ac7"},
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d6b3f1fabe465e819aed2c421a6743d8debbde79b6a8600739300630a01bf2c"},
+ {file = "aiohttp-3.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ae79c1bc12c34082d92bf9422764f799aee4746fd7a392db46b7fd357d4a17a"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d3ebb9e1316ec74277d19c5f482f98cc65a73ccd5430540d6d11682cd857430"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84dabd95154f43a2ea80deffec9cb44d2e301e38a0c9d331cc4aa0166fe28ae3"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a02fbeca6f63cb1f0475c799679057fc9268b77075ab7cf3f1c600e81dd46b"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c26959ca7b75ff768e2776d8055bf9582a6267e24556bb7f7bd29e677932be72"},
+ {file = "aiohttp-3.9.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:714d4e5231fed4ba2762ed489b4aec07b2b9953cf4ee31e9871caac895a839c0"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7a6a8354f1b62e15d48e04350f13e726fa08b62c3d7b8401c0a1314f02e3558"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c413016880e03e69d166efb5a1a95d40f83d5a3a648d16486592c49ffb76d0db"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ff84aeb864e0fac81f676be9f4685f0527b660f1efdc40dcede3c251ef1e867f"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ad7f2919d7dac062f24d6f5fe95d401597fbb015a25771f85e692d043c9d7832"},
+ {file = "aiohttp-3.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:702e2c7c187c1a498a4e2b03155d52658fdd6fda882d3d7fbb891a5cf108bb10"},
+ {file = "aiohttp-3.9.5-cp310-cp310-win32.whl", hash = "sha256:67c3119f5ddc7261d47163ed86d760ddf0e625cd6246b4ed852e82159617b5fb"},
+ {file = "aiohttp-3.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:471f0ef53ccedec9995287f02caf0c068732f026455f07db3f01a46e49d76bbb"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e0ae53e33ee7476dd3d1132f932eeb39bf6125083820049d06edcdca4381f342"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c088c4d70d21f8ca5c0b8b5403fe84a7bc8e024161febdd4ef04575ef35d474d"},
+ {file = "aiohttp-3.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:639d0042b7670222f33b0028de6b4e2fad6451462ce7df2af8aee37dcac55424"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f26383adb94da5e7fb388d441bf09c61e5e35f455a3217bfd790c6b6bc64b2ee"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66331d00fb28dc90aa606d9a54304af76b335ae204d1836f65797d6fe27f1ca2"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ff550491f5492ab5ed3533e76b8567f4b37bd2995e780a1f46bca2024223233"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f22eb3a6c1080d862befa0a89c380b4dafce29dc6cd56083f630073d102eb595"},
+ {file = "aiohttp-3.9.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a81b1143d42b66ffc40a441379387076243ef7b51019204fd3ec36b9f69e77d6"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f64fd07515dad67f24b6ea4a66ae2876c01031de91c93075b8093f07c0a2d93d"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:93e22add827447d2e26d67c9ac0161756007f152fdc5210277d00a85f6c92323"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:55b39c8684a46e56ef8c8d24faf02de4a2b2ac60d26cee93bc595651ff545de9"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4715a9b778f4293b9f8ae7a0a7cef9829f02ff8d6277a39d7f40565c737d3771"},
+ {file = "aiohttp-3.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:afc52b8d969eff14e069a710057d15ab9ac17cd4b6753042c407dcea0e40bf75"},
+ {file = "aiohttp-3.9.5-cp311-cp311-win32.whl", hash = "sha256:b3df71da99c98534be076196791adca8819761f0bf6e08e07fd7da25127150d6"},
+ {file = "aiohttp-3.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:88e311d98cc0bf45b62fc46c66753a83445f5ab20038bcc1b8a1cc05666f428a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c7a4b7a6cf5b6eb11e109a9755fd4fda7d57395f8c575e166d363b9fc3ec4678"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a158704edf0abcac8ac371fbb54044f3270bdbc93e254a82b6c82be1ef08f3c"},
+ {file = "aiohttp-3.9.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d153f652a687a8e95ad367a86a61e8d53d528b0530ef382ec5aaf533140ed00f"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a6a97d9771cb48ae16979c3a3a9a18b600a8505b1115cfe354dfb2054468b4"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60cdbd56f4cad9f69c35eaac0fbbdf1f77b0ff9456cebd4902f3dd1cf096464c"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8676e8fd73141ded15ea586de0b7cda1542960a7b9ad89b2b06428e97125d4fa"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da00da442a0e31f1c69d26d224e1efd3a1ca5bcbf210978a2ca7426dfcae9f58"},
+ {file = "aiohttp-3.9.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18f634d540dd099c262e9f887c8bbacc959847cfe5da7a0e2e1cf3f14dbf2daf"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:320e8618eda64e19d11bdb3bd04ccc0a816c17eaecb7e4945d01deee2a22f95f"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2faa61a904b83142747fc6a6d7ad8fccff898c849123030f8e75d5d967fd4a81"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:8c64a6dc3fe5db7b1b4d2b5cb84c4f677768bdc340611eca673afb7cf416ef5a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:393c7aba2b55559ef7ab791c94b44f7482a07bf7640d17b341b79081f5e5cd1a"},
+ {file = "aiohttp-3.9.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c671dc117c2c21a1ca10c116cfcd6e3e44da7fcde37bf83b2be485ab377b25da"},
+ {file = "aiohttp-3.9.5-cp312-cp312-win32.whl", hash = "sha256:5a7ee16aab26e76add4afc45e8f8206c95d1d75540f1039b84a03c3b3800dd59"},
+ {file = "aiohttp-3.9.5-cp312-cp312-win_amd64.whl", hash = "sha256:5ca51eadbd67045396bc92a4345d1790b7301c14d1848feaac1d6a6c9289e888"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:694d828b5c41255e54bc2dddb51a9f5150b4eefa9886e38b52605a05d96566e8"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0605cc2c0088fcaae79f01c913a38611ad09ba68ff482402d3410bf59039bfb8"},
+ {file = "aiohttp-3.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4558e5012ee03d2638c681e156461d37b7a113fe13970d438d95d10173d25f78"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbc053ac75ccc63dc3a3cc547b98c7258ec35a215a92bd9f983e0aac95d3d5b"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4109adee842b90671f1b689901b948f347325045c15f46b39797ae1bf17019de"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6ea1a5b409a85477fd8e5ee6ad8f0e40bf2844c270955e09360418cfd09abac"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3c2890ca8c59ee683fd09adf32321a40fe1cf164e3387799efb2acebf090c11"},
+ {file = "aiohttp-3.9.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3916c8692dbd9d55c523374a3b8213e628424d19116ac4308e434dbf6d95bbdd"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8d1964eb7617907c792ca00b341b5ec3e01ae8c280825deadbbd678447b127e1"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5ab8e1f6bee051a4bf6195e38a5c13e5e161cb7bad83d8854524798bd9fcd6e"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52c27110f3862a1afbcb2af4281fc9fdc40327fa286c4625dfee247c3ba90156"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7f64cbd44443e80094309875d4f9c71d0401e966d191c3d469cde4642bc2e031"},
+ {file = "aiohttp-3.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b4f72fbb66279624bfe83fd5eb6aea0022dad8eec62b71e7bf63ee1caadeafe"},
+ {file = "aiohttp-3.9.5-cp38-cp38-win32.whl", hash = "sha256:6380c039ec52866c06d69b5c7aad5478b24ed11696f0e72f6b807cfb261453da"},
+ {file = "aiohttp-3.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:da22dab31d7180f8c3ac7c7635f3bcd53808f374f6aa333fe0b0b9e14b01f91a"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1732102949ff6087589408d76cd6dea656b93c896b011ecafff418c9661dc4ed"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c6021d296318cb6f9414b48e6a439a7f5d1f665464da507e8ff640848ee2a58a"},
+ {file = "aiohttp-3.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:239f975589a944eeb1bad26b8b140a59a3a320067fb3cd10b75c3092405a1372"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b7b30258348082826d274504fbc7c849959f1989d86c29bc355107accec6cfb"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd2adf5c87ff6d8b277814a28a535b59e20bfea40a101db6b3bdca7e9926bc24"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a3d838441bebcf5cf442700e3963f58b5c33f015341f9ea86dcd7d503c07e2"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e3a1ae66e3d0c17cf65c08968a5ee3180c5a95920ec2731f53343fac9bad106"},
+ {file = "aiohttp-3.9.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c69e77370cce2d6df5d12b4e12bdcca60c47ba13d1cbbc8645dd005a20b738b"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf56238f4bbf49dab8c2dc2e6b1b68502b1e88d335bea59b3f5b9f4c001475"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d1469f228cd9ffddd396d9948b8c9cd8022b6d1bf1e40c6f25b0fb90b4f893ed"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:45731330e754f5811c314901cebdf19dd776a44b31927fa4b4dbecab9e457b0c"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3fcb4046d2904378e3aeea1df51f697b0467f2aac55d232c87ba162709478c46"},
+ {file = "aiohttp-3.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8cf142aa6c1a751fcb364158fd710b8a9be874b81889c2bd13aa8893197455e2"},
+ {file = "aiohttp-3.9.5-cp39-cp39-win32.whl", hash = "sha256:7b179eea70833c8dee51ec42f3b4097bd6370892fa93f510f76762105568cf09"},
+ {file = "aiohttp-3.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:38d80498e2e169bc61418ff36170e0aad0cd268da8b38a17c4cf29d254a8b3f1"},
+ {file = "aiohttp-3.9.5.tar.gz", hash = "sha256:edea7d15772ceeb29db4aff55e482d4bcfb6ae160ce144f2682de02f6d693551"},
+]
+
+[package.dependencies]
+aiosignal = ">=1.1.2"
+async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""}
+attrs = ">=17.3.0"
+frozenlist = ">=1.1.1"
+multidict = ">=4.5,<7.0"
+yarl = ">=1.0,<2.0"
+
+[package.extras]
+speedups = ["Brotli", "aiodns", "brotlicffi"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
+[[package]]
+name = "aiosignal"
+version = "1.3.1"
+description = "aiosignal: a list of registered asynchronous callbacks"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"},
+ {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"},
+]
+
+[package.dependencies]
+frozenlist = ">=1.1.0"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "aiosqlite"
version = "0.17.0"
@@ -585,6 +705,26 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "cn2an"
+version = "0.5.22"
+description = "Convert Chinese numerals and Arabic numerals."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "cn2an-0.5.22-py3-none-any.whl", hash = "sha256:cba4c8f305b43da01f50696047cca3116c727424ac62338da6a3426e01454f3e"},
+ {file = "cn2an-0.5.22.tar.gz", hash = "sha256:27ae5b56441d7329ed2ececffa026bfa8fc353dcf1fb0d9146b303b9cce3ac37"},
+]
+
+[package.dependencies]
+proces = ">=0.1.3"
+setuptools = ">=47.3.1"
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "colorama"
version = "0.4.6"
@@ -627,6 +767,33 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "dateparser"
+version = "1.2.0"
+description = "Date parsing library designed to parse dates from HTML pages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830"},
+ {file = "dateparser-1.2.0.tar.gz", hash = "sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"},
+]
+
+[package.dependencies]
+python-dateutil = "*"
+pytz = "*"
+regex = "<2019.02.19 || >2019.02.19,<2021.8.27 || >2021.8.27"
+tzlocal = "*"
+
+[package.extras]
+calendars = ["convertdate", "hijri-converter"]
+fasttext = ["fasttext"]
+langdetect = ["langdetect"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "distlib"
version = "0.3.8"
@@ -745,6 +912,97 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "frozenlist"
+version = "1.4.1"
+description = "A list-like structure which implements collections.abc.MutableSequence"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"},
+ {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"},
+ {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"},
+ {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"},
+ {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"},
+ {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"},
+ {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"},
+ {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"},
+ {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"},
+ {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"},
+ {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"},
+ {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"},
+ {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"},
+ {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"},
+ {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"},
+ {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"},
+ {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"},
+ {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"},
+ {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"},
+ {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"},
+ {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"},
+ {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"},
+ {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"},
+ {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"},
+ {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"},
+ {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"},
+ {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"},
+ {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"},
+ {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"},
+ {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"},
+ {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"},
+ {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"},
+ {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"},
+ {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"},
+ {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"},
+ {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"},
+ {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"},
+ {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"},
+ {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"},
+ {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"},
+ {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"},
+ {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"},
+ {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"},
+ {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"},
+ {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"},
+ {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"},
+ {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"},
+ {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"},
+ {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"},
+ {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"},
+ {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"},
+ {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"},
+ {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"},
+ {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"},
+ {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"},
+ {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"},
+ {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"},
+ {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"},
+ {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"},
+ {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"},
+ {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"},
+ {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"},
+ {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"},
+ {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"},
+ {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"},
+ {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"},
+ {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"},
+ {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"},
+ {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"},
+ {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"},
+ {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"},
+ {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"},
+ {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"},
+ {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"},
+ {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"},
+ {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"},
+ {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "greenlet"
version = "3.0.3"
@@ -2013,6 +2271,22 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "proces"
+version = "0.1.7"
+description = "text preprocess."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "proces-0.1.7-py3-none-any.whl", hash = "sha256:308325bbc96877263f06e57e5e9c760c4b42cc722887ad60be6b18fc37d68762"},
+ {file = "proces-0.1.7.tar.gz", hash = "sha256:70a05d9e973dd685f7a9092c58be695a8181a411d63796c213232fd3fdc43775"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "prompt-toolkit"
version = "3.0.43"
@@ -2461,6 +2735,99 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "regex"
+version = "2024.7.24"
+description = "Alternative regular expression module, to replace re."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"},
+ {file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"},
+ {file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"},
+ {file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"},
+ {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"},
+ {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"},
+ {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"},
+ {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"},
+ {file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"},
+ {file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"},
+ {file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"},
+ {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"},
+ {file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"},
+ {file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"},
+ {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"},
+ {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"},
+ {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"},
+ {file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"},
+ {file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"},
+ {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"},
+ {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"},
+ {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"},
+ {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"},
+ {file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"},
+ {file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"},
+ {file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"},
+ {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"},
+ {file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"},
+ {file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"},
+ {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"},
+ {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"},
+ {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"},
+ {file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"},
+ {file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"},
+ {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"},
+ {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"},
+ {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"},
+ {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"},
+ {file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"},
+ {file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"},
+ {file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"},
+ {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"},
+ {file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"},
+ {file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"},
+ {file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"},
+ {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"},
+ {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"},
+ {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"},
+ {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"},
+ {file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"},
+ {file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"},
+ {file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"},
+ {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"},
+ {file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"},
+ {file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"},
+ {file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"},
+ {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"},
+ {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"},
+ {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"},
+ {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"},
+ {file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"},
+ {file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"},
+ {file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"},
+ {file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"},
+]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "requests"
version = "2.31.0"
@@ -2663,6 +3030,27 @@ type = "legacy"
url = "https://mirrors.aliyun.com/pypi/simple"
reference = "ali"
+[[package]]
+name = "setuptools"
+version = "71.1.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"},
+ {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"},
+]
+
+[package.extras]
+core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+
+[package.source]
+type = "legacy"
+url = "https://mirrors.aliyun.com/pypi/simple"
+reference = "ali"
+
[[package]]
name = "sgmllib3k"
version = "1.0.0"
@@ -3502,4 +3890,4 @@ reference = "ali"
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
-content-hash = "bb01964309a665f0348ca69fecf771a9c6f7d99147c47010c0e64d2d13fe25ad"
+content-hash = "8259b57e9de269479dfb70be17e3060fedc0426ae22abf3317448e50edba9b23"
diff --git a/pyproject.toml b/pyproject.toml
index b8cd14b8..a1b297e1 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,6 +41,9 @@ feedparser = "^6.0.11"
opencv-python = "^4.9.0.80"
imagehash = "^4.3.1"
black = "^24.4.2"
+cn2an = "^0.5.22"
+aiohttp = "^3.9.5"
+dateparser = "^1.2.0"
[tool.poetry.dev-dependencies]
diff --git a/zhenxun/plugins/draw_card/__init__.py b/zhenxun/plugins/draw_card/__init__.py
new file mode 100644
index 00000000..36e56ad7
--- /dev/null
+++ b/zhenxun/plugins/draw_card/__init__.py
@@ -0,0 +1,289 @@
+import asyncio
+import traceback
+from dataclasses import dataclass
+from typing import Any
+
+import nonebot
+from cn2an import cn2an
+from nonebot import on_keyword, on_message, on_regex
+from nonebot.log import logger
+from nonebot.matcher import Matcher
+from nonebot.params import RegexGroup
+from nonebot.permission import SUPERUSER
+from nonebot.plugin import PluginMetadata
+from nonebot.typing import T_Handler
+from nonebot_plugin_apscheduler import scheduler
+from nonebot_plugin_saa import Text
+from nonebot_plugin_session import EventSession
+
+from zhenxun.configs.config import Config
+from zhenxun.configs.utils import PluginExtraData
+
+from .handles.azur_handle import AzurHandle
+from .handles.ba_handle import BaHandle
+from .handles.base_handle import BaseHandle
+from .handles.fgo_handle import FgoHandle
+from .handles.genshin_handle import GenshinHandle
+from .handles.guardian_handle import GuardianHandle
+from .handles.onmyoji_handle import OnmyojiHandle
+from .handles.pcr_handle import PcrHandle
+from .handles.pretty_handle import PrettyHandle
+from .handles.prts_handle import PrtsHandle
+from .rule import rule
+
+__plugin_meta__ = PluginMetadata(
+ name="游戏抽卡",
+ description="就算是模拟抽卡也不能改变自己是个非酋",
+ usage="""
+ usage:
+ 模拟赛马娘,原神,明日方舟,坎公骑冠剑,公主连结(国/台),碧蓝航线,FGO,阴阳师,碧蓝档案进行抽卡
+ 指令:
+ 原神[1-180]抽: 原神常驻池
+ 原神角色[1-180]抽: 原神角色UP池子
+ 原神角色2池[1-180]抽: 原神角色UP池子
+ 原神武器[1-180]抽: 原神武器UP池子
+ 重置原神抽卡: 清空当前卡池的抽卡次数[即从0开始计算UP概率]
+ 方舟[1-300]抽: 方舟卡池,当有当期UP时指向UP池
+ 赛马娘[1-200]抽: 赛马娘卡池,当有当期UP时指向UP池
+ 坎公骑冠剑[1-300]抽: 坎公骑冠剑卡池,当有当期UP时指向UP池
+ pcr/公主连接[1-300]抽: 公主连接卡池
+ 碧蓝航线/碧蓝[重型/轻型/特型/活动][1-300]抽: 碧蓝航线重型/轻型/特型/活动卡池
+ fgo[1-300]抽: fgo卡池 (已失效)
+ 阴阳师[1-300]抽: 阴阳师卡池
+ ba/碧蓝档案[1-200]抽:碧蓝档案卡池 (已失效)
+ * 以上指令可以通过 XX一井 来指定最大抽取数量 *
+ * 示例:原神一井 *
+ """.strip(),
+ extra=PluginExtraData(
+ author="HibiKier",
+ version="0.1",
+ menu_type="抽卡相关",
+ superuser_help="""
+ 更新方舟信息
+ 重载方舟卡池
+ 更新原神信息
+ 重载原神卡池
+ 更新赛马娘信息
+ 重载赛马娘卡池
+ 更新坎公骑冠剑信息
+ 更新碧蓝航线信息
+ 更新fgo信息
+ 更新阴阳师信息
+ """,
+ ).dict(),
+)
+
+_hidden = on_message(rule=lambda: False)
+
+
+@dataclass
+class Game:
+ keywords: set[str]
+ handle: BaseHandle
+ flag: bool
+ config_name: str
+ max_count: int = 300 # 一次最大抽卡数
+ reload_time: int | None = None # 重载UP池时间(小时)
+ has_other_pool: bool = False
+
+
+games = (
+ Game(
+ {"azur", "碧蓝航线"},
+ AzurHandle(),
+ Config.get_config("draw_card", "AZUR_FLAG", True),
+ "AZUR_FLAG",
+ ),
+ Game(
+ {"fgo", "命运冠位指定"},
+ FgoHandle(),
+ Config.get_config("draw_card", "FGO_FLAG", True),
+ "FGO_FLAG",
+ ),
+ Game(
+ {"genshin", "原神"},
+ GenshinHandle(),
+ Config.get_config("draw_card", "GENSHIN_FLAG", True),
+ "GENSHIN_FLAG",
+ max_count=180,
+ reload_time=18,
+ has_other_pool=True,
+ ),
+ Game(
+ {"guardian", "坎公骑冠剑"},
+ GuardianHandle(),
+ Config.get_config("draw_card", "GUARDIAN_FLAG", True),
+ "GUARDIAN_FLAG",
+ reload_time=4,
+ ),
+ Game(
+ {"onmyoji", "阴阳师"},
+ OnmyojiHandle(),
+ Config.get_config("draw_card", "ONMYOJI_FLAG", True),
+ "ONMYOJI_FLAG",
+ ),
+ Game(
+ {"pcr", "公主连结", "公主连接", "公主链接", "公主焊接"},
+ PcrHandle(),
+ Config.get_config("draw_card", "PCR_FLAG", True),
+ "PCR_FLAG",
+ ),
+ Game(
+ {"pretty", "马娘", "赛马娘"},
+ PrettyHandle(),
+ Config.get_config("draw_card", "PRETTY_FLAG", True),
+ "PRETTY_FLAG",
+ max_count=200,
+ reload_time=4,
+ ),
+ Game(
+ {"prts", "方舟", "明日方舟"},
+ PrtsHandle(),
+ Config.get_config("draw_card", "PRTS_FLAG", True),
+ "PRTS_FLAG",
+ reload_time=4,
+ ),
+ Game(
+ {"ba", "碧蓝档案"},
+ BaHandle(),
+ Config.get_config("draw_card", "BA_FLAG", True),
+ "BA_FLAG",
+ ),
+)
+
+
+def create_matchers():
+ def draw_handler(game: Game) -> T_Handler:
+ async def handler(
+ session: EventSession,
+ args: tuple[Any, ...] = RegexGroup(),
+ ):
+ pool_name, pool_type_, num, unit = args
+ if num == "单":
+ num = 1
+ else:
+ try:
+ num = int(cn2an(num, mode="smart"))
+ except ValueError:
+ await Text("必!须!是!数!字!").finish(reply=True)
+ if unit == "井":
+ num *= game.max_count
+ if num < 1:
+ await Text("虚空抽卡???").finish(reply=True)
+ elif num > game.max_count:
+ await Text("一井都满不足不了你嘛!快爬开!").finish(reply=True)
+ pool_name = (
+ pool_name.replace("池", "")
+ .replace("武器", "arms")
+ .replace("角色", "char")
+ .replace("卡牌", "card")
+ .replace("卡", "card")
+ )
+ try:
+ if pool_type_ in ["2池", "二池"]:
+ pool_name = pool_name + "1"
+ res = await game.handle.draw(
+ num, pool_name=pool_name, user_id=session.id1
+ )
+ logger.info(
+ f"游戏抽卡 类型: {list(game.keywords)[1]} 卡池: {pool_name} 数量: {num}",
+ "游戏抽卡",
+ session=session,
+ )
+ except:
+ logger.warning(traceback.format_exc())
+ await Text("出错了...").finish(reply=True)
+ await res.send()
+
+ return handler
+
+ def update_handler(game: Game) -> T_Handler:
+ async def handler(matcher: Matcher):
+ await game.handle.update_info()
+ await matcher.finish("更新完成!")
+
+ return handler
+
+ def reload_handler(game: Game) -> T_Handler:
+ async def handler(matcher: Matcher):
+ res = await game.handle.reload_pool()
+ if res:
+ await res.send()
+
+ return handler
+
+ def reset_handler(game: Game) -> T_Handler:
+ async def handler(matcher: Matcher, session: EventSession):
+ if not session.id1:
+ await Text("获取用户id失败...").finish()
+ if game.handle.reset_count(session.id1):
+ await Text("重置成功!").send()
+
+ return handler
+
+ def scheduled_job(game: Game) -> T_Handler:
+ async def handler():
+ await game.handle.reload_pool()
+
+ return handler
+
+ for game in games:
+ pool_pattern = r"([^\s单0-9零一二三四五六七八九百十]{0,3})"
+ num_pattern = r"(单|[0-9零一二三四五六七八九百十]{1,3})"
+ unit_pattern = r"([抽|井|连])"
+ pool_type = "()"
+ if game.has_other_pool:
+ pool_type = r"([2二]池)?"
+ draw_regex = r".*?(?:{})\s*{}\s*{}\s*{}\s*{}".format(
+ "|".join(game.keywords), pool_pattern, pool_type, num_pattern, unit_pattern
+ )
+ update_keywords = {f"更新{keyword}信息" for keyword in game.keywords}
+ reload_keywords = {f"重载{keyword}卡池" for keyword in game.keywords}
+ reset_keywords = {f"重置{keyword}抽卡" for keyword in game.keywords}
+ on_regex(draw_regex, priority=5, block=True, rule=rule(game)).append_handler(
+ draw_handler(game)
+ )
+ on_keyword(
+ update_keywords, priority=1, block=True, permission=SUPERUSER
+ ).append_handler(update_handler(game))
+ on_keyword(
+ reload_keywords, priority=1, block=True, permission=SUPERUSER
+ ).append_handler(reload_handler(game))
+ on_keyword(reset_keywords, priority=5, block=True).append_handler(
+ reset_handler(game)
+ )
+ if game.reload_time:
+ scheduler.add_job(
+ scheduled_job(game), trigger="cron", hour=game.reload_time, minute=1
+ )
+
+
+create_matchers()
+
+
+# 更新资源
+@scheduler.scheduled_job(
+ "cron",
+ hour=4,
+ minute=1,
+)
+async def _():
+ tasks = []
+ for game in games:
+ if game.flag:
+ tasks.append(asyncio.ensure_future(game.handle.update_info()))
+ await asyncio.gather(*tasks)
+
+
+driver = nonebot.get_driver()
+
+
+@driver.on_startup
+async def _():
+ tasks = []
+ for game in games:
+ if game.flag:
+ game.handle.init_data()
+ if not game.handle.data_exists():
+ tasks.append(asyncio.ensure_future(game.handle.update_info()))
+ await asyncio.gather(*tasks)
diff --git a/zhenxun/plugins/draw_card/config.py b/zhenxun/plugins/draw_card/config.py
new file mode 100644
index 00000000..0aff3ef8
--- /dev/null
+++ b/zhenxun/plugins/draw_card/config.py
@@ -0,0 +1,203 @@
+import nonebot
+import ujson as json
+from pydantic import BaseModel, Extra, ValidationError
+
+from zhenxun.configs.config import Config as AConfig
+from zhenxun.configs.path_config import DATA_PATH, IMAGE_PATH
+from zhenxun.services.log import logger
+
+
+# 原神
+class GenshinConfig(BaseModel, extra=Extra.ignore):
+ GENSHIN_FIVE_P: float = 0.006
+ GENSHIN_FOUR_P: float = 0.051
+ GENSHIN_THREE_P: float = 0.43
+ GENSHIN_G_FIVE_P: float = 0.016
+ GENSHIN_G_FOUR_P: float = 0.13
+ I72_ADD: float = 0.0585
+
+
+# 明日方舟
+class PrtsConfig(BaseModel, extra=Extra.ignore):
+ PRTS_SIX_P: float = 0.02
+ PRTS_FIVE_P: float = 0.08
+ PRTS_FOUR_P: float = 0.48
+ PRTS_THREE_P: float = 0.42
+
+
+# 赛马娘
+class PrettyConfig(BaseModel, extra=Extra.ignore):
+ PRETTY_THREE_P: float = 0.03
+ PRETTY_TWO_P: float = 0.18
+ PRETTY_ONE_P: float = 0.79
+
+
+# 坎公骑冠剑
+class GuardianConfig(BaseModel, extra=Extra.ignore):
+ GUARDIAN_THREE_CHAR_P: float = 0.0275
+ GUARDIAN_TWO_CHAR_P: float = 0.19
+ GUARDIAN_ONE_CHAR_P: float = 0.7825
+ GUARDIAN_THREE_CHAR_UP_P: float = 0.01375
+ GUARDIAN_THREE_CHAR_OTHER_P: float = 0.01375
+ GUARDIAN_EXCLUSIVE_ARMS_P: float = 0.03
+ GUARDIAN_FIVE_ARMS_P: float = 0.03
+ GUARDIAN_FOUR_ARMS_P: float = 0.09
+ GUARDIAN_THREE_ARMS_P: float = 0.27
+ GUARDIAN_TWO_ARMS_P: float = 0.58
+ GUARDIAN_EXCLUSIVE_ARMS_UP_P: float = 0.01
+ GUARDIAN_EXCLUSIVE_ARMS_OTHER_P: float = 0.02
+
+
+# 公主连结
+class PcrConfig(BaseModel, extra=Extra.ignore):
+ PCR_THREE_P: float = 0.025
+ PCR_TWO_P: float = 0.18
+ PCR_ONE_P: float = 0.795
+ PCR_G_THREE_P: float = 0.025
+ PCR_G_TWO_P: float = 0.975
+
+
+# 碧蓝航线
+class AzurConfig(BaseModel, extra=Extra.ignore):
+ AZUR_FIVE_P: float = 0.012
+ AZUR_FOUR_P: float = 0.07
+ AZUR_THREE_P: float = 0.12
+ AZUR_TWO_P: float = 0.51
+ AZUR_ONE_P: float = 0.3
+
+
+# 命运-冠位指定
+class FgoConfig(BaseModel, extra=Extra.ignore):
+ FGO_SERVANT_FIVE_P: float = 0.01
+ FGO_SERVANT_FOUR_P: float = 0.03
+ FGO_SERVANT_THREE_P: float = 0.4
+ FGO_CARD_FIVE_P: float = 0.04
+ FGO_CARD_FOUR_P: float = 0.12
+ FGO_CARD_THREE_P: float = 0.4
+
+
+# 阴阳师
+class OnmyojiConfig(BaseModel, extra=Extra.ignore):
+ ONMYOJI_SP: float = 0.0025
+ ONMYOJI_SSR: float = 0.01
+ ONMYOJI_SR: float = 0.2
+ ONMYOJI_R: float = 0.7875
+
+
+# 碧蓝档案
+class BaConfig(BaseModel, extra=Extra.ignore):
+ BA_THREE_P: float = 0.025
+ BA_TWO_P: float = 0.185
+ BA_ONE_P: float = 0.79
+ BA_G_TWO_P: float = 0.975
+
+
+class Config(BaseModel, extra=Extra.ignore):
+ # 开关
+ PRTS_FLAG: bool = True
+ GENSHIN_FLAG: bool = True
+ PRETTY_FLAG: bool = True
+ GUARDIAN_FLAG: bool = True
+ PCR_FLAG: bool = True
+ AZUR_FLAG: bool = True
+ FGO_FLAG: bool = True
+ ONMYOJI_FLAG: bool = True
+ BA_FLAG: bool = True
+
+ # 其他配置
+ PCR_TAI: bool = False
+ SEMAPHORE: int = 5
+
+ # 抽卡概率
+ prts: PrtsConfig = PrtsConfig()
+ genshin: GenshinConfig = GenshinConfig()
+ pretty: PrettyConfig = PrettyConfig()
+ guardian: GuardianConfig = GuardianConfig()
+ pcr: PcrConfig = PcrConfig()
+ azur: AzurConfig = AzurConfig()
+ fgo: FgoConfig = FgoConfig()
+ onmyoji: OnmyojiConfig = OnmyojiConfig()
+ ba: BaConfig = BaConfig()
+
+
+driver = nonebot.get_driver()
+
+# DRAW_PATH = Path("data/draw_card").absolute()
+DRAW_PATH = IMAGE_PATH / "draw_card"
+# try:
+# DRAW_PATH = Path(global_config.draw_path).absolute()
+# except:
+# pass
+config_path = DATA_PATH / "draw_card" / "draw_card_config" / "draw_card_config.json"
+
+draw_config: Config = Config()
+
+for game_flag, game_name in zip(
+ [
+ "PRTS_FLAG",
+ "GENSHIN_FLAG",
+ "PRETTY_FLAG",
+ "GUARDIAN_FLAG",
+ "PCR_FLAG",
+ "AZUR_FLAG",
+ "FGO_FLAG",
+ "ONMYOJI_FLAG",
+ "PCR_TAI",
+ "BA_FLAG",
+ ],
+ [
+ "明日方舟",
+ "原神",
+ "赛马娘",
+ "坎公骑冠剑",
+ "公主连结",
+ "碧蓝航线",
+ "命运-冠位指定(FGO)",
+ "阴阳师",
+ "pcr台服卡池",
+ "碧蓝档案",
+ ],
+):
+ AConfig.add_plugin_config(
+ "draw_card",
+ game_flag,
+ True,
+ help=f"{game_name} 抽卡开关",
+ default_value=True,
+ type=bool,
+ )
+AConfig.add_plugin_config(
+ "draw_card",
+ "SEMAPHORE",
+ 5,
+ help=f"异步数据下载数量限制",
+ default_value=5,
+ type=int,
+)
+AConfig.set_name("draw_card", "游戏抽卡")
+
+
+@driver.on_startup
+def check_config():
+ global draw_config
+
+ if not config_path.exists():
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+ draw_config = Config()
+ logger.warning("draw_card:配置文件不存在,已重新生成配置文件...")
+ else:
+ with config_path.open("r", encoding="utf8") as fp:
+ data = json.load(fp)
+ try:
+ draw_config = Config.parse_obj({**data})
+ except ValidationError:
+ draw_config = Config()
+ logger.warning("draw_card:配置文件格式错误,已重新生成配置文件...")
+
+ with config_path.open("w", encoding="utf8") as fp:
+ json.dump(
+ draw_config.dict(),
+ fp,
+ indent=4,
+ ensure_ascii=False,
+ )
diff --git a/zhenxun/plugins/draw_card/count_manager.py b/zhenxun/plugins/draw_card/count_manager.py
new file mode 100644
index 00000000..7768b057
--- /dev/null
+++ b/zhenxun/plugins/draw_card/count_manager.py
@@ -0,0 +1,149 @@
+from typing import Optional, TypeVar, Generic
+from pydantic import BaseModel
+from cachetools import TTLCache
+
+
+class BaseUserCount(BaseModel):
+ count: int = 0 # 当前抽卡次数
+
+
+TCount = TypeVar("TCount", bound="BaseUserCount")
+
+
+class DrawCountManager(Generic[TCount]):
+ """
+ 抽卡统计保底
+ """
+
+ def __init__(
+ self, game_draw_count_rule: tuple, star2name: tuple, max_draw_count: int
+ ):
+ """
+ 初始化保底统计
+
+ 例如:DrawCountManager((10, 90, 180), ("4", "5", "5"))
+
+ 抽卡保底需要的次数和返回的对应名称,例如星级等
+
+ :param game_draw_count_rule:抽卡规则
+ :param star2name:星级对应的名称
+ :param max_draw_count:最大累计抽卡次数,当下次单次抽卡超过该次数时将会清空数据
+
+ """
+ # 只有保底
+ # 超过60秒重置抽卡次数
+ self._data: TTLCache[int, TCount] = TTLCache(maxsize=1000, ttl=60)
+ self._guarantee_tuple = game_draw_count_rule
+ self._star2name = star2name
+ self._max_draw_count = max_draw_count
+
+ @classmethod
+ def get_count_class(cls) -> TCount:
+ raise NotImplementedError
+
+ def _get_count(self, key: int) -> TCount:
+ if self._data.get(key) is None:
+ self._data[key] = self.get_count_class()
+ else:
+ self._data[key] = self._data[key]
+ return self._data[key]
+
+ def increase(self, key: int, value: int = 1):
+ """
+ 用户抽卡次数加1
+ """
+ self._get_count(key).count += value
+
+ def get_max_guarantee(self):
+ """
+ 获取最大保底抽卡次数
+ """
+ return self._guarantee_tuple[-1]
+
+ def get_user_count(self, key: int) -> int:
+ """
+ 获取当前抽卡次数
+ """
+ return self._get_count(key).count
+
+ def reset(self, key: int):
+ """
+ 清空记录
+ """
+ self._data.pop(key, None)
+
+
+class GenshinUserCount(BaseUserCount):
+ five_index: int = 0 # 获取五星时的抽卡次数
+ four_index: int = 0 # 获取四星时的抽卡次数
+ is_up: bool = False # 下次五星是否必定为up
+
+
+class GenshinCountManager(DrawCountManager[GenshinUserCount]):
+ @classmethod
+ def get_count_class(cls) -> GenshinUserCount:
+ return GenshinUserCount()
+
+ def set_is_up(self, key: int, value: bool):
+ """
+ 设置下次是否必定up
+ """
+ self._get_count(key).is_up = value
+
+ def is_up(self, key: int) -> bool:
+ """
+ 判断该次保底是否必定为up
+ """
+ return self._get_count(key).is_up
+
+ def get_user_five_index(self, key: int) -> int:
+ """
+ 获取用户上次获取五星的次数
+ """
+ return self._get_count(key).five_index
+
+ def get_user_four_index(self, key: int) -> int:
+ """
+ 获取用户上次获取四星的次数
+ """
+ return self._get_count(key).four_index
+
+ def mark_five_index(self, key: int):
+ """
+ 标记用户该次次数为五星
+ """
+ self._get_count(key).five_index = self._get_count(key).count
+
+ def mark_four_index(self, key: int):
+ """
+ 标记用户该次次数为四星
+ """
+ self._get_count(key).four_index = self._get_count(key).count
+
+ def check_count(self, key: int, count: int):
+ """
+ 检查用户该次抽卡次数累计是否超过最大限制次数
+ """
+ if self._get_count(key).count + count > self._max_draw_count:
+ self._data.pop(key, None)
+
+ def get_user_guarantee_count(self, key: int) -> int:
+ user = self._get_count(key)
+ return (
+ self.get_max_guarantee()
+ - (user.count % self.get_max_guarantee() - user.five_index)
+ ) % self.get_max_guarantee() or self.get_max_guarantee()
+
+ def check(self, key: int) -> Optional[int]:
+ """
+ 是否保底
+ """
+ # print(self._data)
+ user = self._get_count(key)
+ if user.count - user.five_index == 90:
+ user.five_index = user.count
+ return 5
+ if user.count - user.four_index == 10:
+ user.four_index = user.count
+ return 4
+ return None
diff --git a/zhenxun/plugins/draw_card/handles/azur_handle.py b/zhenxun/plugins/draw_card/handles/azur_handle.py
new file mode 100644
index 00000000..06949085
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/azur_handle.py
@@ -0,0 +1,307 @@
+import random
+from urllib.parse import unquote
+
+import dateparser
+import ujson as json
+from lxml import etree
+from nonebot_plugin_saa import Image, MessageFactory, Text
+from PIL import ImageDraw
+from pydantic import ValidationError
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle
+from .base_handle import UpChar as _UpChar
+from .base_handle import UpEvent as _UpEvent
+
+
+class AzurChar(BaseData):
+ type_: str # 舰娘类型
+
+ @property
+ def star_str(self) -> str:
+ return ["白", "蓝", "紫", "金"][self.star - 1]
+
+
+class UpChar(_UpChar):
+ type_: str # 舰娘类型
+
+
+class UpEvent(_UpEvent):
+ up_char: list[UpChar] # up对象
+
+
+class AzurHandle(BaseHandle[AzurChar]):
+ def __init__(self):
+ super().__init__("azur", "碧蓝航线")
+ self.max_star = 4
+ self.config = draw_config.azur
+ self.ALL_CHAR: list[AzurChar] = []
+ self.UP_EVENT: UpEvent | None = None
+
+ def get_card(self, pool_name: str, **kwargs) -> AzurChar:
+ if pool_name == "轻型":
+ type_ = ["驱逐", "轻巡", "维修"]
+ elif pool_name == "重型":
+ type_ = ["重巡", "战列", "战巡", "重炮"]
+ else:
+ type_ = ["维修", "潜艇", "重巡", "轻航", "航母"]
+ up_pool_flag = pool_name == "活动"
+ # Up
+ up_ship = (
+ [x for x in self.UP_EVENT.up_char if x.zoom > 0] if self.UP_EVENT else []
+ )
+ # print(up_ship)
+ acquire_char = None
+ if up_ship and up_pool_flag:
+ up_zoom: list[tuple[float, float]] = [(0, up_ship[0].zoom / 100)]
+ # 初始化概率
+ cur_ = up_ship[0].zoom / 100
+ for i in range(len(up_ship)):
+ try:
+ up_zoom.append((cur_, cur_ + up_ship[i + 1].zoom / 100))
+ cur_ += up_ship[i + 1].zoom / 100
+ except IndexError:
+ pass
+ rand = random.random()
+ # 抽取up
+ for i, zoom in enumerate(up_zoom):
+ if zoom[0] <= rand <= zoom[1]:
+ try:
+ acquire_char = [
+ x for x in self.ALL_CHAR if x.name == up_ship[i].name
+ ][0]
+ except IndexError:
+ pass
+ # 没有up或者未抽取到up
+ if not acquire_char:
+ star = self.get_star(
+ # [4, 3, 2, 1],
+ [4, 3, 2, 2],
+ [
+ self.config.AZUR_FOUR_P,
+ self.config.AZUR_THREE_P,
+ self.config.AZUR_TWO_P,
+ self.config.AZUR_ONE_P,
+ ],
+ )
+ acquire_char = random.choice(
+ [
+ x
+ for x in self.ALL_CHAR
+ if x.star == star and x.type_ in type_ and not x.limited
+ ]
+ )
+ return acquire_char
+
+ async def draw(self, count: int, **kwargs) -> MessageFactory:
+ index2card = self.get_cards(count, **kwargs)
+ cards = [card[0] for card in index2card]
+ up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
+ result = self.format_result(index2card, **{**kwargs, "up_list": up_list})
+ gen_img = await self.generate_img(cards)
+ return MessageFactory([Image(gen_img.pic2bytes()), Text(result)])
+
+ async def generate_card_img(self, card: AzurChar) -> BuildImage:
+ sep_w = 5
+ sep_t = 5
+ sep_b = 20
+ w = 100
+ h = 100
+ bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
+ frame_path = str(self.img_path / f"{card.star}_star.png")
+ frame = BuildImage(w, h, background=frame_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(w, h, background=img_path)
+ # 加圆角
+ await frame.circle_corner(6)
+ await img.circle_corner(6)
+ await bg.paste(img, (sep_w, sep_t))
+ await bg.paste(frame, (sep_w, sep_t))
+ # 加名字
+ text = card.name[:6] + "..." if len(card.name) > 7 else card.name
+ font = load_font(fontsize=14)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
+ text,
+ font=font,
+ fill=["#808080", "#3b8bff", "#8000ff", "#c90", "#ee494c"][card.star - 1],
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ AzurChar(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited="可以建造" not in value["获取途径"],
+ type_=value["类型"],
+ )
+ for value in self.load_data().values()
+ ]
+ self.load_up_char()
+
+ def load_up_char(self):
+ try:
+ data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
+ self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
+ except ValidationError:
+ logger.warning(f"{self.game_name}_up_char 解析出错")
+
+ def dump_up_char(self):
+ if self.UP_EVENT:
+ data = {"char": json.loads(self.UP_EVENT.json())}
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+
+ async def _update_info(self):
+ info = {}
+ # 更新图鉴
+ url = "https://wiki.biligame.com/blhx/舰娘图鉴"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ contents = dom.xpath(
+ "//div[@class='mw-body-content mw-content-ltr']/div[@class='mw-parser-output']"
+ )
+ for index, content in enumerate(contents):
+ char_list = content.xpath("./div[@id='CardSelectTr']/div")
+ for char in char_list:
+ try:
+ name = char.xpath("./span/a/text()")[0]
+ frame = char.xpath("./div/div/a/img/@alt")[0]
+ avatar = char.xpath("./div/img/@srcset")[0]
+ except IndexError:
+ continue
+ member_dict = {
+ "名称": remove_prohibited_str(name),
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "星级": self.parse_star(frame),
+ "类型": char.xpath("./@data-param1")[0].split(",")[-1],
+ }
+ info[member_dict["名称"]] = member_dict
+ # 更新额外信息
+ for key in info.keys():
+ # TODO: 各种舰娘·改获取错误
+ url = f"https://wiki.biligame.com/blhx/{key}"
+ result = await self.get_url(url)
+ if not result:
+ info[key]["获取途径"] = []
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ continue
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ time = dom.xpath(
+ "//table[@class='wikitable sv-general']/tbody[1]/tr[4]/td[2]//text()"
+ )[0]
+ sources = []
+ if "无法建造" in time:
+ sources.append("无法建造")
+ elif "活动已关闭" in time:
+ sources.append("活动限定")
+ else:
+ sources.append("可以建造")
+ info[key]["获取途径"] = sources
+ except IndexError:
+ info[key]["获取途径"] = []
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ self.dump_data(info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # 下载头像
+ for value in info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载头像框
+ idx = 1
+ BLHX_URL = "https://patchwiki.biligame.com/images/blhx"
+ for url in [
+ "/1/15/pxho13xsnkyb546tftvh49etzdh74cf.png",
+ "/a/a9/k8t7nx6c8pan5vyr8z21txp45jxeo66.png",
+ "/a/a5/5whkzvt200zwhhx0h0iz9qo1kldnidj.png",
+ "/a/a2/ptog1j220x5q02hytpwc8al7f229qk9.png",
+ "/6/6d/qqv5oy3xs40d3055cco6bsm0j4k4gzk.png",
+ ]:
+ await self.download_img(BLHX_URL + url, f"{idx}_star")
+ idx += 1
+ await self.update_up_char()
+
+ @staticmethod
+ def parse_star(star: str) -> int:
+ if star in ["舰娘头像外框普通.png", "舰娘头像外框白色.png"]:
+ return 1
+ elif star in ["舰娘头像外框稀有.png", "舰娘头像外框蓝色.png"]:
+ return 2
+ elif star in ["舰娘头像外框精锐.png", "舰娘头像外框紫色.png"]:
+ return 3
+ elif star in ["舰娘头像外框超稀有.png", "舰娘头像外框金色.png"]:
+ return 4
+ elif star in ["舰娘头像外框海上传奇.png", "舰娘头像外框彩色.png"]:
+ return 5
+ elif star in [
+ "舰娘头像外框最高方案.png",
+ "舰娘头像外框决战方案.png",
+ "舰娘头像外框超稀有META.png",
+ "舰娘头像外框精锐META.png",
+ ]:
+ return 6
+ else:
+ return 6
+
+ async def update_up_char(self):
+ url = "https://wiki.biligame.com/blhx/游戏活动表"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取活动表出错")
+ return
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ dd = dom.xpath("//div[@class='timeline2']/dl/dd/a")[0]
+ url = "https://wiki.biligame.com" + dd.xpath("./@href")[0]
+ title = dd.xpath("string(.)")
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取活动页面出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ timer = dom.xpath("//span[@class='eventTimer']")[0]
+ start_time = dateparser.parse(timer.xpath("./@data-start")[0])
+ end_time = dateparser.parse(timer.xpath("./@data-end")[0])
+ ships = dom.xpath("//table[@class='shipinfo']")
+ up_chars = []
+ for ship in ships:
+ name = ship.xpath("./tbody/tr/td[2]/p/a/@title")[0]
+ type_ = ship.xpath("./tbody/tr/td[2]/p/small/text()")[0] # 舰船类型
+ try:
+ p = float(str(ship.xpath(".//sup/text()")[0]).strip("%"))
+ except (IndexError, ValueError):
+ p = 0
+ star = self.parse_star(
+ ship.xpath("./tbody/tr/td[1]/div/div/div/a/img/@alt")[0]
+ )
+ up_chars.append(
+ UpChar(name=name, star=star, limited=False, zoom=p, type_=type_)
+ )
+ self.UP_EVENT = UpEvent(
+ title=title,
+ pool_img="",
+ start_time=start_time,
+ end_time=end_time,
+ up_char=up_chars,
+ )
+ self.dump_up_char()
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
+
+ async def _reload_pool(self) -> MessageFactory | None:
+ await self.update_up_char()
+ self.load_up_char()
+ if self.UP_EVENT:
+ return MessageFactory(
+ [Text(f"重载成功!\n当前活动:{self.UP_EVENT.title}")]
+ )
diff --git a/zhenxun/plugins/draw_card/handles/ba_handle.py b/zhenxun/plugins/draw_card/handles/ba_handle.py
new file mode 100644
index 00000000..a5d50743
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/ba_handle.py
@@ -0,0 +1,153 @@
+import random
+
+from PIL import ImageDraw
+
+from zhenxun.services.log import logger
+from zhenxun.utils.http_utils import AsyncHttpx
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font
+from .base_handle import BaseData, BaseHandle
+
+
+class BaChar(BaseData):
+ pass
+
+
+class BaHandle(BaseHandle[BaChar]):
+ def __init__(self):
+ super().__init__("ba", "碧蓝档案")
+ self.max_star = 3
+ self.config = draw_config.ba
+ self.ALL_CHAR: list[BaChar] = []
+
+ def get_card(self, mode: int = 1) -> BaChar:
+ if mode == 2:
+ star = self.get_star(
+ [3, 2], [self.config.BA_THREE_P, self.config.BA_G_TWO_P]
+ )
+ else:
+ star = self.get_star(
+ [3, 2, 1],
+ [self.config.BA_THREE_P, self.config.BA_TWO_P, self.config.BA_ONE_P],
+ )
+ chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
+ return random.choice(chars)
+
+ def get_cards(self, count: int, **kwargs) -> list[tuple[BaChar, int]]:
+ card_list = []
+ card_count = 0 # 保底计算
+ for i in range(count):
+ card_count += 1
+ # 十连保底
+ if card_count == 10:
+ card = self.get_card(2)
+ card_count = 0
+ else:
+ card = self.get_card(1)
+ if card.star > self.max_star - 2:
+ card_count = 0
+ card_list.append((card, i + 1))
+ return card_list
+
+ async def generate_card_img(self, card: BaChar) -> BuildImage:
+ sep_w = 5
+ sep_h = 5
+ star_h = 15
+ img_w = 90
+ img_h = 100
+ font_h = 20
+ bar_h = 20
+ bar_w = 90
+ bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ bar = BuildImage(bar_w, bar_h, color="#6495ED")
+ await bg.paste(img, (sep_w, sep_h))
+ await bg.paste(bar, (sep_w, img_h - bar_h + sep_h))
+ if card.star == 1:
+ star_path = str(self.img_path / "star-1.png")
+ star_w = 15
+ elif card.star == 2:
+ star_path = str(self.img_path / "star-2.png")
+ star_w = 30
+ else:
+ star_path = str(self.img_path / "star-3.png")
+ star_w = 45
+ star = BuildImage(star_w, star_h, background=star_path)
+ await bg.paste(star, (img_w // 2 - 15 * (card.star - 1) // 2, img_h - star_h))
+ text = card.name[:5] + "..." if len(card.name) > 6 else card.name
+ font = load_font(fontsize=14)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
+ text,
+ font=font,
+ fill="gray",
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ BaChar(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited=True if "(" in key else False,
+ )
+ for key, value in self.load_data().items()
+ ]
+
+ def title2star(self, title: int):
+ if title == "Star-3.png":
+ return 3
+ elif title == "Star-2.png":
+ return 2
+ else:
+ return 1
+
+ async def _update_info(self):
+ # TODO: ba获取链接失效
+ info = {}
+ url = "https://lonqie.github.io/SchaleDB/data/cn/students.min.json?v=49"
+ result = (await AsyncHttpx.get(url)).json()
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ else:
+ for char in result:
+ try:
+ name = char["Name"]
+ avatar = (
+ "https://github.com/lonqie/SchaleDB/raw/main/images/student/icon/"
+ + char["CollectionTexture"]
+ + ".png"
+ )
+ star = char["StarGrade"]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": avatar,
+ "名称": name,
+ "星级": star,
+ }
+ info[member_dict["名称"]] = member_dict
+ self.dump_data(info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # 下载头像
+ for value in info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载星星
+ await self.download_img(
+ "https://patchwiki.biligame.com/images/bluearchive/thumb/e/e0/82nj2x9sxko473g7782r14fztd4zyky.png/15px-Star-1.png",
+ "star-1",
+ )
+ await self.download_img(
+ "https://patchwiki.biligame.com/images/bluearchive/thumb/0/0b/msaff2g0zk6nlyl1rrn7n1ri4yobcqc.png/30px-Star-2.png",
+ "star-2",
+ )
+ await self.download_img(
+ "https://patchwiki.biligame.com/images/bluearchive/thumb/8/8a/577yv79x1rwxk8efdccpblo0lozl158.png/46px-Star-3.png",
+ "star-3",
+ )
diff --git a/zhenxun/plugins/draw_card/handles/base_handle.py b/zhenxun/plugins/draw_card/handles/base_handle.py
new file mode 100644
index 00000000..1e48482a
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/base_handle.py
@@ -0,0 +1,296 @@
+import asyncio
+import random
+from asyncio.exceptions import TimeoutError
+from datetime import datetime
+from typing import Generic, TypeVar
+
+import aiohttp
+import anyio
+import ujson as json
+from nonebot_plugin_saa import Image
+from nonebot_plugin_saa import Image as SaaImage
+from nonebot_plugin_saa import MessageFactory, Text
+from PIL import Image
+from pydantic import BaseModel, Extra
+
+from zhenxun.configs.path_config import DATA_PATH
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import DRAW_PATH, draw_config
+from ..util import circled_number, cn2py
+
+
+class BaseData(BaseModel, extra=Extra.ignore):
+ name: str # 名字
+ star: int # 星级
+ limited: bool # 限定
+
+ def __eq__(self, other: "BaseData"):
+ return self.name == other.name
+
+ def __hash__(self):
+ return hash(self.name)
+
+ @property
+ def star_str(self) -> str:
+ return "".join(["★" for _ in range(self.star)])
+
+
+class UpChar(BaseData):
+ zoom: float # up提升倍率
+
+
+class UpEvent(BaseModel):
+ title: str # up池标题
+ pool_img: str # up池封面
+ start_time: datetime | None # 开始时间
+ end_time: datetime | None # 结束时间
+ up_char: list[UpChar] # up对象
+
+
+TC = TypeVar("TC", bound="BaseData")
+
+
+class BaseHandle(Generic[TC]):
+ def __init__(self, game_name: str, game_name_cn: str):
+ self.game_name = game_name
+ self.game_name_cn = game_name_cn
+ self.max_star = 1 # 最大星级
+ self.game_card_color: str = "#ffffff"
+ self.data_path = DATA_PATH / "draw_card"
+ self.img_path = DRAW_PATH / f"{self.game_name}"
+ self.up_path = DATA_PATH / "draw_card" / "draw_card_up"
+ self.img_path.mkdir(parents=True, exist_ok=True)
+ self.up_path.mkdir(parents=True, exist_ok=True)
+ self.data_files: list[str] = [f"{self.game_name}.json"]
+
+ async def draw(self, count: int, **kwargs) -> MessageFactory:
+ index2card = self.get_cards(count, **kwargs)
+ cards = [card[0] for card in index2card]
+ result = self.format_result(index2card)
+ gen_img = await self.generate_img(cards)
+ return MessageFactory([SaaImage(gen_img.pic2bytes()), Text(result)])
+
+ # 抽取卡池
+ def get_card(self, **kwargs) -> TC:
+ raise NotImplementedError
+
+ def get_cards(self, count: int, **kwargs) -> list[tuple[TC, int]]:
+ return [(self.get_card(**kwargs), i) for i in range(count)]
+
+ # 获取星级
+ @staticmethod
+ def get_star(star_list: list[int], probability_list: list[float]) -> int:
+ return random.choices(star_list, weights=probability_list, k=1)[0]
+
+ def format_result(self, index2card: list[tuple[TC, int]], **kwargs) -> str:
+ card_list = [card[0] for card in index2card]
+ results = [
+ self.format_star_result(card_list, **kwargs),
+ self.format_max_star(index2card, **kwargs),
+ self.format_max_card(card_list, **kwargs),
+ ]
+ results = [rst for rst in results if rst]
+ return "\n".join(results)
+
+ def format_star_result(self, card_list: list[TC], **kwargs) -> str:
+ star_dict: dict[str, int] = {} # 记录星级及其次数
+
+ card_list_sorted = sorted(card_list, key=lambda c: c.star, reverse=True)
+ for card in card_list_sorted:
+ try:
+ star_dict[card.star_str] += 1
+ except KeyError:
+ star_dict[card.star_str] = 1
+
+ rst = ""
+ for star_str, count in star_dict.items():
+ rst += f"[{star_str}×{count}] "
+ return rst.strip()
+
+ def format_max_star(
+ self, card_list: list[tuple[TC, int]], up_list: list[str] = [], **kwargs
+ ) -> str:
+ up_list = up_list or kwargs.get("up_list", [])
+ rst = ""
+ for card, index in card_list:
+ if card.star == self.max_star:
+ if card.name in up_list:
+ rst += f"第 {index} 抽获取UP {card.name}\n"
+ else:
+ rst += f"第 {index} 抽获取 {card.name}\n"
+ return rst.strip()
+
+ def format_max_card(self, card_list: list[TC], **kwargs) -> str:
+ card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
+
+ for card in card_list:
+ try:
+ card_dict[card] += 1
+ except KeyError:
+ card_dict[card] = 1
+
+ max_count = max(card_dict.values())
+ max_card = list(card_dict.keys())[list(card_dict.values()).index(max_count)]
+ if max_count <= 1:
+ return ""
+ return f"抽取到最多的是{max_card.name},共抽取了{max_count}次"
+
+ async def generate_img(
+ self,
+ cards: list[TC],
+ num_per_line: int = 5,
+ max_per_line: tuple[int, int] = (40, 10),
+ ) -> BuildImage:
+ """
+ 生成统计图片
+ cards: 卡牌列表
+ num_per_line: 单行角色显示数量
+ max_per_line: 当card_list超过一定数值时,更改单行数量
+ """
+ if len(cards) > max_per_line[0]:
+ num_per_line = max_per_line[1]
+ if len(cards) > 90:
+ card_dict: dict[TC, int] = {} # 记录卡牌抽取次数
+ for card in cards:
+ try:
+ card_dict[card] += 1
+ except KeyError:
+ card_dict[card] = 1
+ card_list = list(card_dict)
+ num_list = list(card_dict.values())
+ else:
+ card_list = cards
+ num_list = [1] * len(cards)
+
+ card_imgs: list[BuildImage] = []
+ for card, num in zip(card_list, num_list):
+ card_img = await self.generate_card_img(card)
+ # 数量 > 1 时加数字上标
+ if num > 1:
+ label = circled_number(num)
+ label_w = int(min(card_img.width, card_img.height) / 7)
+ label = label.resize(
+ (
+ int(label_w * label.width / label.height),
+ label_w,
+ ),
+ Image.ANTIALIAS, # type: ignore
+ )
+ await card_img.paste(label)
+
+ card_imgs.append(card_img)
+
+ # img_w = card_imgs[0].width
+ # img_h = card_imgs[0].height
+ # if len(card_imgs) < num_per_line:
+ # w = img_w * len(card_imgs)
+ # else:
+ # w = img_w * num_per_line
+ # h = img_h * math.ceil(len(card_imgs) / num_per_line)
+ # img = BuildImage(w, h, img_w, img_h, color=self.game_card_color)
+ # for card_img in card_imgs:
+ # await img.paste(card_img)
+ return await BuildImage.auto_paste(card_imgs, 10, color=self.game_card_color) # type: ignore
+
+ async def generate_card_img(self, card: TC) -> BuildImage:
+ img = str(self.img_path / f"{cn2py(card.name)}.png")
+ return BuildImage(100, 100, background=img)
+
+ def load_data(self, filename: str = "") -> dict:
+ if not filename:
+ filename = f"{self.game_name}.json"
+ filepath = self.data_path / filename
+ if not filepath.exists():
+ return {}
+ with filepath.open("r", encoding="utf8") as f:
+ return json.load(f)
+
+ def dump_data(self, data: dict, filename: str = ""):
+ if not filename:
+ filename = f"{self.game_name}.json"
+ filepath = self.data_path / filename
+ with filepath.open("w", encoding="utf8") as f:
+ json.dump(data, f, ensure_ascii=False, indent=4)
+
+ def data_exists(self) -> bool:
+ for file in self.data_files:
+ if not (self.data_path / file).exists():
+ return False
+ return True
+
+ def _init_data(self):
+ raise NotImplementedError
+
+ def init_data(self):
+ try:
+ self._init_data()
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn} 导入角色数据错误:{type(e)}:{e}")
+
+ async def _update_info(self):
+ raise NotImplementedError
+
+ def client(self) -> aiohttp.ClientSession:
+ headers = {
+ "User-Agent": '"Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; TencentTraveler 4.0)"'
+ }
+ return aiohttp.ClientSession(headers=headers)
+
+ async def update_info(self):
+ try:
+ async with asyncio.Semaphore(draw_config.SEMAPHORE):
+ async with self.client() as session:
+ self.session = session
+ await self._update_info()
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn} 更新数据错误:{type(e)}:{e}")
+ self.init_data()
+
+ async def get_url(self, url: str) -> str:
+ result = ""
+ retry = 5
+ for i in range(retry):
+ try:
+ async with self.session.get(url, timeout=10) as response:
+ result = await response.text()
+ break
+ except TimeoutError:
+ logger.warning(f"访问 {url} 超时, 重试 {i + 1}/{retry}")
+ await asyncio.sleep(1)
+ return result
+
+ async def download_img(self, url: str, name: str) -> bool:
+ img_path = self.img_path / f"{cn2py(name)}.png"
+ if img_path.exists():
+ return True
+ try:
+ async with self.session.get(url, timeout=10) as response:
+ async with await anyio.open_file(img_path, "wb") as f:
+ await f.write(await response.read())
+ return True
+ except TimeoutError:
+ logger.warning(
+ f"下载 {self.game_name_cn} 图片超时,名称:{name},url:{url}"
+ )
+ return False
+ except:
+ logger.warning(
+ f"下载 {self.game_name_cn} 链接错误,名称:{name},url:{url}"
+ )
+ return False
+
+ async def _reload_pool(self) -> MessageFactory | None:
+ return None
+
+ async def reload_pool(self) -> MessageFactory | None:
+ try:
+ async with self.client() as session:
+ self.session = session
+ return await self._reload_pool()
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn} 重载UP池错误", e=e)
+
+ def reset_count(self, user_id: str) -> bool:
+ return False
diff --git a/zhenxun/plugins/draw_card/handles/fgo_handle.py b/zhenxun/plugins/draw_card/handles/fgo_handle.py
new file mode 100644
index 00000000..5acb8c5f
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/fgo_handle.py
@@ -0,0 +1,223 @@
+import random
+
+import ujson as json
+from lxml import etree
+from PIL import ImageDraw
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle
+
+
+class FgoData(BaseData):
+ pass
+
+
+class FgoChar(FgoData):
+ pass
+
+
+class FgoCard(FgoData):
+ pass
+
+
+class FgoHandle(BaseHandle[FgoData]):
+ def __init__(self):
+ super().__init__("fgo", "命运-冠位指定")
+ self.data_files.append("fgo_card.json")
+ self.max_star = 5
+ self.config = draw_config.fgo
+ self.ALL_CHAR: list[FgoChar] = []
+ self.ALL_CARD: list[FgoCard] = []
+
+ def get_card(self, mode: int = 1) -> FgoData:
+ if mode == 1:
+ star = self.get_star(
+ [8, 7, 6, 5, 4, 3],
+ [
+ self.config.FGO_SERVANT_FIVE_P,
+ self.config.FGO_SERVANT_FOUR_P,
+ self.config.FGO_SERVANT_THREE_P,
+ self.config.FGO_CARD_FIVE_P,
+ self.config.FGO_CARD_FOUR_P,
+ self.config.FGO_CARD_THREE_P,
+ ],
+ )
+ elif mode == 2:
+ star = self.get_star(
+ [5, 4], [self.config.FGO_CARD_FIVE_P, self.config.FGO_CARD_FOUR_P]
+ )
+ else:
+ star = self.get_star(
+ [8, 7, 6],
+ [
+ self.config.FGO_SERVANT_FIVE_P,
+ self.config.FGO_SERVANT_FOUR_P,
+ self.config.FGO_SERVANT_THREE_P,
+ ],
+ )
+ if star > 5:
+ star -= 3
+ chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
+ else:
+ chars = [x for x in self.ALL_CARD if x.star == star and not x.limited]
+ return random.choice(chars)
+
+ def get_cards(self, count: int, **kwargs) -> list[tuple[FgoData, int]]:
+ card_list = [] # 获取所有角色
+ servant_count = 0 # 保底计算
+ card_count = 0 # 保底计算
+ for i in range(count):
+ servant_count += 1
+ card_count += 1
+ if card_count == 9: # 四星卡片保底
+ mode = 2
+ elif servant_count == 10: # 三星从者保底
+ mode = 3
+ else: # 普通抽
+ mode = 1
+ card = self.get_card(mode)
+ if isinstance(card, FgoCard) and card.star > self.max_star - 2:
+ card_count = 0
+ if isinstance(card, FgoChar):
+ servant_count = 0
+ card_list.append((card, i + 1))
+ return card_list
+
+ async def generate_card_img(self, card: FgoData) -> BuildImage:
+ sep_w = 5
+ sep_t = 5
+ sep_b = 20
+ w = 128
+ h = 140
+ bg = BuildImage(w + sep_w * 2, h + sep_t + sep_b)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(w, h, background=img_path)
+ await bg.paste(img, (sep_w, sep_t))
+ # 加名字
+ text = card.name[:6] + "..." if len(card.name) > 7 else card.name
+ font = load_font(fontsize=16)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ (sep_w + (w - text_w) / 2, h + sep_t + (sep_b - text_h) / 2),
+ text,
+ font=font,
+ fill="gray",
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ FgoChar(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited=(
+ True
+ if not (
+ "圣晶石召唤" in value["入手方式"]
+ or "圣晶石召唤(Story卡池)" in value["入手方式"]
+ )
+ else False
+ ),
+ )
+ for value in self.load_data().values()
+ ]
+ self.ALL_CARD = [
+ FgoCard(name=value["名称"], star=int(value["星级"]), limited=False)
+ for value in self.load_data("fgo_card.json").values()
+ ]
+
+ async def _update_info(self):
+ # TODO: fgo获取链接失效
+ fgo_info = {}
+ for i in range(500):
+ url = f"http://fgo.vgtime.com/servant/ajax?card=&wd=&ids=&sort=12777&o=desc&pn={i}"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} page {i} 出错")
+ continue
+ fgo_data = json.loads(result)
+ if int(fgo_data["nums"]) <= 0:
+ break
+ for x in fgo_data["data"]:
+ name = remove_prohibited_str(x["name"])
+ member_dict = {
+ "id": x["id"],
+ "card_id": x["charid"],
+ "头像": x["icon"],
+ "名称": remove_prohibited_str(x["name"]),
+ "职阶": x["classes"],
+ "星级": int(x["star"]),
+ "hp": x["lvmax4hp"],
+ "atk": x["lvmax4atk"],
+ "card_quick": x["cardquick"],
+ "card_arts": x["cardarts"],
+ "card_buster": x["cardbuster"],
+ "宝具": x["tprop"],
+ }
+ fgo_info[name] = member_dict
+ # 更新额外信息
+ for key in fgo_info.keys():
+ url = f'http://fgo.vgtime.com/servant/{fgo_info[key]["id"]}'
+ result = await self.get_url(url)
+ if not result:
+ fgo_info[key]["入手方式"] = ["圣晶石召唤"]
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ continue
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ obtain = dom.xpath(
+ "//table[contains(string(.),'入手方式')]/tr[8]/td[3]/text()"
+ )[0]
+ obtain = str(obtain).strip()
+ if "限时活动免费获取 活动结束后无法获得" in obtain:
+ obtain = ["活动获取"]
+ elif "非限时UP无法获得" in obtain:
+ obtain = ["限时召唤"]
+ else:
+ if "&" in obtain:
+ obtain = obtain.split("&")
+ else:
+ obtain = obtain.split(" ")
+ obtain = [s.strip() for s in obtain if s.strip()]
+ fgo_info[key]["入手方式"] = obtain
+ except IndexError:
+ fgo_info[key]["入手方式"] = ["圣晶石召唤"]
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ self.dump_data(fgo_info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # fgo_card.json
+ fgo_card_info = {}
+ for i in range(500):
+ url = f"http://fgo.vgtime.com/equipment/ajax?wd=&ids=&sort=12958&o=desc&pn={i}"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn}卡牌 page {i} 出错")
+ continue
+ fgo_data = json.loads(result)
+ if int(fgo_data["nums"]) <= 0:
+ break
+ for x in fgo_data["data"]:
+ name = remove_prohibited_str(x["name"])
+ member_dict = {
+ "id": x["id"],
+ "card_id": x["equipid"],
+ "头像": x["icon"],
+ "名称": name,
+ "星级": int(x["star"]),
+ "hp": x["lvmax_hp"],
+ "atk": x["lvmax_atk"],
+ "skill_e": str(x["skill_e"]).split("
")[:-1],
+ }
+ fgo_card_info[name] = member_dict
+ self.dump_data(fgo_card_info, "fgo_card.json")
+ logger.info(f"{self.game_name_cn} 卡牌更新成功")
+ # 下载头像
+ for value in fgo_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ for value in fgo_card_info.values():
+ await self.download_img(value["头像"], value["名称"])
diff --git a/zhenxun/plugins/draw_card/handles/genshin_handle.py b/zhenxun/plugins/draw_card/handles/genshin_handle.py
new file mode 100644
index 00000000..3298e988
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/genshin_handle.py
@@ -0,0 +1,465 @@
+import random
+from datetime import datetime, timedelta
+from urllib.parse import unquote
+
+import dateparser
+import ujson as json
+from lxml import etree
+from nonebot_plugin_saa import Image as SaaImage
+from nonebot_plugin_saa import MessageFactory, Text
+from PIL import Image, ImageDraw
+from pydantic import ValidationError
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..count_manager import GenshinCountManager
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
+
+
+class GenshinData(BaseData):
+ pass
+
+
+class GenshinChar(GenshinData):
+ pass
+
+
+class GenshinArms(GenshinData):
+ pass
+
+
+class GenshinHandle(BaseHandle[GenshinData]):
+ def __init__(self):
+ super().__init__("genshin", "原神")
+ self.data_files.append("genshin_arms.json")
+ self.max_star = 5
+ self.game_card_color = "#ebebeb"
+ self.config = draw_config.genshin
+
+ self.ALL_CHAR: list[GenshinData] = []
+ self.ALL_ARMS: list[GenshinData] = []
+ self.UP_CHAR: UpEvent | None = None
+ self.UP_CHAR_LIST: UpEvent | None = []
+ self.UP_ARMS: UpEvent | None = None
+
+ self.count_manager = GenshinCountManager((10, 90), ("4", "5"), 180)
+
+ # 抽取卡池
+ def get_card(
+ self,
+ pool_name: str,
+ mode: int = 1,
+ add: float = 0.0,
+ is_up: bool = False,
+ card_index: int = 0,
+ ):
+ """
+ mode 1:普通抽 2:四星保底 3:五星保底
+ """
+ if mode == 1:
+ star = self.get_star(
+ [5, 4, 3],
+ [
+ self.config.GENSHIN_FIVE_P + add,
+ self.config.GENSHIN_FOUR_P,
+ self.config.GENSHIN_THREE_P,
+ ],
+ )
+ elif mode == 2:
+ star = self.get_star(
+ [5, 4],
+ [self.config.GENSHIN_G_FIVE_P + add, self.config.GENSHIN_G_FOUR_P],
+ )
+ else:
+ star = 5
+
+ if pool_name == "char":
+ up_event = self.UP_CHAR_LIST[card_index]
+ all_list = self.ALL_CHAR + [
+ x for x in self.ALL_ARMS if x.star == star and x.star < 5
+ ]
+ elif pool_name == "arms":
+ up_event = self.UP_ARMS
+ all_list = self.ALL_ARMS + [
+ x for x in self.ALL_CHAR if x.star == star and x.star < 5
+ ]
+ else:
+ up_event = None
+ all_list = self.ALL_ARMS + self.ALL_CHAR
+
+ acquire_char = None
+ # 是否UP
+ if up_event and star > 3:
+ # 获取up角色列表
+ up_list = [x.name for x in up_event.up_char if x.star == star]
+ # 成功获取up角色
+ if random.random() < 0.5 or is_up:
+ up_name = random.choice(up_list)
+ try:
+ acquire_char = [x for x in all_list if x.name == up_name][0]
+ except IndexError:
+ pass
+ if not acquire_char:
+ chars = [x for x in all_list if x.star == star and not x.limited]
+ acquire_char = random.choice(chars)
+ return acquire_char
+
+ def get_cards(
+ self, count: int, user_id: int, pool_name: str, card_index: int = 0
+ ) -> list[tuple[GenshinData, int]]:
+ card_list = [] # 获取角色列表
+ add = 0.0
+ count_manager = self.count_manager
+ count_manager.check_count(user_id, count) # 检查次数累计
+ pool = self.UP_CHAR_LIST[card_index] if pool_name == "char" else self.UP_ARMS
+ for i in range(count):
+ count_manager.increase(user_id)
+ star = count_manager.check(user_id) # 是否有四星或五星保底
+ if (
+ count_manager.get_user_count(user_id)
+ - count_manager.get_user_five_index(user_id)
+ ) % count_manager.get_max_guarantee() >= 72:
+ add += draw_config.genshin.I72_ADD
+ if star:
+ if star == 4:
+ card = self.get_card(pool_name, 2, add=add, card_index=card_index)
+ else:
+ card = self.get_card(
+ pool_name,
+ 3,
+ add,
+ count_manager.is_up(user_id),
+ card_index=card_index,
+ )
+ else:
+ card = self.get_card(
+ pool_name,
+ 1,
+ add,
+ count_manager.is_up(user_id),
+ card_index=card_index,
+ )
+ # print(f"{count_manager.get_user_count(user_id)}:",
+ # count_manager.get_user_five_index(user_id), star, card.star, add)
+ # 四星角色
+ if card.star == 4:
+ count_manager.mark_four_index(user_id)
+ # 五星角色
+ elif card.star == self.max_star:
+ add = 0
+ count_manager.mark_five_index(user_id) # 记录五星保底
+ count_manager.mark_four_index(user_id) # 记录四星保底
+ if pool and card.name in [
+ x.name for x in pool.up_char if x.star == self.max_star
+ ]:
+ count_manager.set_is_up(user_id, True)
+ else:
+ count_manager.set_is_up(user_id, False)
+ card_list.append((card, count_manager.get_user_count(user_id)))
+ return card_list
+
+ async def generate_card_img(self, card: GenshinData) -> BuildImage:
+ sep_w = 10
+ sep_h = 5
+ frame_w = 112
+ frame_h = 132
+ img_w = 106
+ img_h = 106
+ bg = BuildImage(frame_w + sep_w * 2, frame_h + sep_h * 2, color="#EBEBEB")
+ frame_path = str(self.img_path / "avatar_frame.png")
+ frame = Image.open(frame_path)
+ # 加名字
+ text = card.name
+ font = load_font(fontsize=14)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(frame)
+ draw.text(
+ ((frame_w - text_w) / 2, frame_h - 15 - text_h / 2),
+ text,
+ font=font,
+ fill="gray",
+ )
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ if isinstance(card, GenshinArms):
+ # 武器卡背景不是透明的,切去上方两个圆弧
+ r = 12
+ circle = Image.new("L", (r * 2, r * 2), 0)
+ alpha = Image.new("L", img.size, 255)
+ alpha.paste(circle, (-r - 3, -r - 3)) # 左上角
+ alpha.paste(circle, (img_h - r + 3, -r - 3)) # 右上角
+ img.markImg.putalpha(alpha)
+ star_path = str(self.img_path / f"{card.star}_star.png")
+ star = Image.open(star_path)
+ await bg.paste(frame, (sep_w, sep_h))
+ await bg.paste(img, (sep_w + 3, sep_h + 3))
+ await bg.paste(star, (sep_w + int((frame_w - star.width) / 2), sep_h - 6))
+ return bg
+
+ def format_pool_info(self, pool_name: str, card_index: int = 0) -> str:
+ info = ""
+ up_event = None
+ if pool_name == "char":
+ up_event = self.UP_CHAR_LIST[card_index]
+ elif pool_name == "arms":
+ up_event = self.UP_ARMS
+ if up_event:
+ star5_list = [x.name for x in up_event.up_char if x.star == 5]
+ star4_list = [x.name for x in up_event.up_char if x.star == 4]
+ if star5_list:
+ info += f"五星UP:{' '.join(star5_list)}\n"
+ if star4_list:
+ info += f"四星UP:{' '.join(star4_list)}\n"
+ info = f"当前up池:{up_event.title}\n{info}"
+ return info.strip()
+
+ async def draw(
+ self, count: int, user_id: int, pool_name: str = "", **kwargs
+ ) -> Text | MessageFactory:
+ card_index = 0
+ if "1" in pool_name:
+ card_index = 1
+ pool_name = pool_name.replace("1", "")
+ index2cards = self.get_cards(count, user_id, pool_name, card_index)
+ cards = [card[0] for card in index2cards]
+ up_event = None
+ if pool_name == "char":
+ if card_index == 1 and len(self.UP_CHAR_LIST) == 1:
+ return Text("当前没有第二个角色UP池")
+ up_event = self.UP_CHAR_LIST[card_index]
+ elif pool_name == "arms":
+ up_event = self.UP_ARMS
+ up_list = [x.name for x in up_event.up_char] if up_event else []
+ result = self.format_star_result(cards)
+ result += (
+ "\n" + max_star_str
+ if (max_star_str := self.format_max_star(index2cards, up_list=up_list))
+ else ""
+ )
+ result += f"\n距离保底发还剩 {self.count_manager.get_user_guarantee_count(user_id)} 抽"
+ # result += "\n【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】"
+ pool_info = self.format_pool_info(pool_name, card_index)
+ img = await self.generate_img(cards)
+ bk = BuildImage(img.width, img.height + 50, font_size=20, color="#ebebeb")
+ await bk.paste(img)
+ await bk.text(
+ (0, img.height + 10),
+ "【五星:0.6%,四星:5.1%,第72抽开始五星概率每抽加0.585%】",
+ )
+ return MessageFactory([Text(pool_info), SaaImage(bk.pic2bytes()), Text(result)])
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ GenshinChar(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited=value["常驻/限定"] == "限定UP",
+ )
+ for key, value in self.load_data().items()
+ if "旅行者" not in key
+ ]
+ self.ALL_ARMS = [
+ GenshinArms(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited="祈愿" not in value["获取途径"],
+ )
+ for value in self.load_data("genshin_arms.json").values()
+ ]
+ self.load_up_char()
+
+ def load_up_char(self):
+ try:
+ data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
+ self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char", {})))
+ self.UP_CHAR_LIST.append(UpEvent.parse_obj(data.get("char1", {})))
+ self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
+ except ValidationError:
+ logger.warning(f"{self.game_name}_up_char 解析出错")
+
+ def dump_up_char(self):
+ if self.UP_CHAR_LIST and self.UP_ARMS:
+ data = {
+ "char": json.loads(self.UP_CHAR_LIST[0].json()),
+ "arms": json.loads(self.UP_ARMS.json()),
+ }
+ if len(self.UP_CHAR_LIST) > 1:
+ data["char1"] = json.loads(self.UP_CHAR_LIST[1].json())
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+
+ async def _update_info(self):
+ # genshin.json
+ char_info = {}
+ url = "https://wiki.biligame.com/ys/角色筛选"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[1]/a/@title")[0]
+ avatar = char.xpath("./td[1]/a/img/@srcset")[0]
+ star = char.xpath("./td[3]/text()")[0]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).strip()[:1]),
+ }
+ char_info[member_dict["名称"]] = member_dict
+ # 更新额外信息
+ for key in char_info.keys():
+ result = await self.get_url(f"https://wiki.biligame.com/ys/{key}")
+ if not result:
+ char_info[key]["常驻/限定"] = "未知"
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ continue
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ limit = dom.xpath(
+ "//table[contains(string(.),'常驻/限定')]/tbody/tr[6]/td/text()"
+ )[0]
+ char_info[key]["常驻/限定"] = str(limit).strip()
+ except IndexError:
+ char_info[key]["常驻/限定"] = "未知"
+ logger.warning(f"{self.game_name_cn} 获取额外信息错误 {key}")
+ self.dump_data(char_info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # genshin_arms.json
+ arms_info = {}
+ url = "https://wiki.biligame.com/ys/武器图鉴"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[1]/a/@title")[0]
+ avatar = char.xpath("./td[1]/a/img/@srcset")[0]
+ star = char.xpath("./td[4]/img/@alt")[0]
+ sources = str(char.xpath("./td[5]/text()")[0]).split(",")
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).strip()[:1]),
+ "获取途径": [s.strip() for s in sources if s.strip()],
+ }
+ arms_info[member_dict["名称"]] = member_dict
+ self.dump_data(arms_info, "genshin_arms.json")
+ logger.info(f"{self.game_name_cn} 武器更新成功")
+ # 下载头像
+ for value in char_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ for value in arms_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载星星
+ idx = 1
+ YS_URL = "https://patchwiki.biligame.com/images/ys"
+ for url in [
+ "/1/13/7xzg7tgf8dsr2hjpmdbm5gn9wvzt2on.png",
+ "/b/bc/sd2ige6d7lvj7ugfumue3yjg8gyi0d1.png",
+ "/e/ec/l3mnhy56pyailhn3v7r873htf2nofau.png",
+ "/9/9c/sklp02ffk3aqszzvh8k1c3139s0awpd.png",
+ "/c/c7/qu6xcndgj6t14oxvv7yz2warcukqv1m.png",
+ ]:
+ await self.download_img(YS_URL + url, f"{idx}_star")
+ idx += 1
+ # 下载头像框
+ await self.download_img(
+ YS_URL + "/2/2e/opbcst4xbtcq0i4lwerucmosawn29ti.png", f"avatar_frame"
+ )
+ await self.update_up_char()
+
+ async def update_up_char(self):
+ self.UP_CHAR_LIST = []
+ url = "https://wiki.biligame.com/ys/祈愿"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取祈愿页面出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ tables = dom.xpath(
+ "//div[@class='mw-parser-output']/div[@class='row']/div/table[@class='wikitable']/tbody"
+ )
+ if not tables or len(tables) < 2:
+ logger.warning(f"{self.game_name_cn}获取活动祈愿出错")
+ return
+ try:
+ for index, table in enumerate(tables):
+ title = table.xpath("./tr[1]/th/img/@title")[0]
+ title = str(title).split("」")[0] + "」" if "」" in title else title
+ pool_img = str(table.xpath("./tr[1]/th/img/@srcset")[0]).split(" ")[-2]
+ time = table.xpath("./tr[2]/td/text()")[0]
+ star5_list = table.xpath("./tr[3]/td/a/@title")
+ star4_list = table.xpath("./tr[4]/td/a/@title")
+ start, end = str(time).split("~")
+ start_time = dateparser.parse(start)
+ end_time = dateparser.parse(end)
+ if not start_time and end_time:
+ start_time = end_time - timedelta(days=20)
+ if start_time and end_time and start_time <= datetime.now() <= end_time:
+ up_event = UpEvent(
+ title=title,
+ pool_img=pool_img,
+ start_time=start_time,
+ end_time=end_time,
+ up_char=[
+ UpChar(name=name, star=5, limited=False, zoom=50)
+ for name in star5_list
+ ]
+ + [
+ UpChar(name=name, star=4, limited=False, zoom=50)
+ for name in star4_list
+ ],
+ )
+ if "神铸赋形" not in title:
+ self.UP_CHAR_LIST.append(up_event)
+ else:
+ self.UP_ARMS = up_event
+ if self.UP_CHAR_LIST and self.UP_ARMS:
+ self.dump_up_char()
+ char_title = " & ".join([x.title for x in self.UP_CHAR_LIST])
+ logger.info(
+ f"成功获取{self.game_name_cn}当前up信息...当前up池: {char_title} & {self.UP_ARMS.title}"
+ )
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
+
+ def reset_count(self, user_id: str) -> bool:
+ self.count_manager.reset(user_id)
+ return True
+
+ async def _reload_pool(self) -> MessageFactory | None:
+ await self.update_up_char()
+ self.load_up_char()
+ if self.UP_CHAR_LIST and self.UP_ARMS:
+ if len(self.UP_CHAR_LIST) > 1:
+ return MessageFactory(
+ [
+ Text(
+ f"重载成功!\n当前UP池子:{self.UP_CHAR_LIST[0].title} & {self.UP_CHAR_LIST[1].title} & {self.UP_ARMS.title}"
+ ),
+ Image(self.UP_CHAR_LIST[0].pool_img),
+ Image(self.UP_CHAR_LIST[1].pool_img),
+ Image(self.UP_ARMS.pool_img),
+ ]
+ )
+ return MessageFactory(
+ [
+ Text(
+ f"重载成功!\n当前UP池子:{char_title} & {self.UP_ARMS.title}"
+ ),
+ Image(self.UP_CHAR_LIST[0].pool_img),
+ Image(self.UP_ARMS.pool_img),
+ ]
+ )
diff --git a/zhenxun/plugins/draw_card/handles/guardian_handle.py b/zhenxun/plugins/draw_card/handles/guardian_handle.py
new file mode 100644
index 00000000..c24caa0b
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/guardian_handle.py
@@ -0,0 +1,399 @@
+import random
+import re
+from datetime import datetime
+from urllib.parse import unquote
+
+import dateparser
+import ujson as json
+from lxml import etree
+from nonebot_plugin_saa import Image, MessageFactory, Text
+from PIL import ImageDraw
+from pydantic import ValidationError
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
+
+
+class GuardianData(BaseData):
+ pass
+
+
+class GuardianChar(GuardianData):
+ pass
+
+
+class GuardianArms(GuardianData):
+ pass
+
+
+class GuardianHandle(BaseHandle[GuardianData]):
+ def __init__(self):
+ super().__init__("guardian", "坎公骑冠剑")
+ self.data_files.append("guardian_arms.json")
+ self.config = draw_config.guardian
+
+ self.ALL_CHAR: list[GuardianChar] = []
+ self.ALL_ARMS: list[GuardianArms] = []
+ self.UP_CHAR: UpEvent | None = None
+ self.UP_ARMS: UpEvent | None = None
+
+ def get_card(self, pool_name: str, mode: int = 1) -> GuardianData:
+ if pool_name == "char":
+ if mode == 1:
+ star = self.get_star(
+ [3, 2, 1],
+ [
+ self.config.GUARDIAN_THREE_CHAR_P,
+ self.config.GUARDIAN_TWO_CHAR_P,
+ self.config.GUARDIAN_ONE_CHAR_P,
+ ],
+ )
+ else:
+ star = self.get_star(
+ [3, 2],
+ [
+ self.config.GUARDIAN_THREE_CHAR_P,
+ self.config.GUARDIAN_TWO_CHAR_P,
+ ],
+ )
+ up_event = self.UP_CHAR
+ self.max_star = 3
+ all_data = self.ALL_CHAR
+ else:
+ if mode == 1:
+ star = self.get_star(
+ [5, 4, 3, 2],
+ [
+ self.config.GUARDIAN_FIVE_ARMS_P,
+ self.config.GUARDIAN_FOUR_ARMS_P,
+ self.config.GUARDIAN_THREE_ARMS_P,
+ self.config.GUARDIAN_TWO_ARMS_P,
+ ],
+ )
+ else:
+ star = self.get_star(
+ [5, 4],
+ [
+ self.config.GUARDIAN_FIVE_ARMS_P,
+ self.config.GUARDIAN_FOUR_ARMS_P,
+ ],
+ )
+ up_event = self.UP_ARMS
+ self.max_star = 5
+ all_data = self.ALL_ARMS
+
+ acquire_char = None
+ # 是否UP
+ if up_event and star == self.max_star and pool_name:
+ # 获取up角色列表
+ up_list = [x.name for x in up_event.up_char if x.star == star]
+ # 成功获取up角色
+ if random.random() < 0.5:
+ up_name = random.choice(up_list)
+ try:
+ acquire_char = [x for x in all_data if x.name == up_name][0]
+ except IndexError:
+ pass
+ if not acquire_char:
+ chars = [x for x in all_data if x.star == star and not x.limited]
+ acquire_char = random.choice(chars)
+ return acquire_char
+
+ def get_cards(self, count: int, pool_name: str) -> list[tuple[GuardianData, int]]:
+ card_list = []
+ card_count = 0 # 保底计算
+ for i in range(count):
+ card_count += 1
+ # 十连保底
+ if card_count == 10:
+ card = self.get_card(pool_name, 2)
+ card_count = 0
+ else:
+ card = self.get_card(pool_name, 1)
+ if card.star > self.max_star - 2:
+ card_count = 0
+ card_list.append((card, i + 1))
+ return card_list
+
+ def format_pool_info(self, pool_name: str) -> str:
+ info = ""
+ up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
+ if up_event:
+ if pool_name == "char":
+ up_list = [x.name for x in up_event.up_char if x.star == 3]
+ info += f'三星UP:{" ".join(up_list)}\n'
+ else:
+ up_list = [x.name for x in up_event.up_char if x.star == 5]
+ info += f'五星UP:{" ".join(up_list)}\n'
+ info = f"当前up池:{up_event.title}\n{info}"
+ return info.strip()
+
+ async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory:
+ index2card = self.get_cards(count, pool_name)
+ cards = [card[0] for card in index2card]
+ up_event = self.UP_CHAR if pool_name == "char" else self.UP_ARMS
+ up_list = [x.name for x in up_event.up_char] if up_event else []
+ result = self.format_result(index2card, up_list=up_list)
+ pool_info = self.format_pool_info(pool_name)
+ img = await self.generate_img(cards)
+ return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
+
+ async def generate_card_img(self, card: GuardianData) -> BuildImage:
+ sep_w = 1
+ sep_h = 1
+ block_w = 170
+ block_h = 90
+ img_w = 90
+ img_h = 90
+ if isinstance(card, GuardianChar):
+ block_color = "#2e2923"
+ font_color = "#e2ccad"
+ star_w = 90
+ star_h = 30
+ star_name = f"{card.star}_star.png"
+ frame_path = ""
+ else:
+ block_color = "#EEE4D5"
+ font_color = "#A65400"
+ star_w = 45
+ star_h = 45
+ star_name = f"{card.star}_star_rank.png"
+ frame_path = str(self.img_path / "avatar_frame.png")
+ bg = BuildImage(block_w + sep_w * 2, block_h + sep_h * 2, color="#F6F4ED")
+ block = BuildImage(block_w, block_h, color=block_color)
+ star_path = str(self.img_path / star_name)
+ star = BuildImage(star_w, star_h, background=star_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ await block.paste(img, (0, 0))
+ if frame_path:
+ frame = BuildImage(img_w, img_h, background=frame_path)
+ await block.paste(frame, (0, 0))
+ await block.paste(
+ star,
+ (int((block_w + img_w - star_w) / 2), block_h - star_h - 30),
+ )
+ # 加名字
+ text = card.name[:4] + "..." if len(card.name) > 5 else card.name
+ font = load_font(fontsize=14)
+ text_w, _ = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(block.markImg)
+ draw.text(
+ ((block_w + img_w - text_w) / 2, 55),
+ text,
+ font=font,
+ fill=font_color,
+ )
+ await bg.paste(block, (sep_w, sep_h))
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ GuardianChar(name=value["名称"], star=int(value["星级"]), limited=False)
+ for value in self.load_data().values()
+ ]
+ self.ALL_ARMS = [
+ GuardianArms(name=value["名称"], star=int(value["星级"]), limited=False)
+ for value in self.load_data("guardian_arms.json").values()
+ ]
+ self.load_up_char()
+
+ def load_up_char(self):
+ try:
+ data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
+ self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
+ self.UP_ARMS = UpEvent.parse_obj(data.get("arms", {}))
+ except ValidationError:
+ logger.warning(f"{self.game_name}_up_char 解析出错")
+
+ def dump_up_char(self):
+ if self.UP_CHAR and self.UP_ARMS:
+ data = {
+ "char": json.loads(self.UP_CHAR.json()),
+ "arms": json.loads(self.UP_ARMS.json()),
+ }
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+
+ async def _update_info(self):
+ # guardian.json
+ guardian_info = {}
+ url = "https://wiki.biligame.com/gt/英雄筛选表"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ # name = char.xpath("./td[1]/a/@title")[0]
+ # avatar = char.xpath("./td[1]/a/img/@src")[0]
+ # star = char.xpath("./td[1]/span/img/@alt")[0]
+ name = char.xpath("./th[1]/a[1]/@title")[0]
+ avatar = char.xpath("./th[1]/a/img/@src")[0]
+ star = char.xpath("./th[1]/span/img/@alt")[0]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar)),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).split(" ")[0].replace("Rank", "")),
+ }
+ guardian_info[member_dict["名称"]] = member_dict
+ self.dump_data(guardian_info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # guardian_arms.json
+ guardian_arms_info = {}
+ url = "https://wiki.biligame.com/gt/武器"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 武器出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[2]/a/@title")[0]
+ avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
+ url = char.xpath("./td[3]/img/@srcset")[0]
+ if r := re.search(r"Rank-mini-star_(\d).png", url):
+ star = r.group(1)
+ else:
+ continue
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar)),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).strip()),
+ }
+ guardian_arms_info[member_dict["名称"]] = member_dict
+ self.dump_data(guardian_arms_info, "guardian_arms.json")
+ logger.info(f"{self.game_name_cn} 武器更新成功")
+ url = "https://wiki.biligame.com/gt/盾牌"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 盾牌出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath(
+ "//div[@class='resp-tabs-container']/div[2]/div/table[1]/tbody/tr"
+ )
+ for char in char_list:
+ try:
+ name = char.xpath("./td[2]/a/@title")[0]
+ avatar = char.xpath("./td[1]/div/div/div/a/img/@src")[0]
+ star = char.xpath("./td[3]/text()")[0]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar)),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).strip()),
+ }
+ guardian_arms_info[member_dict["名称"]] = member_dict
+ self.dump_data(guardian_arms_info, "guardian_arms.json")
+ logger.info(f"{self.game_name_cn} 盾牌更新成功")
+ # 下载头像
+ for value in guardian_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ for value in guardian_arms_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载星星
+ idx = 1
+ GT_URL = "https://patchwiki.biligame.com/images/gt"
+ for url in [
+ "/4/4b/ardr3bi2yf95u4zomm263tc1vke6i3i.png",
+ "/5/55/6vow7lh76gzus6b2g9cfn325d1sugca.png",
+ "/b/b9/du8egrd2vyewg0cuyra9t8jh0srl0ds.png",
+ ]:
+ await self.download_img(GT_URL + url, f"{idx}_star")
+ idx += 1
+ # 另一种星星
+ idx = 1
+ for url in [
+ "/6/66/4e2tfa9kvhfcbikzlyei76i9crva145.png",
+ "/1/10/r9ihsuvycgvsseyneqz4xs22t53026m.png",
+ "/7/7a/o0k86ru9k915y04azc26hilxead7xp1.png",
+ "/c/c9/rxz99asysz0rg391j3b02ta09mnpa7v.png",
+ "/2/2a/sfxz0ucv1s6ewxveycz9mnmrqs2rw60.png",
+ ]:
+ await self.download_img(GT_URL + url, f"{idx}_star_rank")
+ idx += 1
+ # 头像框
+ await self.download_img(
+ GT_URL + "/8/8e/ogbqslbhuykjhnc8trtoa0p0nhfzohs.png", f"avatar_frame"
+ )
+ await self.update_up_char()
+
+ async def update_up_char(self):
+ url = "https://wiki.biligame.com/gt/首页"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取公告出错")
+ return
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ announcement = dom.xpath(
+ "//div[@class='mw-parser-output']/div/div[3]/div[2]/div/div[2]/div[3]"
+ )[0]
+ title = announcement.xpath("./font/p/b/text()")[0]
+ match = re.search(r"从(.*?)开始.*?至(.*?)结束", title)
+ if not match:
+ logger.warning(f"{self.game_name_cn}找不到UP时间")
+ return
+ start, end = match.groups()
+ start_time = dateparser.parse(start.replace("月", "/").replace("日", ""))
+ end_time = dateparser.parse(end.replace("月", "/").replace("日", ""))
+ if not (start_time and end_time) or not (
+ start_time <= datetime.now() <= end_time
+ ):
+ return
+ divs = announcement.xpath("./font/div")
+ char_index = 0
+ arms_index = 0
+ for index, div in enumerate(divs):
+ if div.xpath("string(.)") == "角色":
+ char_index = index
+ elif div.xpath("string(.)") == "武器":
+ arms_index = index
+ chars = divs[char_index + 1 : arms_index]
+ arms = divs[arms_index + 1 :]
+ up_chars = []
+ up_arms = []
+ for char in chars:
+ name = char.xpath("./p/a/@title")[0]
+ up_chars.append(UpChar(name=name, star=3, limited=False, zoom=0))
+ for arm in arms:
+ name = arm.xpath("./p/a/@title")[0]
+ up_arms.append(UpChar(name=name, star=5, limited=False, zoom=0))
+ self.UP_CHAR = UpEvent(
+ title=title,
+ pool_img="",
+ start_time=start_time,
+ end_time=end_time,
+ up_char=up_chars,
+ )
+ self.UP_ARMS = UpEvent(
+ title=title,
+ pool_img="",
+ start_time=start_time,
+ end_time=end_time,
+ up_char=up_arms,
+ )
+ self.dump_up_char()
+ logger.info(f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}")
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn}UP更新出错 {type(e)}:{e}")
+
+ async def _reload_pool(self) -> MessageFactory | None:
+ await self.update_up_char()
+ self.load_up_char()
+ if self.UP_CHAR and self.UP_ARMS:
+ return MessageFactory(
+ [Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}")]
+ )
diff --git a/zhenxun/plugins/draw_card/handles/onmyoji_handle.py b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py
new file mode 100644
index 00000000..25d05c38
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/onmyoji_handle.py
@@ -0,0 +1,178 @@
+import random
+
+import ujson as json
+from lxml import etree
+from PIL import Image, ImageDraw
+from PIL.Image import Image as IMG
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle
+
+
+class OnmyojiChar(BaseData):
+ @property
+ def star_str(self) -> str:
+ return ["N", "R", "SR", "SSR", "SP"][self.star - 1]
+
+
+class OnmyojiHandle(BaseHandle[OnmyojiChar]):
+ def __init__(self):
+ super().__init__("onmyoji", "阴阳师")
+ self.max_star = 5
+ self.config = draw_config.onmyoji
+ self.ALL_CHAR: list[OnmyojiChar] = []
+
+ def get_card(self, **kwargs) -> OnmyojiChar:
+ star = self.get_star(
+ [5, 4, 3, 2],
+ [
+ self.config.ONMYOJI_SP,
+ self.config.ONMYOJI_SSR,
+ self.config.ONMYOJI_SR,
+ self.config.ONMYOJI_R,
+ ],
+ )
+ chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
+ return random.choice(chars)
+
+ def format_max_star(self, card_list: list[tuple[OnmyojiChar, int]]) -> str:
+ rst = ""
+ for card, index in card_list:
+ if card.star == self.max_star:
+ rst += f"第 {index} 抽获取SP {card.name}\n"
+ elif card.star == self.max_star - 1:
+ rst += f"第 {index} 抽获取SSR {card.name}\n"
+ return rst.strip()
+
+ @staticmethod
+ def star_label(star: int) -> IMG:
+ text, color1, color2 = [
+ ("N", "#7E7E82", "#F5F6F7"),
+ ("R", "#014FA8", "#37C6FD"),
+ ("SR", "#6E0AA4", "#E94EFD"),
+ ("SSR", "#E5511D", "#FAF905"),
+ ("SP", "#FA1F2D", "#FFBBAF"),
+ ][star - 1]
+ w = 200
+ h = 110
+ # 制作渐变色图片
+ base = Image.new("RGBA", (w, h), color1)
+ top = Image.new("RGBA", (w, h), color2)
+ mask = Image.new("L", (w, h))
+ mask_data = []
+ for y in range(h):
+ mask_data.extend([int(255 * (y / h))] * w)
+ mask.putdata(mask_data)
+ base.paste(top, (0, 0), mask)
+ # 透明图层
+ font = load_font("gorga.otf", 100)
+ alpha = Image.new("L", (w, h))
+ draw = ImageDraw.Draw(alpha)
+ draw.text((20, -30), text, fill="white", font=font)
+ base.putalpha(alpha)
+ # stroke
+ bg = Image.new("RGBA", (w, h))
+ draw = ImageDraw.Draw(bg)
+ draw.text(
+ (20, -30),
+ text,
+ font=font,
+ fill="gray",
+ stroke_width=3,
+ stroke_fill="gray",
+ )
+ bg.paste(base, (0, 0), base)
+ return bg
+
+ async def generate_img(self, card_list: list[OnmyojiChar]) -> BuildImage:
+ return await super().generate_img(card_list, num_per_line=10)
+
+ async def generate_card_img(self, card: OnmyojiChar) -> BuildImage:
+ bg = BuildImage(73, 240, color="#F1EFE9")
+ img_path = str(self.img_path / f"{cn2py(card.name)}_mark_btn.png")
+ img = BuildImage(0, 0, background=img_path)
+ img = Image.open(img_path).convert("RGBA")
+ label = self.star_label(card.star).resize((60, 33), Image.ANTIALIAS)
+ await bg.paste(img, (0, 0))
+ await bg.paste(label, (0, 135))
+ font = load_font("msyh.ttf", 16)
+ draw = ImageDraw.Draw(bg.markImg)
+ text = "\n".join([t for t in card.name[:4]])
+ _, text_h = font.getsize_multiline(text, spacing=0)
+ draw.text(
+ (40, 150 + (90 - text_h) / 2), text, font=font, fill="gray", spacing=0
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ OnmyojiChar(
+ name=value["名称"],
+ star=["N", "R", "SR", "SSR", "SP"].index(value["星级"]) + 1,
+ limited=(
+ True
+ if key
+ in [
+ "奴良陆生",
+ "卖药郎",
+ "鬼灯",
+ "阿香",
+ "蜜桃&芥子",
+ "犬夜叉",
+ "杀生丸",
+ "桔梗",
+ "朽木露琪亚",
+ "黑崎一护",
+ "灶门祢豆子",
+ "灶门炭治郎",
+ ]
+ else False
+ ),
+ )
+ for key, value in self.load_data().items()
+ ]
+
+ async def _update_info(self):
+ info = {}
+ url = "https://yys.res.netease.com/pc/zt/20161108171335/js/app/all_shishen.json?v74="
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ data = json.loads(result)
+ for x in data:
+ name = remove_prohibited_str(x["name"])
+ member_dict = {
+ "id": x["id"],
+ "名称": name,
+ "星级": x["level"],
+ }
+ info[name] = member_dict
+ # logger.info(f"{name} is update...")
+ # 更新头像
+ for key in info.keys():
+ url = f'https://yys.163.com/shishen/{info[key]["id"]}.html'
+ result = await self.get_url(url)
+ if not result:
+ info[key]["头像"] = ""
+ continue
+ try:
+ dom = etree.HTML(result, etree.HTMLParser())
+ avatar = dom.xpath("//div[@class='pic_wrap']/img/@src")[0]
+ avatar = "https:" + avatar
+ info[key]["头像"] = avatar
+ except IndexError:
+ info[key]["头像"] = ""
+ logger.warning(f"{self.game_name_cn} 获取头像错误 {key}")
+ self.dump_data(info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # 下载头像
+ for value in info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载书签形式的头像
+ url = f"https://yys.res.netease.com/pc/zt/20161108171335/data/mark_btn/{value['id']}.png"
+ await self.download_img(url, value["名称"] + "_mark_btn")
diff --git a/zhenxun/plugins/draw_card/handles/pcr_handle.py b/zhenxun/plugins/draw_card/handles/pcr_handle.py
new file mode 100644
index 00000000..666a6842
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/pcr_handle.py
@@ -0,0 +1,149 @@
+import random
+from urllib.parse import unquote
+
+from lxml import etree
+from PIL import ImageDraw
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle
+
+
+class PcrChar(BaseData):
+ pass
+
+
+class PcrHandle(BaseHandle[PcrChar]):
+ def __init__(self):
+ super().__init__("pcr", "公主连结")
+ self.max_star = 3
+ self.config = draw_config.pcr
+ self.ALL_CHAR: list[PcrChar] = []
+
+ def get_card(self, mode: int = 1) -> PcrChar:
+ if mode == 2:
+ star = self.get_star(
+ [3, 2], [self.config.PCR_G_THREE_P, self.config.PCR_G_TWO_P]
+ )
+ else:
+ star = self.get_star(
+ [3, 2, 1],
+ [self.config.PCR_THREE_P, self.config.PCR_TWO_P, self.config.PCR_ONE_P],
+ )
+ chars = [x for x in self.ALL_CHAR if x.star == star and not x.limited]
+ return random.choice(chars)
+
+ def get_cards(self, count: int, **kwargs) -> list[tuple[PcrChar, int]]:
+ card_list = []
+ card_count = 0 # 保底计算
+ for i in range(count):
+ card_count += 1
+ # 十连保底
+ if card_count == 10:
+ card = self.get_card(2)
+ card_count = 0
+ else:
+ card = self.get_card(1)
+ if card.star > self.max_star - 2:
+ card_count = 0
+ card_list.append((card, i + 1))
+ return card_list
+
+ async def generate_card_img(self, card: PcrChar) -> BuildImage:
+ sep_w = 5
+ sep_h = 5
+ star_h = 15
+ img_w = 90
+ img_h = 90
+ font_h = 20
+ bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
+ star_path = str(self.img_path / "star.png")
+ star = BuildImage(star_h, star_h, background=star_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ await bg.paste(img, (sep_w, sep_h))
+ for i in range(card.star):
+ await bg.paste(star, (sep_w + img_w - star_h * (i + 1), sep_h))
+ # 加名字
+ text = card.name[:5] + "..." if len(card.name) > 6 else card.name
+ font = load_font(fontsize=14)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
+ text,
+ font=font,
+ fill="gray",
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ PcrChar(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited=True if "(" in key else False,
+ )
+ for key, value in self.load_data().items()
+ ]
+
+ async def _update_info(self):
+ info = {}
+ if draw_config.PCR_TAI:
+ url = "https://wiki.biligame.com/pcr/角色图鉴"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ # TODO: PCR台湾更新失败
+ char_list = dom.xpath(
+ "//*[@id='CardSelectCard']/div[@class='unit-icon trcard']"
+ )
+ for char in char_list:
+ try:
+ name = char.xpath("./a/@title")[0]
+ avatar = char.xpath("./a/img/@srcset")[0]
+ star = len(char.xpath("./div[1]/img"))
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "星级": star,
+ }
+ info[member_dict["名称"]] = member_dict
+ else:
+ url = "https://wiki.biligame.com/pcr/角色筛选表"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[1]/a/@title")[0]
+ avatar = char.xpath("./td[1]/a/img/@srcset")[0]
+ star = char.xpath("./td[4]/text()")[0]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "星级": int(str(star).strip()),
+ }
+ info[member_dict["名称"]] = member_dict
+ self.dump_data(info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # 下载头像
+ for value in info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载星星
+ await self.download_img(
+ "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
+ "star",
+ )
diff --git a/zhenxun/plugins/draw_card/handles/pretty_handle.py b/zhenxun/plugins/draw_card/handles/pretty_handle.py
new file mode 100644
index 00000000..ecc2dfe3
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/pretty_handle.py
@@ -0,0 +1,422 @@
+import random
+import re
+from datetime import datetime
+from urllib.parse import unquote
+
+import dateparser
+import ujson as json
+from bs4 import BeautifulSoup
+from lxml import etree
+from nonebot_plugin_saa import Image, MessageFactory, Text
+from PIL import ImageDraw
+from pydantic import ValidationError
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
+
+
+class PrettyData(BaseData):
+ pass
+
+
+class PrettyChar(PrettyData):
+ pass
+
+
+class PrettyCard(PrettyData):
+ @property
+ def star_str(self) -> str:
+ return ["R", "SR", "SSR"][self.star - 1]
+
+
+class PrettyHandle(BaseHandle[PrettyData]):
+ def __init__(self):
+ super().__init__("pretty", "赛马娘")
+ self.data_files.append("pretty_card.json")
+ self.max_star = 3
+ self.game_card_color = "#eff2f5"
+ self.config = draw_config.pretty
+
+ self.ALL_CHAR: list[PrettyChar] = []
+ self.ALL_CARD: list[PrettyCard] = []
+ self.UP_CHAR: UpEvent | None = None
+ self.UP_CARD: UpEvent | None = None
+
+ def get_card(self, pool_name: str, mode: int = 1) -> PrettyData:
+ if mode == 1:
+ star = self.get_star(
+ [3, 2, 1],
+ [
+ self.config.PRETTY_THREE_P,
+ self.config.PRETTY_TWO_P,
+ self.config.PRETTY_ONE_P,
+ ],
+ )
+ else:
+ star = self.get_star(
+ [3, 2], [self.config.PRETTY_THREE_P, self.config.PRETTY_TWO_P]
+ )
+ up_pool = None
+ if pool_name == "char":
+ up_pool = self.UP_CHAR
+ all_list = self.ALL_CHAR
+ else:
+ up_pool = self.UP_CARD
+ all_list = self.ALL_CARD
+
+ all_char = [x for x in all_list if x.star == star and not x.limited]
+ acquire_char = None
+ # 有UP池子
+ if up_pool and star in [x.star for x in up_pool.up_char]:
+ up_list = [x.name for x in up_pool.up_char if x.star == star]
+ # 抽到UP
+ if random.random() < 1 / len(all_char) * (0.7 / 0.1385):
+ up_name = random.choice(up_list)
+ try:
+ acquire_char = [x for x in all_list if x.name == up_name][0]
+ except IndexError:
+ pass
+ if not acquire_char:
+ acquire_char = random.choice(all_char)
+ return acquire_char
+
+ def get_cards(self, count: int, pool_name: str) -> list[tuple[PrettyData, int]]:
+ card_list = []
+ card_count = 0 # 保底计算
+ for i in range(count):
+ card_count += 1
+ # 十连保底
+ if card_count == 10:
+ card = self.get_card(pool_name, 2)
+ card_count = 0
+ else:
+ card = self.get_card(pool_name, 1)
+ if card.star > self.max_star - 2:
+ card_count = 0
+ card_list.append((card, i + 1))
+ return card_list
+
+ def format_pool_info(self, pool_name: str) -> str:
+ info = ""
+ up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
+ if up_event:
+ star3_list = [x.name for x in up_event.up_char if x.star == 3]
+ star2_list = [x.name for x in up_event.up_char if x.star == 2]
+ star1_list = [x.name for x in up_event.up_char if x.star == 1]
+ if star3_list:
+ if pool_name == "char":
+ info += f'三星UP:{" ".join(star3_list)}\n'
+ else:
+ info += f'SSR UP:{" ".join(star3_list)}\n'
+ if star2_list:
+ if pool_name == "char":
+ info += f'二星UP:{" ".join(star2_list)}\n'
+ else:
+ info += f'SR UP:{" ".join(star2_list)}\n'
+ if star1_list:
+ if pool_name == "char":
+ info += f'一星UP:{" ".join(star1_list)}\n'
+ else:
+ info += f'R UP:{" ".join(star1_list)}\n'
+ info = f"当前up池:{up_event.title}\n{info}"
+ return info.strip()
+
+ async def draw(self, count: int, pool_name: str, **kwargs) -> MessageFactory:
+ pool_name = "char" if not pool_name else pool_name
+ index2card = self.get_cards(count, pool_name)
+ cards = [card[0] for card in index2card]
+ up_event = self.UP_CHAR if pool_name == "char" else self.UP_CARD
+ up_list = [x.name for x in up_event.up_char] if up_event else []
+ result = self.format_result(index2card, up_list=up_list)
+ pool_info = self.format_pool_info(pool_name)
+ img = await self.generate_img(cards)
+ return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
+
+ async def generate_card_img(self, card: PrettyData) -> BuildImage:
+ if isinstance(card, PrettyChar):
+ star_h = 30
+ img_w = 200
+ img_h = 219
+ font_h = 50
+ bg = BuildImage(img_w, img_h + font_h, color="#EFF2F5")
+ star_path = str(self.img_path / "star.png")
+ star = BuildImage(star_h, star_h, background=star_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ star_w = star_h * card.star
+ for i in range(card.star):
+ await bg.paste(star, (int((img_w - star_w) / 2) + star_h * i, 0))
+ await bg.paste(img, (0, 0))
+ # 加名字
+ text = card.name[:5] + "..." if len(card.name) > 6 else card.name
+ font = load_font(fontsize=30)
+ text_w, _ = font.getsize(text)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ ((img_w - text_w) / 2, img_h),
+ text,
+ font=font,
+ fill="gray",
+ )
+ return bg
+ else:
+ sep_w = 10
+ img_w = 200
+ img_h = 267
+ font_h = 75
+ bg = BuildImage(img_w + sep_w * 2, img_h + font_h, color="#EFF2F5")
+ label_path = str(self.img_path / f"{card.star}_label.png")
+ label = BuildImage(40, 40, background=label_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ await bg.paste(img, (sep_w, 0))
+ await bg.paste(label, (30, 3))
+ # 加名字
+ text = ""
+ texts = []
+ font = load_font(fontsize=25)
+ for t in card.name:
+ if BuildImage.get_text_size((text + t), font)[0] > 190:
+ texts.append(text)
+ text = ""
+ if len(texts) >= 2:
+ texts[-1] += "..."
+ break
+ else:
+ text += t
+ if text:
+ texts.append(text)
+ text = "\n".join(texts)
+ text_w, _ = font.getsize_multiline(text)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ ((img_w - text_w) / 2, img_h),
+ text,
+ font=font,
+ align="center",
+ fill="gray",
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_CHAR = [
+ PrettyChar(
+ name=value["名称"],
+ star=int(value["初始星级"]),
+ limited=False,
+ )
+ for value in self.load_data().values()
+ ]
+ self.ALL_CARD = [
+ PrettyCard(
+ name=value["中文名"],
+ star=["R", "SR", "SSR"].index(value["稀有度"]) + 1,
+ limited=True if "卡池" not in value["获取方式"] else False,
+ )
+ for value in self.load_data("pretty_card.json").values()
+ ]
+ self.load_up_char()
+
+ def load_up_char(self):
+ try:
+ data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
+ self.UP_CHAR = UpEvent.parse_obj(data.get("char", {}))
+ self.UP_CARD = UpEvent.parse_obj(data.get("card", {}))
+ except ValidationError:
+ logger.warning(f"{self.game_name}_up_char 解析出错")
+
+ def dump_up_char(self):
+ if self.UP_CHAR and self.UP_CARD:
+ data = {
+ "char": json.loads(self.UP_CHAR.json()),
+ "card": json.loads(self.UP_CARD.json()),
+ }
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+
+ async def _update_info(self):
+ # pretty.json
+ pretty_info = {}
+ url = "https://wiki.biligame.com/umamusume/赛马娘图鉴"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[1]/a/@title")[0]
+ avatar = char.xpath("./td[1]/a/img/@srcset")[0]
+ star = len(char.xpath("./td[3]/img"))
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "初始星级": star,
+ }
+ pretty_info[member_dict["名称"]] = member_dict
+ self.dump_data(pretty_info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # pretty_card.json
+ pretty_card_info = {}
+ url = "https://wiki.biligame.com/umamusume/支援卡图鉴"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 卡牌出错")
+ else:
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ name = char.xpath("./td[1]/div/a/@title")[0]
+ name_cn = char.xpath("./td[3]/a/text()")[0]
+ avatar = char.xpath("./td[1]/div/a/img/@srcset")[0]
+ star = str(char.xpath("./td[5]/text()")[0]).strip()
+ sources = str(char.xpath("./td[7]/text()")[0]).strip()
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(name),
+ "中文名": remove_prohibited_str(name_cn),
+ "稀有度": star,
+ "获取方式": [sources] if sources else [],
+ }
+ pretty_card_info[member_dict["中文名"]] = member_dict
+ self.dump_data(pretty_card_info, "pretty_card.json")
+ logger.info(f"{self.game_name_cn} 卡牌更新成功")
+ # 下载头像
+ for value in pretty_info.values():
+ await self.download_img(value["头像"], value["名称"])
+ for value in pretty_card_info.values():
+ await self.download_img(value["头像"], value["中文名"])
+ # 下载星星
+ PRETTY_URL = "https://patchwiki.biligame.com/images/umamusume"
+ await self.download_img(
+ PRETTY_URL + "/1/13/e1hwjz4vmhtvk8wlyb7c0x3ld1s2ata.png", "star"
+ )
+ # 下载稀有度标志
+ idx = 1
+ for url in [
+ "/f/f7/afqs7h4snmvovsrlifq5ib8vlpu2wvk.png",
+ "/3/3b/d1jmpwrsk4irkes1gdvoos4ic6rmuht.png",
+ "/0/06/q23szwkbtd7pfkqrk3wcjlxxt9z595o.png",
+ ]:
+ await self.download_img(PRETTY_URL + url, f"{idx}_label")
+ idx += 1
+ await self.update_up_char()
+
+ async def update_up_char(self):
+ announcement_url = "https://wiki.biligame.com/umamusume/公告"
+ result = await self.get_url(announcement_url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取公告出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ announcements = dom.xpath("//div[@id='mw-content-text']/div/div/span/a")
+ title = ""
+ url = ""
+ for announcement in announcements:
+ try:
+ title = announcement.xpath("./@title")[0]
+ url = "https://wiki.biligame.com/" + announcement.xpath("./@href")[0]
+ if re.match(r".*?\d{8}$", title) or re.match(
+ r"^\d{1,2}月\d{1,2}日.*?", title
+ ):
+ break
+ except IndexError:
+ continue
+ if not title:
+ logger.warning(f"{self.game_name_cn}未找到新UP公告")
+ return
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取UP公告出错")
+ return
+ try:
+ start_time = None
+ end_time = None
+ char_img = ""
+ card_img = ""
+ up_chars = []
+ up_cards = []
+ soup = BeautifulSoup(result, "lxml")
+ heads = soup.find_all("span", {"class": "mw-headline"})
+ for head in heads:
+ if "时间" in head.text:
+ time = head.find_next("p").text.split("\n")[0]
+ if "~" in time:
+ start, end = time.split("~")
+ start_time = dateparser.parse(start)
+ end_time = dateparser.parse(end)
+ elif "赛马娘" in head.text:
+ char_img = head.find_next("a", {"class": "image"}).find("img")[
+ "src"
+ ]
+ lines = str(head.find_next("p").text).split("\n")
+ chars = [
+ line
+ for line in lines
+ if "★" in line and "(" in line and ")" in line
+ ]
+ for char in chars:
+ star = char.count("★")
+ name = re.split(r"[()]", char)[-2].strip()
+ up_chars.append(
+ UpChar(name=name, star=star, limited=False, zoom=70)
+ )
+ elif "支援卡" in head.text:
+ card_img = head.find_next("a", {"class": "image"}).find("img")[
+ "src"
+ ]
+ lines = str(head.find_next("p").text).split("\n")
+ cards = [
+ line
+ for line in lines
+ if "R" in line and "(" in line and ")" in line
+ ]
+ for card in cards:
+ star = 3 if "SSR" in card else 2 if "SR" in card else 1
+ name = re.split(r"[()]", card)[-2].strip()
+ up_cards.append(
+ UpChar(name=name, star=star, limited=False, zoom=70)
+ )
+ if start_time and end_time:
+ if start_time <= datetime.now() <= end_time:
+ self.UP_CHAR = UpEvent(
+ title=title,
+ pool_img=char_img,
+ start_time=start_time,
+ end_time=end_time,
+ up_char=up_chars,
+ )
+ self.UP_CARD = UpEvent(
+ title=title,
+ pool_img=card_img,
+ start_time=start_time,
+ end_time=end_time,
+ up_char=up_cards,
+ )
+ self.dump_up_char()
+ logger.info(
+ f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}"
+ )
+ except Exception as e:
+ logger.warning(f"{self.game_name_cn}UP更新出错", e=e)
+
+ async def _reload_pool(self) -> MessageFactory | None:
+ await self.update_up_char()
+ self.load_up_char()
+ if self.UP_CHAR and self.UP_CARD:
+ return MessageFactory(
+ [
+ Text(f"重载成功!\n当前UP池子:{self.UP_CHAR.title}"),
+ Image(self.UP_CHAR.pool_img),
+ Image(self.UP_CARD.pool_img),
+ ]
+ )
diff --git a/zhenxun/plugins/draw_card/handles/prts_handle.py b/zhenxun/plugins/draw_card/handles/prts_handle.py
new file mode 100644
index 00000000..b06fc87c
--- /dev/null
+++ b/zhenxun/plugins/draw_card/handles/prts_handle.py
@@ -0,0 +1,343 @@
+import random
+import re
+from datetime import datetime
+from urllib.parse import unquote
+
+import dateparser
+import ujson as json
+from lxml import etree
+from lxml.etree import _Element
+from nonebot_plugin_saa import Image, MessageFactory, Text
+from PIL import ImageDraw
+from pydantic import ValidationError
+
+from zhenxun.services.log import logger
+from zhenxun.utils.image_utils import BuildImage
+
+from ..config import draw_config
+from ..util import cn2py, load_font, remove_prohibited_str
+from .base_handle import BaseData, BaseHandle, UpChar, UpEvent
+
+
+class Operator(BaseData):
+ recruit_only: bool # 公招限定
+ event_only: bool # 活动获得干员
+ core_only: bool # 中坚干员
+ # special_only: bool # 升变/异格干员
+
+
+class PrtsHandle(BaseHandle[Operator]):
+ def __init__(self):
+ super().__init__(game_name="prts", game_name_cn="明日方舟")
+ self.max_star = 6
+ self.game_card_color = "#eff2f5"
+ self.config = draw_config.prts
+
+ self.ALL_OPERATOR: list[Operator] = []
+ self.UP_EVENT: UpEvent | None = None
+
+ def get_card(self, add: float) -> Operator:
+ star = self.get_star(
+ star_list=[6, 5, 4, 3],
+ probability_list=[
+ self.config.PRTS_SIX_P + add,
+ self.config.PRTS_FIVE_P,
+ self.config.PRTS_FOUR_P,
+ self.config.PRTS_THREE_P,
+ ],
+ )
+
+ all_operators = [
+ x
+ for x in self.ALL_OPERATOR
+ if x.star == star
+ and not any([x.limited, x.recruit_only, x.event_only, x.core_only])
+ ]
+ acquire_operator = None
+
+ if self.UP_EVENT:
+ up_operators = [x for x in self.UP_EVENT.up_char if x.star == star]
+ # UPs
+ try:
+ zooms = [x.zoom for x in up_operators]
+ zoom_sum = sum(zooms)
+ if random.random() < zoom_sum:
+ up_name = random.choices(up_operators, weights=zooms, k=1)[0].name
+ acquire_operator = [
+ x for x in self.ALL_OPERATOR if x.name == up_name
+ ][0]
+ except IndexError:
+ pass
+ if not acquire_operator:
+ acquire_operator = random.choice(all_operators)
+ return acquire_operator
+
+ def get_cards(self, count: int, **kwargs) -> list[tuple[Operator, int]]:
+ card_list = [] # 获取所有角色
+ add = 0.0
+ count_idx = 0
+ for i in range(count):
+ count_idx += 1
+ card = self.get_card(add)
+ if card.star == self.max_star:
+ add = 0.0
+ count_idx = 0
+ elif count_idx > 50:
+ add += 0.02
+ card_list.append((card, i + 1))
+ return card_list
+
+ def format_pool_info(self) -> str:
+ info = ""
+ if self.UP_EVENT:
+ star6_list = [x.name for x in self.UP_EVENT.up_char if x.star == 6]
+ star5_list = [x.name for x in self.UP_EVENT.up_char if x.star == 5]
+ star4_list = [x.name for x in self.UP_EVENT.up_char if x.star == 4]
+ if star6_list:
+ info += f"六星UP:{' '.join(star6_list)}\n"
+ if star5_list:
+ info += f"五星UP:{' '.join(star5_list)}\n"
+ if star4_list:
+ info += f"四星UP:{' '.join(star4_list)}\n"
+ info = f"当前up池: {self.UP_EVENT.title}\n{info}"
+ return info.strip()
+
+ async def draw(self, count: int, **kwargs) -> MessageFactory:
+ index2card = self.get_cards(count)
+ """这里cards修复了抽卡图文不符的bug"""
+ cards = [card[0] for card in index2card]
+ up_list = [x.name for x in self.UP_EVENT.up_char] if self.UP_EVENT else []
+ result = self.format_result(index2card, up_list=up_list)
+ pool_info = self.format_pool_info()
+ img = await self.generate_img(cards)
+ return MessageFactory([Text(pool_info), Image(img.pic2bytes()), Text(result)])
+
+ async def generate_card_img(self, card: Operator) -> BuildImage:
+ sep_w = 5
+ sep_h = 5
+ star_h = 15
+ img_w = 120
+ img_h = 120
+ font_h = 20
+ bg = BuildImage(img_w + sep_w * 2, img_h + font_h + sep_h * 2, color="#EFF2F5")
+ star_path = str(self.img_path / "star.png")
+ star = BuildImage(star_h, star_h, background=star_path)
+ img_path = str(self.img_path / f"{cn2py(card.name)}.png")
+ img = BuildImage(img_w, img_h, background=img_path)
+ await bg.paste(img, (sep_w, sep_h))
+ for i in range(card.star):
+ await bg.paste(star, (sep_w + img_w - 5 - star_h * (i + 1), sep_h))
+ # 加名字
+ text = card.name[:7] + "..." if len(card.name) > 8 else card.name
+ font = load_font(fontsize=16)
+ text_w, text_h = BuildImage.get_text_size(text, font)
+ draw = ImageDraw.Draw(bg.markImg)
+ draw.text(
+ (sep_w + (img_w - text_w) / 2, sep_h + img_h + (font_h - text_h) / 2),
+ text,
+ font=font,
+ fill="gray",
+ )
+ return bg
+
+ def _init_data(self):
+ self.ALL_OPERATOR = [
+ Operator(
+ name=value["名称"],
+ star=int(value["星级"]),
+ limited="标准寻访" not in value["获取途径"]
+ and "中坚寻访" not in value["获取途径"],
+ recruit_only=(
+ True
+ if "标准寻访" not in value["获取途径"]
+ and "中坚寻访" not in value["获取途径"]
+ and "公开招募" in value["获取途径"]
+ else False
+ ),
+ event_only=True if "活动获取" in value["获取途径"] else False,
+ core_only=(
+ True
+ if "标准寻访" not in value["获取途径"]
+ and "中坚寻访" in value["获取途径"]
+ else False
+ ),
+ )
+ for key, value in self.load_data().items()
+ if "阿米娅" not in key
+ ]
+ self.load_up_char()
+
+ def load_up_char(self):
+ try:
+ data = self.load_data(f"draw_card_up/{self.game_name}_up_char.json")
+ """这里的 waring 有点模糊,更新游戏信息时没有up池的情况下也会报错,所以细分了一下"""
+ if not data:
+ logger.warning(f"当前无UP池或 {self.game_name}_up_char.json 文件不存在")
+ else:
+ self.UP_EVENT = UpEvent.parse_obj(data.get("char", {}))
+ except ValidationError:
+ logger.warning(f"{self.game_name}_up_char 解析出错")
+
+ def dump_up_char(self):
+ if self.UP_EVENT:
+ data = {"char": json.loads(self.UP_EVENT.json())}
+ self.dump_data(data, f"draw_card_up/{self.game_name}_up_char.json")
+
+ async def _update_info(self):
+ """更新信息"""
+ info = {}
+ url = "https://wiki.biligame.com/arknights/干员数据表"
+ result = await self.get_url(url)
+ if not result:
+ logger.warning(f"更新 {self.game_name_cn} 出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ char_list: list[_Element] = dom.xpath("//table[@id='CardSelectTr']/tbody/tr")
+ for char in char_list:
+ try:
+ avatar = char.xpath("./td[1]/div/div/div/a/img/@srcset")[0]
+ name = char.xpath("./td[1]/center/a/text()")[0]
+ star = char.xpath("./td[2]/text()")[0]
+ """这里sources修好了干员获取标签有问题的bug,如三星只能抽到卡缇就是这个原因"""
+ sources = [_.strip("\n") for _ in char.xpath("./td[7]/text()")]
+ except IndexError:
+ continue
+ member_dict = {
+ "头像": unquote(str(avatar).split(" ")[-2]),
+ "名称": remove_prohibited_str(str(name).strip()),
+ "星级": int(str(star).strip()),
+ "获取途径": sources,
+ }
+ info[member_dict["名称"]] = member_dict
+ self.dump_data(info)
+ logger.info(f"{self.game_name_cn} 更新成功")
+ # 下载头像
+ for value in info.values():
+ await self.download_img(value["头像"], value["名称"])
+ # 下载星星
+ await self.download_img(
+ "https://patchwiki.biligame.com/images/pcr/0/02/s75ys2ecqhu2xbdw1wf1v9ccscnvi5g.png",
+ "star",
+ )
+ await self.update_up_char()
+
+ async def update_up_char(self):
+ """重载卡池"""
+ announcement_url = "https://ak.hypergryph.com/news.html"
+ result = await self.get_url(announcement_url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取公告出错")
+ return
+ dom = etree.HTML(result, etree.HTMLParser())
+ activity_urls = dom.xpath(
+ "//ol[@class='articlelist' and @data-category-key='ACTIVITY']/li/a/@href"
+ )
+ start_time = None
+ end_time = None
+ up_chars = []
+ pool_img = ""
+ for activity_url in activity_urls[:10]: # 减少响应时间, 10个就够了
+ activity_url = f"https://ak.hypergryph.com{activity_url}"
+ result = await self.get_url(activity_url)
+ if not result:
+ logger.warning(f"{self.game_name_cn}获取公告 {activity_url} 出错")
+ continue
+
+ """因为鹰角的前端太自由了,这里重写了匹配规则以尽可能避免因为前端乱七八糟而导致的重载失败"""
+ dom = etree.HTML(result, etree.HTMLParser())
+ contents = dom.xpath(
+ "//div[@class='article-content']/p/text() | //div[@class='article-content']/p/span/text() | //div[@class='article-content']/div[@class='media-wrap image-wrap']/img/@src"
+ )
+ title = ""
+ time = ""
+ chars: list[str] = []
+ for index, content in enumerate(contents):
+ if re.search("(.*)(寻访|复刻).*?开启", content):
+ title = re.split(r"[【】]", content)
+ title = "".join(title[1:-1]) if "-" in title else title[1]
+ lines = [
+ contents[index - 2 + _] for _ in range(8)
+ ] # 从 -2 开始是因为xpath获取的时间有的会在寻访开启这一句之前
+ lines.append("") # 防止IndexError,加个空字符串
+ for idx, line in enumerate(lines):
+ match = re.search(
+ r"(\d{1,2}月\d{1,2}日.*?-.*?\d{1,2}月\d{1,2}日.*?$)", line
+ )
+ if match:
+ time = match.group(1)
+ """因为
的诡异排版,所以有了下面的一段""" + if ("★★" in line and "%" in line) or ( + "★★" in line and "%" in lines[idx + 1] + ): + ( + chars.append(line) + if ("★★" in line and "%" in line) + else chars.append(line + lines[idx + 1]) + ) + if not time: + continue + start, end = ( + time.replace("月", "/").replace("日", " ").split("-")[:2] + ) # 日替换为空格是因为有日后面不接空格的情况,导致 split 出问题 + start_time = dateparser.parse(start) + end_time = dateparser.parse(end) + pool_img = contents[index - 2] + r"""两类格式:用/分割,用\分割;★+(概率)+名字,★+名字+(概率)""" + for char in chars: + star = char.split("(")[0].count("★") + name = ( + re.split(r"[:(]", char)[1] + if "★(" not in char + else re.split("):", char)[1] + ) # 有的括号在前面有的在后面 + dual_up = False + if "\\" in name: + names = name.split("\\") + dual_up = True + elif "/" in name: + names = name.split("/") + dual_up = True + else: + names = [name] # 既有用/分割的,又有用\分割的 + + names = [name.replace("[限定]", "").strip() for name in names] + zoom = 1 + if "权值" in char: + zoom = 0.03 + else: + match = re.search(r"(占.*?的.*?(\d+).*?%)", char) + if dual_up == True: + zoom = float(match.group(1)) / 2 + else: + zoom = float(match.group(1)) + zoom = zoom / 100 if zoom > 1 else zoom + for name in names: + up_chars.append( + UpChar(name=name, star=star, limited=False, zoom=zoom) + ) + break # 这里break会导致个问题:如果一个公告里有两个池子,会漏掉下面的池子,比如 5.19 的定向寻访。但目前我也没啥好想法解决 + if title and start_time and end_time: + if start_time <= datetime.now() <= end_time: + self.UP_EVENT = UpEvent( + title=title, + pool_img=pool_img, + start_time=start_time, + end_time=end_time, + up_char=up_chars, + ) + self.dump_up_char() + logger.info( + f"成功获取{self.game_name_cn}当前up信息...当前up池: {title}" + ) + break + + async def _reload_pool(self) -> MessageFactory | None: + await self.update_up_char() + self.load_up_char() + if self.UP_EVENT: + return MessageFactory( + [ + Text(f"重载成功!\n当前UP池子:{self.UP_EVENT.title}"), + Image(self.UP_EVENT.pool_img), + ] + ) diff --git a/zhenxun/plugins/draw_card/rule.py b/zhenxun/plugins/draw_card/rule.py new file mode 100644 index 00000000..49746d95 --- /dev/null +++ b/zhenxun/plugins/draw_card/rule.py @@ -0,0 +1,10 @@ +from nonebot.internal.rule import Rule + +from zhenxun.configs.config import Config + + +def rule(game) -> Rule: + async def _rule() -> bool: + return Config.get_config("draw_card", game.config_name, True) + + return Rule(_rule) diff --git a/zhenxun/plugins/draw_card/util.py b/zhenxun/plugins/draw_card/util.py new file mode 100644 index 00000000..d0cefc91 --- /dev/null +++ b/zhenxun/plugins/draw_card/util.py @@ -0,0 +1,61 @@ +import platform +from pathlib import Path + +import pypinyin +from PIL import Image, ImageDraw, ImageFont +from PIL.Image import Image as IMG +from PIL.ImageFont import FreeTypeFont + +from zhenxun.configs.path_config import FONT_PATH +from zhenxun.utils._build_image import BuildImage + +dir_path = Path(__file__).parent.absolute() + + +def cn2py(word) -> str: + """保存声调,防止出现类似方舟干员红与吽拼音相同声调不同导致红照片无法保存的问题""" + temp = "" + for i in pypinyin.pinyin(word, style=pypinyin.Style.TONE3): + temp += "".join(i) + return temp + + +# 移除windows和linux下特殊字符 +def remove_prohibited_str(name: str) -> str: + if platform.system().lower() == "windows": + tmp = "" + for i in name: + if i not in ["\\", "/", ":", "*", "?", '"', "<", ">", "|"]: + tmp += i + name = tmp + else: + name = name.replace("/", "\\") + return name + + +def load_font(fontname: str = "msyh.ttf", fontsize: int = 16) -> FreeTypeFont: + return ImageFont.truetype( + str(FONT_PATH / f"{fontname}"), fontsize, encoding="utf-8" + ) + + +def circled_number(num: int) -> IMG: + font = load_font(fontsize=450) + text = str(num) + text_w = BuildImage.get_text_size(text, font=font)[0] + w = 240 + text_w + w = w if w >= 500 else 500 + img = Image.new("RGBA", (w, 500)) + draw = ImageDraw.Draw(img) + draw.ellipse(((0, 0), (500, 500)), fill="red") + draw.ellipse(((w - 500, 0), (w, 500)), fill="red") + draw.rectangle(((250, 0), (w - 250, 500)), fill="red") + draw.text( + (120, -60), + text, + font=font, + fill="white", + stroke_width=10, + stroke_fill="white", + ) + return img