├── LICENSE ├── README.md ├── anticoncurrency ├── __init__.py └── anti_concurrency.py ├── bilisearchspider └── bilisearchspider.py ├── frame.jpg ├── ngaclanbattlespider └── ngaclanbattlespider.py ├── pcravatarguess └── pcravatarguess.py ├── pcrdescguess ├── _chara_data.py └── pcrdescguess.py ├── pcrmemorygames ├── AtlasMinigameSrtPanel_shrink.png ├── Background.png ├── __init__.py ├── _jielong_data.py ├── game_util.py ├── pcr_neurasthenia.py └── pcr_perfect_match.py ├── pcrmiddaymusic ├── _song_data.py └── pcrmiddaymusic.py ├── pcrsealkiller ├── __init__.py ├── _opencv_util.py ├── sealkiller.jpg └── sealkiller.py └── pokemanpcr ├── __init__.py ├── image ├── background.png ├── frame.png ├── new.png ├── normal.png ├── pokecriticalstrike.png ├── quantity.png ├── rare.png ├── superiorsuperrare.png └── superrare.png └── poke_man_pcr.py /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 公主连结实用/娱乐插件 for HoshinoBot on Mirai 2 | 3 | A repository for [HoshinoBot(V2)](https://github.com/Ice-Cirno/HoshinoBot) based [PCR](http://priconne-redive.jp/) plugins made by myself. 4 | 5 | 6 | ## 简介 7 | 8 | 一些基于 [HoshinoBot(V2)](https://github.com/Ice-Cirno/HoshinoBot) 开发的公主连结实用插件或有趣的娱乐插件,开发环境为Mirai。(开发环境用的是[go-cqhttp](https://github.com/Mrs4s/go-cqhttp),理论上绝大部分插件也能在[cqhttp-mirai](https://github.com/yyuueexxiinngg/cqhttp-mirai)上使用,但由于cqhttp-mirai最近没更新,进而导致cqhttp-mirai还未实现部分插件所用的的CQ码,因此会出现一些不兼容的情况,建议尽量还是使用go-cqhttp) 9 | 10 | 其他还有4(3?)个之前在酷Q Pro环境下开发的HoshinoBot插件,我没迁移到此仓库中,它们基本都可以直接装在Mirai的HoshinoBot上,它们是: 11 | 12 | - **[轴管理插件](https://github.com/GWYOG/HoshinoBotTimelinePlugin)**:(支持Mirai)加入了向机器人录入图片或者文字轴的指令,之后可以方便地查询数据库中存放的轴。此插件可使公会在会战中更方便的查询、交流、优化轴,也可以让几个公会之间共享轴。 13 | 14 | - **[box统计插件](https://github.com/GWYOG/HoshinoBotBoxCollectionPlugin)**:(支持Mirai)加入指令,在管理员设定好需要统计的角色星级后,机器人会自动私聊指定人员询问并统计汇总他们的角色星级。统计完毕后,在群内可以使用指令方便的查看分类汇总的统计结果;也可以自动生成统计结果的csv文件,或者把统计结果的表格以图片形式发送到群中(这个功能很实用!)。 15 | 16 | - **[猜语音小游戏插件](https://github.com/GWYOG/HoshinoBotVoiceGuessPlugin)**:(代码已重构,支持Mirai。~~如果用的是go-cqhttp,那么语音需要自己转码,具体流程请参看猜语音插件代码仓库的README~~ 请使用最新版的go-cqhttp并配置ffmpeg,已支持全格式语音发送)机器人会自动从干炸里脊资源站下载所有角色的打开游戏时说的的那句"cygames"语音。之后机器人会随机发送一句语音到群里,让群友猜猜是哪位角色说的。 17 | 18 | - **[会战名次查询插件](https://github.com/GWYOG/HoshinoBotClanRankSearchPlugin)**:(已弃坑,勿用)使用查询指令后,机器人会从镜华会战名次查询网获取数据,把需要查询的公会的会战当前名次或历史名次发送到群里。这个插件功能刚写到一半就开巨蟹座会战了,所以功能比较简陋。打完会战发现github上有不少同类型的功能更完善的插件,所以就弃坑了(笑)。 19 | 20 | ## 此仓库插件介绍(基于Mirai开发)(目前共9个): 21 | ## 1. 猜角色小游戏插件pcrdescguess 22 | 注意:此插件的代码已被[@Ice-Cirno](https://github.com/Ice-Cirno) 重构且并入了HoshinoBot本体。此仓库中此插件的代码仅供归档用,不再额外开发新功能。 23 | |指令|说明| 24 | |-----|-----| 25 | |猜角色|机器人会随机给出角色的一些描述,群友需要根据这些描述猜出是哪个角色| 26 | |猜角色排行榜|显示猜角色小游戏猜对次数的群排行榜| 27 | 28 | ## 2. 猜头像小游戏插件pcravatarguess 29 | 注意:此插件的代码已被[@Ice-Cirno](https://github.com/Ice-Cirno) 重构且并入了HoshinoBot本体。此仓库中此插件的代码仅供归档用,不再额外开发新功能。 30 | |指令|说明| 31 | |-----|-----| 32 | |猜头像|机器人会发送某个角色头像随机截取的一小部分,群友需要猜出它来自哪个角色头像| 33 | |猜头像排行榜|显示猜头像小游戏猜对次数的群排行榜| 34 | 35 | ## 3. B站搜索爬虫bilisearchspider 36 | 这是B站视频搜索引擎的爬虫。在设定好爬取关键词后,每隔5分钟机器人会把这几分钟里以这些关键词搜索出来的新发布的视频推送到QQ群中。推送的内容包括:视频封面、视频标题、up主名字、链接。 37 | 38 | 这个插件主要是为了会战而生,比如如果设定关键词为“狮子座公会战”,那么会战期间所有在B站发布的轴都会推送到QQ群里。此外,也可以设置一些其他的关键词供娱乐使用。 39 | 40 | 需要注意的是,由于B站搜索引擎的特点,设定单一关键词“狮子座公会战”并不能把所有相关视频都搜出来。比如起名“狮子座工会战B2130W轴”的视频就没法靠这个关键词搜出来。这时需要再添加另一个关键词“狮子座会战”。插件支持任意多的关键词,这些关键词搜索出视频的并集会被推送到群里。 41 | 42 | 特别地,如果设置的关键词是up主名字,使用这个插件等价于自动收到这个up主新投稿的提醒。 43 | 44 | 只要存在关键词爬虫就会自动启动,如果想停用请使用指令把关键词全删掉。 45 | |指令|说明| 46 | |-----|-----| 47 | |添加B站爬虫 <关键词>|添加爬取关键词。每次添加一个,可添加多次| 48 | |查看B站爬虫|查看当前爬取关键词列表| 49 | |删除B站爬虫 <关键词>|删除指定爬取关键词| 50 | 51 | ## 4. nga会战爬虫ngaclanbattlespider 52 | 顾名思义,这是爬取nga会战相关帖子的HoshinoBot爬虫插件。目前内置的关键词只会爬取一些有价值信息的帖子,包括但不限于会战轴。无意义的感叹帖、水贴不会爬取。目前暂不支持外部指令添加自定义关键词。 53 | 54 | 注意!安装此插件需先安装第三方库`selenium`,并配置好chrome和chromedriver,windows系统可参考[这篇文章](https://www.jianshu.com/p/64c92b1c05dd)。之后再使用通常方法安装此插件到HoshinoBot上。 55 | 56 | |指令|说明| 57 | |-----|-----| 58 | |启用nga会战爬虫 [国服/日服/台服]|启用nga会战爬虫并设置爬取版块为:国服讨论/日服讨论/台服讨论,默认是国服讨论。每隔一段时间爬虫将自动爬取nga会战相关帖子| 59 | |禁用nga会战爬虫|关闭nga会战爬虫服务| 60 | 61 | ## 5. 公主连结午间音乐pcrmiddaymusic 62 | 插件汇总了公主连结全活动ed、游戏主线op/ed以及一些角色歌。每日午间会随机推送一首音乐到群中。机器人首先会发送一些歌曲的基本信息:包括音乐出处、相关图片、歌名和歌手名,如果是活动ed还会包含活动剧情简介。之后机器人会发送音乐的qq音乐/网易云音乐/B站视频卡片。 63 | 64 | 平时也可以使用以下指令直接请求机器人推送一首pcr音乐。 65 | 66 | 推荐使用v0.9.24+的go-cqhttp,老版本的go-cqhttp使用xml发送音乐卡片容易被腾讯风控。 67 | 68 | |指令|说明| 69 | |-----|-----| 70 | |来点音乐|随机推送一首公主连结相关音乐| 71 | |来点音乐 [关键词]|寻找活动名/歌曲名含有关键词的音乐并推送| 72 | 73 | ## 6. 公主连结记忆小游戏pcrmemorygames 74 | 两个公主连结主题的记忆力小游戏。刚上手可能有点难,不过熟悉规则、掌握技巧后应该蛮容易的。如果还是觉得难可以自己修改相关参数。 75 | 76 | 目前存在作弊手段,全凭自觉。真想根除作弊只能等go-cqhttp把闪照功能实现,不过那样游戏性估计会降低。 77 | 78 | 完美配对小游戏的记忆技巧是联想记忆法,把图案和周围的图案联想记忆,比如如果看到"亚里莎"和"鱼竿"两个图案靠的近,就联想亚里莎在钓鱼,一次记住两张图。 79 | 80 | |指令|说明| 81 | |-----|-----| 82 | |完美配对|idea来自糖豆人的完美配对关卡。规则类似:4×4方格内藏有16种不同图案,初始不显示其内容。每次机器人会随机展示其中8个位置的图案,持续若干秒,共展示3轮,请记住各个图案所在的位置。之后机器人会随机提问一种图案,请回答这个图案在4×4方格中的位置编号。| 83 | |完美配对排行榜|显示完美配对小游戏分数排行榜| 84 | |神经衰弱|老实说我不清楚这种游戏该叫什么,暂时借用《狂赌之渊》中纸牌游戏“双重神经衰弱”之名。4×4方格中放置了8种不同的图案,每种图案共两张。机器人首先会给几秒钟时间记忆,然后把全部图片翻面并随机翻开其中一张。玩家需要在规定时间内回答另一张相同图案所在位置的编号。| 85 | |神经衰弱排行榜|显示神经衰弱小游戏分数排行榜| 86 | 87 | ## 7. 反并发插件anticoncurrency 88 | 在大群中使用多个不同的插件时容易出现非计划内的并发:比如群友A想玩猜角色、群友B想玩猜语音,他们不小心同时发送了指令,这时机器人会同时开始两个不同的游戏,而小游戏一般持续时间都比较长,这样会造成混乱。 89 | 90 | anticoncurrency插件解决了这一问题。给插件设置好不想并发的指令后,只有当一个指令执行完毕,另一个指令才会被机器人接受并开始执行,否则会自动忽略。 91 | 92 | 从理论上来说,在设置好相关参数后,这个插件可以防止任何插件(哪怕被魔改过)的指令出现并发,适用范围很广。 93 | 94 | 安装和使用方式:先用常规方式安装插件进HoshinoBot,然后使用notepad++之类的文本编辑器打开文件`anticoncurrency/anti_concurrency.py`,参照里面的注释修改`ANTI_CONCURRENCY_GROUPS`和`SELF_CONCURRENCY`的值。之后再启动bot就可以了。 95 | 96 | ## 8. 戳机器人集卡小游戏pokemanpcr 97 | 试试戳一戳机器人~ 她可能会回戳你,也可能会送你随机的公主连结角色卡片,尝试通过卡片交换和融合实现卡片全收集吧! 98 | 99 | 插件需要 go-cqhttp v0.9.25+, 并在配置文件中把protocol改成Android Phone,详见go-cqhttp的[doc](https://github.com/Mrs4s/go-cqhttp/blob/master/docs/config.md#%E8%AE%BE%E5%A4%87%E4%BF%A1%E6%81%AF)。此外,插件需要使用字体`arial.ttf`,Linux系统请自行下载字体并放在HoshinoBot的主目录下(即和`run.py`放在同一目录)。 100 | 101 | ~~图鉴中的卡片可以自己添加、删除、修改,但请使用相同的命名格式。所用资源来自[干炸里脊资源站](https://redive.estertion.win/)。~~ 新版插件会自动生成图鉴中卡片,详情请见下方的更新2说明。 102 | 103 | 每人每天戳一戳机器人得卡的次数有上限,想要修改数量请改代码开头部分的相关参数。同样在代码开头部分还有其他一些供自定义的参数,包括但不限于生成仓库图片时每行显示的图片个数、获取不同稀有度卡的概率等等。 104 | 105 | 更新2: 得到了[@A-kirami](https://github.com/A-kirami) 的许多帮助,此插件目前已大更新完毕。原有需要自定义添加的卡库改为使用Hoshino自带的res资源包中的角色头像。所有`res/img/priconne/unit`文件夹中文件名格式为`icon_unit_[四位数角色id][1/3/6]1.[后缀名]`的角色头像将被自动纳入卡库(同时需要在Hoshino的[_pcr_data.py](https://github.com/Ice-Cirno/HoshinoBot/blob/master/hoshino/modules/priconne/_pcr_data.py)中添加对应角色的昵称)。这样,插件卡库会随着res资源包的更新自动扩充了。此外,新版插件戳一戳时一次能获取多张卡了,这样一定程度可减少刷屏现象。 106 | 注:新版插件兼容老版本数据库,但卡片资源不通用。老版本插件可在仓库的[Release页面](https://github.com/GWYOG/GWYOG-Hoshino-plugins/releases)中找到。 107 | 108 | 更新:感谢[@var-mixer](https://github.com/var-mixer) 的PR,当前版本插件图鉴已自带全部149个角色(启动Bot时插件会预加载全部图片至内存,这样生成仓库图的速度有较大提升) 109 | 110 | |指令|说明| 111 | |-----|-----| 112 | |查看仓库 |查看自己的仓库、卡片收集情况和排名| 113 | |查看仓库 [@某人]|查看指定群友的仓库、卡片收集情况和排名| 114 | |合成 [卡片1昵称] [卡片2昵称]|消耗两张卡片以获得一张新的卡片。如是稀有或超稀有卡片请在卡片昵称前加上"稀有"或''超稀有",如"稀有黑猫"| 115 | |一键合成 [稀有度1] [稀有度2] [合成轮数(可不填)]|一键进行若干轮"稀有度1"和"稀有度2"的卡片合成(每张卡片会自动保留1张)。若不填合成轮数,则合成尽可能多的轮次。稀有度指"普通","稀有"或"超稀有"| 116 | |赠送 [@某人] [赠送的卡片名]|将自己的卡片赠予别人| 117 | |交换 [卡片1昵称] [@某人] [卡片2昵称] |向某人发起卡片交换请求,用自己的卡片1交换他的卡片2。同样,如是稀有卡片请在卡片昵称前加上"稀有"二字。| 118 | |确认交换 |收到换卡请求后一定时间内输入这个指令可完成换卡| 119 | 120 | ## 9. 海豹杀手pcrsealkiller 121 | 公会群里被海豹骑脸的非酋们的福音(雾)。 122 | 123 | 简单来说,这个插件会自动检测群员发的图片,并使用QQ自带的OCR提取图中文字。如果发现是抽卡截图,且抽卡次数未达阈值就抽中NEW的话,则被判定为海豹,享受撤回消息+禁言二连击。只支持识别公主连结抽卡截图,国服/台服/日服均可。 124 | 125 | 由于需使用`.ocr_image`API,请使用v0.9.26+的go-cqhttp。此外,请给机器人管理员权限,群主权限最佳。最新版插件需使用`opencv-python`库,请自行安装,安装方式:`pip install opencv-python`。 126 | 127 | 更新2:目前新版插件使用了opencv,极大程度提高了识别准确度。使用插件前请自行安装`opencv-python`库。如实在不想额外安装库,可在代码前部把`USE_OPENCV`改为`False`(注:这样会降低识别准确度,不推荐)。 128 | 129 | 更新1:`STRICT MODE`: 由于不同手机分辨率不同和QQ压图的原因,有时并不能很好地识别出抽卡图中的文字“NEW”。为了防止漏鲨海豹,新增`STRICT MODE`,默认为开启状态,可在代码开头部分修改,开启后对于检测不出“NEW”的未达抽卡阈值的抽卡图,同样视为海豹图,不过不再禁言,只撤回。(EDIT: 在更新2后已经基本不会识别不出“NEW”了~) 130 | 131 | 目前还处于实验性阶段,由于插件会对群里发送的~~大部分图片~~(EDIT: 感谢HoshinoBot群友们提供的思路,通过进行文件大小过滤,表情包被滤掉很多。现在插件只会对小部分图片OCR了,因此一直开着此插件也无妨)进行文字识别,不清楚会不会因此产生风控之类的不可测后果(小规模测试尚未发现)。 132 | 133 | |指令|说明| 134 | |-----|-----| 135 | |启用海豹杀手 [海豹判定阈值]|在当前群启用海豹杀手插件。如果不输入参数,默认阈值是100抽,小于100抽出货的均判定为海豹。| 136 | |禁用海豹杀手|在当前群禁用海豹杀手插件。| 137 | 138 | 139 | ## 安装方式 140 | 141 | 1. clone或者下载此仓库的代码 142 | 143 | 2. 将需要安装的插件文件夹放在`hoshino/modules/`文件夹中。比如如果想安装pcrdescguess插件,那么就把`pcrdescguess`文件夹放入。 144 | 145 | 3. 打开`hoshino/config/`文件夹中的`__bot__.py`文件,在`MODULES_ON`中加入插件的名称。比如如果想安装pcrdescguess插件,那么就加入一行`'pcrdescguess',` 146 | 147 | 4. 个别插件可能需要安装额外的第三方库。如果插件有需要安装的前置依赖,我在此README的插件介绍一节中注明。 148 | 149 | ## 注意事项 150 | 最好事先下载全角色头像放在HoshinoBot的资源文件夹中。虽然机器人在发现需要发送的角色头像缺失时也会自动从干炸里脊资源站下载,不过这样机器人会变卡。 151 | -------------------------------------------------------------------------------- /anticoncurrency/__init__.py: -------------------------------------------------------------------------------- 1 | class Process_Monitor: 2 | def __init__(self, gid, trigger_group_index, trigger_word, process_status_dict): 3 | self.gid = gid 4 | self.trigger_group_index = trigger_group_index 5 | self.trigger_word = trigger_word 6 | self.process_status_dict = process_status_dict 7 | 8 | def __enter__(self): 9 | if (self.gid, self.trigger_group_index) in self.process_status_dict: 10 | self.process_status_dict[(self.gid, self.trigger_group_index)].append(self.trigger_word) 11 | else: 12 | self.process_status_dict[(self.gid, self.trigger_group_index)] = [self.trigger_word] 13 | return self 14 | 15 | def __exit__(self, type_, value, trace): 16 | if self.trigger_word in self.process_status_dict[(self.gid, self.trigger_group_index)]: 17 | self.process_status_dict[(self.gid, self.trigger_group_index)].remove(self.trigger_word) -------------------------------------------------------------------------------- /anticoncurrency/anti_concurrency.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | from hoshino import Service, trigger 5 | from hoshino.typing import CQEvent 6 | from . import Process_Monitor 7 | 8 | 9 | """ 10 | 在下面的列表中填入不希望机器人并发执行的指令, 这些指令只有等其中某个被完毕执行完毕才能开始执行另外一个 11 | 例: ANTI_CONCURRENCY_GROUPS = [['猜头像', '猜角色', 'cygames']] 12 | 13 | 支持设置多组反并发, 14 | 如: ANTI_CONCURRENCY_GROUPS = [['猜头像', '猜角色'], ['完美配对', '神经衰弱']] 15 | 表示"猜头像"指令不能和"猜角色"指令并发, "完美配对"指令不能和"神经衰弱"指令并发 16 | """ 17 | ANTI_CONCURRENCY_GROUPS = [[]] 18 | # 是否允许同一条指令自己和自己并发 19 | SELF_CONCURRENCY = True 20 | # HoshinoBot的触发器字典,一般不用修改 21 | HOSHINO_TRIGGER_DICTS = [trigger.prefix.trie, trigger.suffix.trie, trigger.keyword.allkw, trigger.rex.allrex] 22 | 23 | 24 | sv = Service('anti-concurrency') 25 | process_status_dict = {} 26 | 27 | 28 | def is_list_generated_by_one_element(list_, element): 29 | return element in list_ and len(set(list_)) == 1 30 | 31 | 32 | def sf_wrapper(func, trigger_group_index, trigger_word): 33 | @wraps(func) 34 | async def wrapper(bot, ev: CQEvent): 35 | if process_status_dict.get((ev.group_id, trigger_group_index)): 36 | if not SELF_CONCURRENCY or (SELF_CONCURRENCY and not is_list_generated_by_one_element(process_status_dict[(ev.group_id, trigger_group_index)], trigger_word)): 37 | await bot.send(ev, '要、要同时执行这么多指令吗? 呜, 晕头转向了...做不到') 38 | sv.logger.info('由于检测到未执行完的同组指令, 消息已被anti-concurrency插件自动忽略') 39 | return 40 | with Process_Monitor(ev.group_id, trigger_group_index, trigger_word, process_status_dict) as pm: 41 | return await func(bot, ev) 42 | return wrapper 43 | 44 | 45 | async def add_trigger_words_wrapper(): 46 | for trigger_group_index, trigger_group in enumerate(ANTI_CONCURRENCY_GROUPS): 47 | for trigger_word in trigger_group: 48 | for trigger_words_dict in HOSHINO_TRIGGER_DICTS: 49 | if trigger_word in trigger_words_dict: 50 | sf = trigger_words_dict[trigger_word] 51 | sf.func = sf_wrapper(sf.func, trigger_group_index, trigger_word) 52 | sv.logger.info(f'Succeed to add wrapper on trigger `{trigger_word}`') 53 | break 54 | 55 | 56 | @sv.scheduled_job('date', next_run_time=datetime.datetime.now()) 57 | async def add_trigger_words_wrapper_job(): 58 | await add_trigger_words_wrapper() -------------------------------------------------------------------------------- /bilisearchspider/bilisearchspider.py: -------------------------------------------------------------------------------- 1 | from hoshino import Service, priv, aiorequests 2 | from hoshino.typing import MessageSegment, CQEvent 3 | from hoshino.modules.priconne.news.spider import BaseSpider 4 | 5 | from dataclasses import dataclass 6 | from typing import List, Union 7 | import hoshino, json, os 8 | 9 | sv = Service('bili-search-spider', bundle='pcr订阅', help_=''' 10 | 添加B站爬虫 <关键词> | 添加爬取关键词。每次添加一个,可添加多次 11 | 查看B站爬虫 | 查看当前爬取关键词列表 12 | 删除B站爬虫 <关键词> | 删除指定爬取关键词 13 | '''.strip()) 14 | 15 | FILE_FOLDER_PATH=os.path.dirname(__file__) 16 | 17 | @dataclass 18 | class Item: 19 | idx: Union[str, int] 20 | pic: str = "" 21 | content: str = "" 22 | 23 | def __eq__(self, other): 24 | return self.idx == other.idx 25 | 26 | 27 | class BiliSearchSpider(BaseSpider): 28 | url = {} 29 | src_name = "B站爬虫" 30 | idx_cache = {} 31 | item_cache = {} 32 | 33 | @classmethod 34 | def set_url(cls, gid, keyword_list): 35 | cls.url[gid] = [f'http://api.bilibili.com/x/web-interface/search/type?search_type=video&keyword={keyword}&order=pubdate&duration=0&tids_1=0' for keyword in keyword_list] 36 | if gid not in cls.idx_cache.keys(): cls.idx_cache[gid] = [] 37 | if gid not in cls.item_cache.keys(): cls.item_cache[gid] = [] 38 | 39 | @staticmethod 40 | async def get_response(url) -> aiorequests.AsyncResponse: 41 | resp = await aiorequests.get(url) 42 | resp.raise_for_status() 43 | return resp 44 | 45 | @classmethod 46 | async def get_update(cls, gid) -> List[Item]: 47 | updates_all = [] 48 | items_all = [] 49 | for url in cls.url[gid]: 50 | resp = await cls.get_response(url) 51 | items = await cls.get_items(resp) 52 | updates_all.extend([i for i in items if i.idx not in cls.idx_cache[gid] and i not in updates_all]) 53 | items_all.extend(items) 54 | if updates_all: 55 | cls.idx_cache[gid] = set(i.idx for i in items_all) 56 | cls.item_cache[gid] = items_all 57 | return updates_all 58 | 59 | @staticmethod 60 | async def get_items(resp:aiorequests.AsyncResponse): 61 | content = await resp.json() 62 | return [ 63 | Item(idx=result['arcurl'], 64 | pic=result['pic'], 65 | content="{}\nup主: {}\n{}".format(result['title'].replace('', '').replace('', ''), result['author'], result['arcurl']) 66 | ) for result in content['data']['result'] 67 | ] 68 | 69 | @classmethod 70 | def format_items(cls, items): 71 | ret = [f'{cls.src_name}发现了新发布的视频:'] 72 | ret.extend([i.content for i in items]) 73 | return ret 74 | 75 | 76 | def load_config(): 77 | try: 78 | config_path = os.path.join(FILE_FOLDER_PATH,'spider_conifg.json') 79 | if os.path.exists(config_path): 80 | with open(config_path, 'r', encoding='utf8') as config_file: 81 | return json.load(config_file) 82 | else: 83 | return {} 84 | except: 85 | return {} 86 | 87 | 88 | def save_config(config): 89 | try: 90 | with open(os.path.join(FILE_FOLDER_PATH,'spider_conifg.json'), 'w', encoding='utf8') as config_file: 91 | json.dump(config, config_file, ensure_ascii=False, indent=4) 92 | return True 93 | except: 94 | return False 95 | 96 | 97 | async def spider_work(spider:BaseSpider, bot, gid, sv:Service, TAG): 98 | if not spider.item_cache[gid]: 99 | await spider.get_update(gid) 100 | sv.logger.info(f'群{gid}的{TAG}缓存为空,已加载至最新') 101 | return 102 | updates = await spider.get_update(gid) 103 | if not updates: 104 | sv.logger.info(f'群{gid}的{TAG}未检索到新视频') 105 | return 106 | sv.logger.info(f'群{gid}的{TAG}检索到{len(updates)}个新视频!') 107 | msg_list = spider.format_items(updates) 108 | for i in range(len(updates)): 109 | pic = MessageSegment.image('http:'+updates[i].pic) 110 | msg = f'{msg_list[0]}{pic}{msg_list[i+1]}' 111 | await bot.send_group_msg(group_id=int(gid), message=msg) 112 | 113 | 114 | @sv.on_prefix('添加B站爬虫') 115 | async def add_spider_keyword(bot, ev: CQEvent): 116 | if not priv.check_priv(ev, priv.ADMIN): 117 | await bot.finish(ev, '抱歉,您非管理员,无此指令使用权限') 118 | s = ev.message.extract_plain_text() 119 | config = load_config() 120 | gid = str(ev.group_id) 121 | if gid in config.keys(): 122 | if s not in config[gid]: 123 | config[gid].append(s) 124 | else: 125 | await bot.finish(ev, '此群已经添加过该关键词,请勿重复添加') 126 | else: 127 | config[gid] = [s] 128 | if save_config(config): 129 | await bot.send(ev, f'添加关键词"{s}"成功!') 130 | # 重置群gid的item_cache和idx_cache,并重新加载缓存 131 | BiliSearchSpider.item_cache[gid] = [] 132 | BiliSearchSpider.idx_cache[gid] = [] 133 | await bili_search_spider() 134 | else: 135 | await bot.send(ev, '添加关键词失败,请重试') 136 | 137 | 138 | @sv.on_fullmatch('查看B站爬虫') 139 | async def get_spider_keyword_list(bot, ev: CQEvent): 140 | config = load_config() 141 | gid = str(ev.group_id) 142 | if gid in config.keys() and config[gid]: 143 | msg = 'B站爬虫已开启!\n此群设置的爬虫关键词为:\n' + '\n'.join(config[gid]) 144 | else: 145 | msg = '此群还未添加B站爬虫关键词' 146 | await bot.send(ev, msg) 147 | 148 | 149 | @sv.on_prefix('删除B站爬虫') 150 | async def delete_spider_keyword(bot, ev: CQEvent): 151 | config = load_config() 152 | s = ev.message.extract_plain_text() 153 | gid = str(ev.group_id) 154 | if gid in config.keys() and s in config[gid]: 155 | config[gid].remove(s) 156 | msg = f'删除关键词"{s}"成功' 157 | else: 158 | msg = f'删除失败, 此群未设置关键词"{s}"' 159 | if not save_config(config): 160 | await bot.finish(ev, '删除爬虫关键词失败,请重试') 161 | await bot.send(ev, msg) 162 | 163 | 164 | @sv.scheduled_job('cron', minute='*/5', second='30', jitter=20) 165 | async def bili_search_spider(): 166 | bot = hoshino.get_bot() 167 | config = load_config() 168 | for gid in config.keys(): 169 | BiliSearchSpider.set_url(gid, config[gid]) 170 | await spider_work(BiliSearchSpider, bot, gid, sv, 'B站搜索爬虫') -------------------------------------------------------------------------------- /frame.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/frame.jpg -------------------------------------------------------------------------------- /ngaclanbattlespider/ngaclanbattlespider.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Union 3 | 4 | import re, asyncio, os, json 5 | from bs4 import BeautifulSoup 6 | from selenium import webdriver 7 | 8 | import hoshino 9 | from hoshino import Service, priv, aiorequests 10 | from hoshino.modules.priconne.news import BaseSpider, Item 11 | from hoshino.typing import CQEvent 12 | 13 | sv = Service('nga-clan-battle-spider', bundle='pcr订阅', help_=''' 14 | 启用nga会战爬虫 [国服/日服/台服] | 启用nga会战爬虫并设置爬取版块为:国服讨论/日服讨论/台服讨论,默认是国服讨论,每隔一段时间爬虫将自动爬取nga会战相关帖子 15 | 禁用nga会战爬虫 | 关闭nga会战爬虫服务 16 | '''.strip()) 17 | 18 | FILE_FOLDER_PATH=os.path.dirname(__file__) 19 | 20 | URL_CN = 'https://bbs.nga.cn/thread.php?stid=20775069' 21 | URL_JP = 'https://ngabbs.com/thread.php?stid=20774924' 22 | URL_TW = 'https://ngabbs.com/thread.php?fid=739' 23 | ADDITIONAL_CLAN_BATTLE_KEYWORDS = ['周目', '排刀', '筛刀', '尾刀', '补偿刀', '补时刀', '挂树', '弟弟刀', '物理刀', '法刀', '白羊座', '金牛座', '双子座', '巨蟹座', 24 | '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '水瓶座', '双鱼座'] 25 | 26 | 27 | @dataclass 28 | class Item: 29 | idx: Union[str, int] 30 | title: str = "" 31 | content: str = "" 32 | 33 | def __eq__(self, other): 34 | return self.idx == other.idx 35 | 36 | 37 | class NGASpider(BaseSpider): 38 | url = {'cn': URL_CN, 'jp': URL_JP, 'tw': URL_TW} 39 | src_name = "nga会战爬虫" 40 | cookies = {} 41 | idx_cache = {'cn': [], 'jp': [], 'tw': []} 42 | item_cache = {'cn': [], 'jp': [], 'tw': []} 43 | 44 | @classmethod 45 | def set_cookies(cls, cookies): 46 | cls.cookies = cookies 47 | 48 | @classmethod 49 | async def get_response(cls, section) -> aiorequests.AsyncResponse: 50 | resp = await aiorequests.get(url = cls.url[section], cookies = cls.cookies) 51 | resp.raw_response.encoding = 'gbk' 52 | resp.raise_for_status() 53 | return resp 54 | 55 | @classmethod 56 | async def get_update(cls, section) -> List[Item]: 57 | resp = await cls.get_response(section) 58 | items = await cls.get_items(resp) 59 | updates = [i for i in items if has_clan_battle_keyword(i.title) and i.idx not in cls.idx_cache[section]] 60 | if updates: 61 | cls.idx_cache[section].extend([i.idx for i in updates]) 62 | return updates 63 | 64 | @staticmethod 65 | async def get_items(resp: aiorequests.AsyncResponse): 66 | soup = BeautifulSoup(await resp.text, 'lxml') 67 | return [ 68 | Item(idx=result['href'].split('=')[1], 69 | title=result.get_text(), 70 | content="{}\n{}".format( 71 | result.get_text(), 'https://bbs.nga.cn' + result['href']) 72 | ) for result in soup.find_all(class_='topic') 73 | ] 74 | 75 | @classmethod 76 | def format_items(cls, items): 77 | contents = [i.content for i in items] 78 | return f'{cls.src_name}在首页发现{len(contents)}个新的帖子:\n' + '\n'.join(contents) 79 | 80 | 81 | def load_file(filename, default_obj = {}): 82 | try: 83 | file_path = os.path.join(FILE_FOLDER_PATH,filename) 84 | if os.path.exists(file_path): 85 | with open(file_path, 'r', encoding='utf8') as file: 86 | return json.load(file) 87 | else: 88 | return default_obj 89 | except: 90 | return default_obj 91 | 92 | 93 | def save_file(obj, filename): 94 | try: 95 | with open(os.path.join(FILE_FOLDER_PATH,filename), 'w', encoding='utf8') as file: 96 | json.dump(obj, file, ensure_ascii=False, indent=4) 97 | return True 98 | except: 99 | return False 100 | 101 | 102 | def has_clan_battle_keyword(string): 103 | has_keyword1 = re.search('[一二三四五12345]王', string) 104 | if has_keyword1 is not None: 105 | return True 106 | has_keyword2 = re.search('[ABCDabcd][123456]', string) 107 | if has_keyword2 is not None: 108 | return True 109 | has_keyword3 = re.search('[一二三四1234][阶段]', string) 110 | if has_keyword3 is not None: 111 | return True 112 | for key_word in ADDITIONAL_CLAN_BATTLE_KEYWORDS: 113 | if key_word in string: 114 | return True 115 | return False 116 | 117 | 118 | async def spider_work(spider: BaseSpider, bot, section, broadcast_groups, sv: Service, TAG): 119 | updates = await spider.get_update(section) 120 | if not updates: 121 | sv.logger.info(f'{TAG}({section})未检索到新帖子') 122 | return 123 | sv.logger.info(f'{TAG}({section})检索到{len(updates)}个新帖子!') 124 | msg = spider.format_items(updates) 125 | for gid in broadcast_groups: 126 | await bot.send_group_msg(group_id=int(gid), message=msg) 127 | 128 | 129 | def get_broadcast_groups(): 130 | config = load_file('spider_config.json') 131 | broadcast_groups = {} 132 | broadcast_groups['cn'] = [gid for gid in config.keys() if config[gid]=='国服'] 133 | broadcast_groups['jp'] = [gid for gid in config.keys() if config[gid]=='日服'] 134 | broadcast_groups['tw'] = [gid for gid in config.keys() if config[gid]=='台服'] 135 | return broadcast_groups 136 | 137 | 138 | async def get_nga_cookies(): 139 | driver = webdriver.Chrome() 140 | driver.get(URL_CN) 141 | await asyncio.sleep(5) 142 | cookies = driver.get_cookies() 143 | driver.quit() 144 | cookies_dict = {} 145 | for cookie in cookies: 146 | cookies_dict[cookie['name']] = cookie['value'] 147 | return cookies_dict 148 | 149 | 150 | @sv.on_prefix(('启用nga会战爬虫', '启动nga会战爬虫', '开启nga会战爬虫')) 151 | async def turn_on_spider(bot, ev: CQEvent): 152 | if not priv.check_priv(ev, priv.ADMIN): 153 | await bot.finish(ev, '抱歉,您非管理员,无此指令使用权限') 154 | config = load_file('spider_config.json') 155 | gid = str(ev.group_id) 156 | s = ev.message.extract_plain_text() 157 | if s == '': 158 | s = config[gid].replace('(已禁用)', '') if config.get(gid) is not None else '国服' 159 | if s not in ['国服', '日服', '台服']: 160 | await bot.finish(ev, '错误: 参数请从"国服"/"日服"/"台服"中选择') 161 | else: 162 | section = s 163 | config[gid] = section 164 | if save_file(config, 'spider_config.json'): 165 | await bot.send(ev, f'nga会战爬虫已启用(爬取版面:{section}讨论)') 166 | else: 167 | await bot.send(ev, '启用nga会战爬虫失败,请重试') 168 | 169 | 170 | @sv.on_fullmatch(('关闭nga会战爬虫', '禁用nga会战爬虫')) 171 | async def turn_off_spider(bot, ev: CQEvent): 172 | if not priv.check_priv(ev, priv.ADMIN): 173 | await bot.finish(ev, '抱歉,您非管理员,无此指令使用权限') 174 | config = load_file('spider_config.json') 175 | gid = str(ev.group_id) 176 | if config.get(gid) is not None and not config[gid].endswith('(已禁用)'): 177 | config[gid] += '(已禁用)' 178 | if save_file(config, 'spider_config.json'): 179 | await bot.send(ev, 'nga会战爬虫已禁用') 180 | else: 181 | await bot.send(ev, '禁用nga会战爬虫失败,请重试') 182 | 183 | 184 | @sv.scheduled_job('cron', minute='*/5', second='15', jitter=20) 185 | async def nga_spider(): 186 | broadcast_groups = get_broadcast_groups() 187 | if not (broadcast_groups['cn'] or broadcast_groups['jp'] or broadcast_groups['tw']): 188 | return 189 | bot = hoshino.get_bot() 190 | NGASpider.set_cookies(await get_nga_cookies()) 191 | for section in broadcast_groups.keys(): 192 | if broadcast_groups[section]: 193 | if not NGASpider.idx_cache[section]: 194 | NGASpider.idx_cache[section] = load_file(f'idx_cache_{section}.json', []) 195 | await spider_work(NGASpider, bot, section, broadcast_groups[section], sv, 'nga会战爬虫') 196 | save_file(NGASpider.idx_cache[section], f'idx_cache_{section}.json') 197 | -------------------------------------------------------------------------------- /pcravatarguess/pcravatarguess.py: -------------------------------------------------------------------------------- 1 | from nonebot import MessageSegment 2 | from aiocqhttp.message import escape 3 | 4 | 5 | from hoshino import Service, util 6 | from hoshino.typing import CQEvent 7 | from hoshino.modules.priconne import chara 8 | from hoshino.modules.priconne import _pcr_data 9 | 10 | import hoshino 11 | import math, sqlite3, os, random, asyncio 12 | 13 | 14 | sv = Service('avatarguess', bundle='pcr娱乐', help_=''' 15 | 猜头像 | 猜猜机器人随机发送的头像的一小部分来自哪位角色 16 | 猜头像群排行 | 显示猜头像小游戏猜对次数的群排行榜(只显示前十名) 17 | '''.strip()) 18 | 19 | 20 | PIC_SIDE_LENGTH = 25 21 | ONE_TURN_TIME = 20 22 | DB_PATH = os.path.expanduser('~/.hoshino/pcr_avatar_guess_winning_counter.db') 23 | BLACKLIST_ID = [1072, 1908, 4031, 9000, 1000] 24 | 25 | class WinnerJudger: 26 | def __init__(self): 27 | self.on = {} 28 | self.winner = {} 29 | self.correct_chara_id = {} 30 | 31 | def record_winner(self, gid, uid): 32 | self.winner[gid] = str(uid) 33 | 34 | def get_winner(self, gid): 35 | return self.winner[gid] if self.winner.get(gid) is not None else '' 36 | 37 | def get_on_off_status(self, gid): 38 | return self.on[gid] if self.on.get(gid) is not None else False 39 | 40 | def set_correct_chara_id(self, gid, cid): 41 | self.correct_chara_id[gid] = cid 42 | 43 | def get_correct_chara_id(self, gid): 44 | return self.correct_chara_id[gid] if self.correct_chara_id.get(gid) is not None else chara.UNKNOWN 45 | 46 | def turn_on(self, gid): 47 | self.on[gid] = True 48 | 49 | def turn_off(self, gid): 50 | self.on[gid] = False 51 | self.winner[gid] = '' 52 | self.correct_chara_id[gid] = chara.UNKNOWN 53 | 54 | 55 | winner_judger = WinnerJudger() 56 | 57 | 58 | class WinningCounter: 59 | def __init__(self): 60 | os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) 61 | self._create_table() 62 | 63 | 64 | def _connect(self): 65 | return sqlite3.connect(DB_PATH) 66 | 67 | 68 | def _create_table(self): 69 | try: 70 | self._connect().execute('''CREATE TABLE IF NOT EXISTS WINNINGCOUNTER 71 | (GID INT NOT NULL, 72 | UID INT NOT NULL, 73 | COUNT INT NOT NULL, 74 | PRIMARY KEY(GID, UID));''') 75 | except: 76 | raise Exception('创建表发生错误') 77 | 78 | 79 | def _record_winning(self, gid, uid): 80 | try: 81 | winning_number = self._get_winning_number(gid, uid) 82 | conn = self._connect() 83 | conn.execute("INSERT OR REPLACE INTO WINNINGCOUNTER (GID,UID,COUNT) \ 84 | VALUES (?,?,?)", (gid, uid, winning_number+1)) 85 | conn.commit() 86 | except: 87 | raise Exception('更新表发生错误') 88 | 89 | 90 | def _get_winning_number(self, gid, uid): 91 | try: 92 | r = self._connect().execute("SELECT COUNT FROM WINNINGCOUNTER WHERE GID=? AND UID=?",(gid,uid)).fetchone() 93 | return 0 if r is None else r[0] 94 | except: 95 | raise Exception('查找表发生错误') 96 | 97 | 98 | async def get_user_card_dict(bot, group_id): 99 | mlist = await bot.get_group_member_list(group_id=group_id) 100 | d = {} 101 | for m in mlist: 102 | d[m['user_id']] = m['card'] if m['card']!='' else m['nickname'] 103 | return d 104 | 105 | 106 | def uid2card(uid, user_card_dict): 107 | return str(uid) if uid not in user_card_dict.keys() else user_card_dict[uid] 108 | 109 | 110 | @sv.on_fullmatch(('猜头像排行榜', '猜头像群排行')) 111 | async def description_guess_group_ranking(bot, ev: CQEvent): 112 | try: 113 | user_card_dict = await get_user_card_dict(bot, ev.group_id) 114 | card_winningcount_dict = {} 115 | winning_counter = WinningCounter() 116 | for uid in user_card_dict.keys(): 117 | if uid != ev.self_id: 118 | card_winningcount_dict[user_card_dict[uid]] = winning_counter._get_winning_number(ev.group_id, uid) 119 | group_ranking = sorted(card_winningcount_dict.items(), key = lambda x:x[1], reverse = True) 120 | msg = '猜头像小游戏此群排行为:\n' 121 | for i in range(min(len(group_ranking), 10)): 122 | if group_ranking[i][1] != 0: 123 | msg += f'第{i+1}名: {escape(group_ranking[i][0])}, 猜对次数: {group_ranking[i][1]}次\n' 124 | await bot.send(ev, msg.strip()) 125 | except Exception as e: 126 | await bot.send(ev, '错误:\n' + str(e)) 127 | 128 | 129 | @sv.on_fullmatch('猜头像') 130 | async def avatar_guess(bot, ev: CQEvent): 131 | if winner_judger.get_on_off_status(ev.group_id): 132 | await bot.send(ev, "此轮游戏还没结束,请勿重复使用指令") 133 | return 134 | winner_judger.turn_on(ev.group_id) 135 | chara_id_list = list(_pcr_data.CHARA_NAME.keys()) 136 | list_len = len(chara_id_list) - 1 137 | while True: 138 | index = random.randint(0, list_len) 139 | if chara_id_list[index] not in BLACKLIST_ID: break 140 | winner_judger.set_correct_chara_id(ev.group_id, chara_id_list[index]) 141 | dir_path = os.path.join(os.path.expanduser(hoshino.config.RES_DIR), 'img', 'priconne', 'unit') 142 | if not os.path.exists(dir_path): 143 | os.makedirs(dir_path) 144 | c = chara.fromid(chara_id_list[index]) 145 | img = c.icon.open() 146 | left = math.floor(random.random()*(129-PIC_SIDE_LENGTH)) 147 | upper = math.floor(random.random()*(129-PIC_SIDE_LENGTH)) 148 | cropped = img.crop((left, upper, left+PIC_SIDE_LENGTH, upper+PIC_SIDE_LENGTH)) 149 | cropped = MessageSegment.image(util.pic2b64(cropped)) 150 | msg = f'猜猜这个图片是哪位角色头像的一部分?({ONE_TURN_TIME}s后公布答案){cropped}' 151 | await bot.send(ev, msg) 152 | await asyncio.sleep(ONE_TURN_TIME) 153 | if winner_judger.get_winner(ev.group_id) != '': 154 | winner_judger.turn_off(ev.group_id) 155 | return 156 | msg = f'正确答案是: {c.name}{c.icon.cqcode}\n很遗憾,没有人答对~' 157 | winner_judger.turn_off(ev.group_id) 158 | await bot.send(ev, msg) 159 | 160 | 161 | @sv.on_message() 162 | async def on_input_chara_name(bot, ev: CQEvent): 163 | try: 164 | if winner_judger.get_on_off_status(ev.group_id): 165 | s = ev.message.extract_plain_text() 166 | cid = chara.name2id(s) 167 | if cid != chara.UNKNOWN and cid == winner_judger.get_correct_chara_id(ev.group_id) and winner_judger.get_winner(ev.group_id) == '': 168 | winner_judger.record_winner(ev.group_id, ev.user_id) 169 | winning_counter = WinningCounter() 170 | winning_counter._record_winning(ev.group_id, ev.user_id) 171 | winning_count = winning_counter._get_winning_number(ev.group_id, ev.user_id) 172 | user_card_dict = await get_user_card_dict(bot, ev.group_id) 173 | user_card = uid2card(ev.user_id, user_card_dict) 174 | msg_part = f'{escape(user_card)}猜对了,真厉害!TA已经猜对{winning_count}次了~\n(此轮游戏将在时间到后自动结束,请耐心等待)' 175 | c = chara.fromid(winner_judger.get_correct_chara_id(ev.group_id)) 176 | msg = f'正确答案是: {c.name}{c.icon.cqcode}\n{msg_part}' 177 | await bot.send(ev, msg) 178 | except Exception as e: 179 | await bot.send(ev, '错误:\n' + str(e)) 180 | -------------------------------------------------------------------------------- /pcrdescguess/_chara_data.py: -------------------------------------------------------------------------------- 1 | # 名字, 公会, 生日, 年龄, 身高, 体重, 血型, 种族, 喜好, 声优 2 | CHARA_DATA= { 3 | 1001: ["日和", "破晓之星", "8月27日", "16", "155", "44", "A", "兽人族", "助人、打气加油", "东山奈央"], 4 | 1002: ["优衣", "破晓之星", "4月5日", "17", "158", "47", "O", "人族", "料理、观察人类", "种田梨沙"], 5 | 1003: ["怜", "破晓之星", "1月12日", "18", "163", "46", "B", "魔族", "读书、骑马、茶", "早见沙织"], 6 | 1004: ["禊", "小小甜心", "8月10日", "9", "128", "27", "O", "人族", "恶作剧、探险", "诸星堇"], 7 | 1005: ["茉莉", "王宫骑士团", "11月25日", "12", "146", "40", "O", "兽人族", "英雄扮演游戏", "下田麻美"], 8 | 1006: ["茜里", "恶魔伪王国军", "11月22日", "13", "150", "42", "O", "魔族", "萨克斯风", "浅仓杏美"], 9 | 1007: ["宫子", "恶魔伪王国军", "1月23日", "14", "130", "32", "B", "魔族", "吃布丁", "雨宫天"], 10 | 1008: ["雪", "纯白之翼 兰德索尔分部", "10月10日", "14", "150", "40", "AB", "精灵族", "欣赏镜中的自己", "大空直美"], 11 | 1009: ["杏奈", "暮光流星群", "7月5日", "17", "159", "45", "A", "魔族", "写小说", "高野麻美"], 12 | 1010: ["真步", "自卫团", "9月22日", "16", "155", "42", "O", "兽人族", "幻想、收集玩偶", "内田真礼"], 13 | 1011: ["璃乃", "拉比林斯", "8月25日", "15", "156", "44", "A", "人族", "裁缝", "阿澄佳奈"], 14 | 1012: ["初音", "森林守卫", "12月24日", "17", "156", "46", "A", "精灵族", "和妹妹一起玩、回笼觉、午睡、早睡", "大桥彩香"], 15 | 1013: ["七七香", "暮光流星群", "8月21日", "18", "166", "55", "O", "魔族", "读书、魔法", "佳村遥"], 16 | 1014: ["霞", "自卫团", "11月3日", "15", "152", "41", "AB", "兽人族", "读书、推理", "水濑祈"], 17 | 1015: ["美里", "森林守卫", "9月5日", "21", "165", "54", "O", "精灵族", "制作绘本", "国府田麻理子"], 18 | 1016: ["铃奈", "月光学院", "4月10日", "18", "167", "48", "O", "魔族", "时尚", "上坂堇"], 19 | 1017: ["香织", "自卫团", "7月7日", "19", "158", "53", "A", "兽人族", "跳舞、空手道", "高森奈津美"], 20 | 1018: ["伊绪", "月光学院", "8月14日", "23", "162", "52", "B", "魔族", "看恋爱小说、恋爱剧、恋爱漫画", "伊藤静"], 21 | 22 | 1020: ["美美", "小小甜心", "4月3日", "10", "117", "21", "O", "兽人族", "收集可爱的东西", "日高里菜"], 23 | 1021: ["胡桃", "咲恋救济院", "6月9日", "12", "150", "40", "B", "人族", "看戏、扮家家酒", "植田佳奈"], 24 | 1022: ["依里", "恶魔伪王国军", "11月22日", "13", "150", "40", "O", "魔族", "所有游戏", "原纱友里"], 25 | 1023: ["绫音", "咲恋救济院", "5月10日", "14", "148", "38", "B", "人族", "能在房间里玩的游戏", "芹泽优"], 26 | 27 | 1025: ["铃莓", "咲恋救济院", "12月12日", "15", "154", "43", "O", "人族", "服侍", "悠木碧"], 28 | 1026: ["铃", "伊丽莎白牧场", "1月1日", "17", "144", "42", "B", "兽人族", "红豆面包", "小岩井小鸟"], 29 | 1027: ["惠理子", "暮光流星群", "7月30日", "16", "154", "43", "B", "魔族", "实验、裁缝、料理", "桥本千波"], 30 | 1028: ["咲恋", "咲恋救济院", "10月4日", "17", "156", "43", "A", "精灵族", "经营、茶会", "堀江由衣"], 31 | 1029: ["望", "慈乐之音", "1月24日", "17", "157", "40", "B", "人族", "看舞台剧、跳舞", "日笠阳子"], 32 | 1030: ["妮诺", "纯白之翼 兰德索尔分部", "8月31日", "16", "163", "51", "O", "人族", "忍术开发", "佐藤聪美"], 33 | 1031: ["忍", "恶魔伪王国军", "12月22日", "18", "157", "42", "AB", "魔族", "占卜", "大坪由佳"], 34 | 1032: ["秋乃", "墨丘利财团", "3月12日", "18", "157", "45", "AB", "人族", "慈善事业", "松嵜丽"], 35 | 1033: ["真阳", "伊丽莎白牧场", "3月3日", "20", "142", "35", "B", "人族", "相声", "新田惠海"], 36 | 1034: ["优花梨", "墨丘利财团", "3月16日", "22", "164", "55", "A", "精灵族", "随意逛街", "今井麻美"], 37 | 38 | 1036: ["镜华", "小小甜心", "2月2日", "8", "118", "21", "A", "精灵族", "读书", "小仓唯"], 39 | 1037: ["智", "王宫骑士团", "8月11日", "13", "149", "43", "A", "人族", "剑术、戏弄长者", "茅原实里"], 40 | 1038: ["栞", "伊丽莎白牧场", "11月3日", "14", "153", "40", "A", "兽人族", "读书、散步", "小清水亚美"], 41 | 42 | 1040: ["碧", "森林守卫", "6月6日", "13", "158", "44", "AB", "精灵族", "交朋友的想象训练", "花泽香菜"], 43 | 44 | 1042: ["千歌", "慈乐之音", "6月3日", "17", "163", "46", "O", "人族", "各种乐器", "福原绫香"], 45 | 1043: ["真琴", "自卫团", "8月9日", "17", "168", "54", "O", "兽人族", "做点心", "小松未可子"], 46 | 1044: ["伊莉亚", "恶魔伪王国军", "5月5日", "???", "172", "50", "A", "魔族", "征服世界", "丹下樱"], 47 | 1045: ["空花", "纯白之翼 兰德索尔分部", "11月19日", "18", "157", "49", "AB", "人族", "阅读小说", "长妻树里"], 48 | 1046: ["珠希", "墨丘利财团", "3月1日", "18", "158", "48", "AB", "兽人族", "与猫咪玩耍", "沼仓爱美"], 49 | 1047: ["纯", "王宫骑士团", "10月25日", "25", "171", "50", "A", "人族", "格斗技、入浴", "川澄绫子"], 50 | 1048: ["美冬", "墨丘利财团", "11月11日", "20", "163", "49", "O", "人族", "佣兵等等的打工", "田所梓"], 51 | 1049: ["静流", "拉比林斯", "10月24日", "18", "168", "54", "O", "人族", "所有家事", "天生目仁美"], 52 | 1050: ["美咲", "月光学院", "1月3日", "11", "120", "22", "A", "魔族", "阅读流行杂志、搜集化妆品", "久野美咲"], 53 | 1051: ["深月", "暮光流星群", "3月7日", "27", "166", "53", "A", "人族", "研究、实验", "三石琴乃"], 54 | 1052: ["莉玛", "伊丽莎白牧场", "3月14日", "18", "150", "100", "A", "兽人族", "理毛、聊天", "德井青空"], 55 | 1053: ["莫妮卡", "纯白之翼 兰德索尔分部", "7月28日", "18", "140", "33", "A", "人族", "逛糖果店", "辻亚由美"], 56 | 1054: ["纺希", "慈乐之音", "9月7日", "14", "153", "45", "AB", "人族", "裁缝", "木户衣吹"], 57 | 1055: ["步未", "纯白之翼 兰德索尔分部", "4月7日", "16", "155", "43", "O", "精灵族", "观察", "大关英里"], 58 | 1056: ["流夏", "暮光流星群", "7月11日", "25", "167", "54", "B", "人族", "钓鱼", "佐藤利奈"], 59 | 1057: ["吉塔", "???", "3月10日", "17", "156", "45", "O", "人族", "冒险、聊天", "金元寿子"], 60 | 1058: ["贪吃佩可", "美食殿堂", "3月31日", "17", "156", "46", "O", "人族", "边走边吃、料理", "M·A·O"], 61 | 1059: ["可可萝", "美食殿堂", "5月11日", "11", "140", "35", "B", "精灵族", "冥想、养育动植物", "伊藤美来"], 62 | 1060: ["凯留", "美食殿堂", "9月2日", "14", "152", "39", "A", "兽人族", "和猫咪玩耍", "立花理香"], 63 | 1061: ["矛依未", "???", "8月11日", "16", "148", "40", "O", "人族", "冒险、回忆故事", "潘惠美"], 64 | 65 | 1063: ["亚里莎", "???", "6月17日", "15", "155", "42", "O", "精灵族", "搜集漂亮的叶子", "优木加奈"], 66 | 67 | 1065: ["嘉夜", "龙族巢穴", "6月25日", "16", "156", "???", "B", "龙人族", "格斗技", "小市真琴"], 68 | 1066: ["祈梨", "龙族巢穴", "9月29日", "13", "145", "???", "AB", "龙人族", "游戏", "藤田茜"], 69 | 70 | 1070: ["似似花", "???", "3月24日", "24", "149", "???", "O", "精灵族", "模仿、艺术欣赏", "井口裕香"], 71 | 1071: ["克莉丝提娜", "王宫骑士团", "2月7日", "27", "165", "???", "O", "人族", "和强敌之间的竞争", "高桥智秋"], 72 | 73 | 1092: ["安", "???", "12月1日", "17", "156", "55", "AB", "人族", "读书", "日笠阳子"], 74 | 1093: ["露", "???", "2月4日", "15", "144", "45", "O", "人族", "吃饭、睡觉", "古山贵实子"], 75 | 1094: ["古蕾娅", "???", "11月3日", "17", "167", "67", "B", "半人半龙", "钢琴", "福原绫香"], 76 | 77 | 1097: ["雷姆", "???", "2月2日", "17", "154", "???", "???", "鬼族", "戏剧欣赏、诗和散文", "水濑祈"], 78 | 1098: ["拉姆", "???", "2月2日", "17", "154", "???", "???", "鬼族", "读书", "村川梨衣"], 79 | 1099: ["爱蜜莉雅", "???", "9月23日", "114", "164", "???", "???", "半精灵族", "帮帕克梳理毛发、念书", "高桥李依"], 80 | 81 | 1108: ["克萝依", "圣特蕾莎女子学院(好朋友社)", "8月7日", "17", "154", "42", "O", "精灵族", "飞镖", "种崎敦美"], 82 | 1109: ["琪爱儿", "圣特蕾莎女子学院(好朋友社)", "9月15日", "16", "156", "46", "O", "人族", "跳舞、卡拉OK", "佐仓绫音"], 83 | 1110: ["优妮", "圣特蕾莎女子学院(好朋友社)", "2月28日", "18", "142", "36", "O", "人族", "读书", "小原好美"], 84 | 85 | 1114: ["露娜", "???", "???", "???", "142", "28", "???", "人族", "找「朋友」之事", "小仓唯"], 86 | 87 | 1124: ["卯月(偶像大师)", "new generations", "4月24日", "17", "159", "45", "O", "人族", "和朋友打长电话", "大桥彩香"], 88 | 1125: ["凛(偶像大师)", "new generations", "8月10日", "15", "165", "44", "B", "人族", "带狗散步", "福原绫香"], 89 | 1126: ["未央(偶像大师)", "new generations", "12月1日", "15", "161", "46", "B", "人族", "购物", "原纱友里"], 90 | } -------------------------------------------------------------------------------- /pcrdescguess/pcrdescguess.py: -------------------------------------------------------------------------------- 1 | from aiocqhttp.message import escape 2 | from hoshino import Service 3 | from hoshino.typing import CQEvent 4 | from hoshino.modules.priconne import chara 5 | from . import _chara_data 6 | 7 | import hoshino 8 | import sqlite3, os, random, asyncio 9 | 10 | sv = Service('descguess', bundle='pcr娱乐', help_=''' 11 | 猜角色 | 猜猜机器人随机发送的文本在描述哪位角色 12 | 猜角色群排行 | 显示猜角色小游戏猜对次数的群排行榜(只显示前十名) 13 | '''.strip()) 14 | 15 | 16 | PREPARE_TIME = 5 17 | ONE_TURN_TIME = 12 18 | TURN_NUMBER = 5 19 | DB_PATH = os.path.expanduser('~/.hoshino/pcr_desc_guess_winning_counter.db') 20 | 21 | 22 | class WinnerJudger: 23 | def __init__(self): 24 | self.on = {} 25 | self.winner = {} 26 | self.correct_chara_id = {} 27 | 28 | def record_winner(self, gid, uid): 29 | self.winner[gid] = str(uid) 30 | 31 | def get_winner(self, gid): 32 | return self.winner[gid] if self.winner.get(gid) is not None else '' 33 | 34 | def get_on_off_status(self, gid): 35 | return self.on[gid] if self.on.get(gid) is not None else False 36 | 37 | def set_correct_chara_id(self, gid, cid): 38 | self.correct_chara_id[gid] = cid 39 | 40 | def get_correct_chara_id(self, gid): 41 | return self.correct_chara_id[gid] if self.correct_chara_id.get(gid) is not None else chara.UNKNOWN 42 | 43 | def turn_on(self, gid): 44 | self.on[gid] = True 45 | 46 | def turn_off(self, gid): 47 | self.on[gid] = False 48 | self.winner[gid] = '' 49 | self.correct_chara_id[gid] = chara.UNKNOWN 50 | 51 | 52 | winner_judger = WinnerJudger() 53 | 54 | 55 | class WinningCounter: 56 | def __init__(self): 57 | os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) 58 | self._create_table() 59 | 60 | 61 | def _connect(self): 62 | return sqlite3.connect(DB_PATH) 63 | 64 | 65 | def _create_table(self): 66 | try: 67 | self._connect().execute('''CREATE TABLE IF NOT EXISTS WINNINGCOUNTER 68 | (GID INT NOT NULL, 69 | UID INT NOT NULL, 70 | COUNT INT NOT NULL, 71 | PRIMARY KEY(GID, UID));''') 72 | except: 73 | raise Exception('创建表发生错误') 74 | 75 | 76 | def _record_winning(self, gid, uid): 77 | try: 78 | winning_number = self._get_winning_number(gid, uid) 79 | conn = self._connect() 80 | conn.execute("INSERT OR REPLACE INTO WINNINGCOUNTER (GID,UID,COUNT) \ 81 | VALUES (?,?,?)", (gid, uid, winning_number+1)) 82 | conn.commit() 83 | except: 84 | raise Exception('更新表发生错误') 85 | 86 | 87 | def _get_winning_number(self, gid, uid): 88 | try: 89 | r = self._connect().execute("SELECT COUNT FROM WINNINGCOUNTER WHERE GID=? AND UID=?",(gid,uid)).fetchone() 90 | return 0 if r is None else r[0] 91 | except: 92 | raise Exception('查找表发生错误') 93 | 94 | 95 | async def get_user_card_dict(bot, group_id): 96 | mlist = await bot.get_group_member_list(group_id=group_id) 97 | d = {} 98 | for m in mlist: 99 | d[m['user_id']] = m['card'] if m['card']!='' else m['nickname'] 100 | return d 101 | 102 | 103 | def uid2card(uid, user_card_dict): 104 | return str(uid) if uid not in user_card_dict.keys() else user_card_dict[uid] 105 | 106 | 107 | def get_cqcode(chara_id): 108 | dir_path = os.path.join(os.path.expanduser(hoshino.config.RES_DIR), 'img', 'priconne', 'unit') 109 | if not os.path.exists(dir_path): 110 | os.makedirs(dir_path) 111 | c = chara.fromid(chara_id) 112 | cqcode = '' if not c.icon.exist else c.icon.cqcode 113 | return c.name, cqcode 114 | 115 | 116 | @sv.on_fullmatch(('猜角色排行榜', '猜角色群排行')) 117 | async def description_guess_group_ranking(bot, ev: CQEvent): 118 | try: 119 | user_card_dict = await get_user_card_dict(bot, ev.group_id) 120 | card_winningcount_dict = {} 121 | winning_counter = WinningCounter() 122 | for uid in user_card_dict.keys(): 123 | if uid != ev.self_id: 124 | card_winningcount_dict[user_card_dict[uid]] = winning_counter._get_winning_number(ev.group_id, uid) 125 | group_ranking = sorted(card_winningcount_dict.items(), key = lambda x:x[1], reverse = True) 126 | msg = '猜角色小游戏此群排行为:\n' 127 | for i in range(min(len(group_ranking), 10)): 128 | if group_ranking[i][1] != 0: 129 | msg += f'第{i+1}名: {escape(group_ranking[i][0])}, 猜对次数: {group_ranking[i][1]}次\n' 130 | await bot.send(ev, msg.strip()) 131 | except Exception as e: 132 | await bot.send(ev, '错误:\n' + str(e)) 133 | 134 | 135 | @sv.on_fullmatch(('猜角色', '猜人物')) 136 | async def description_guess(bot, ev: CQEvent): 137 | try: 138 | if winner_judger.get_on_off_status(ev.group_id): 139 | await bot.send(ev, "此轮游戏还没结束,请勿重复使用指令") 140 | return 141 | winner_judger.turn_on(ev.group_id) 142 | await bot.send(ev, f'{PREPARE_TIME}秒钟后每隔{ONE_TURN_TIME}秒我会给出某位角色的一个描述,根据这些描述猜猜她是谁~') 143 | await asyncio.sleep(PREPARE_TIME) 144 | desc_lable = ['名字', '公会', '生日', '年龄', '身高', '体重', '血型', '种族', '喜好', '声优'] 145 | desc_suffix = ['', '', '', '', 'cm', 'kg', '', '', '', ''] 146 | index_list = list(range(1,10)) 147 | random.shuffle(index_list) 148 | chara_id_list = list(_chara_data.CHARA_DATA.keys()) 149 | random.shuffle(chara_id_list) 150 | chara_id = chara_id_list[0] 151 | chara_desc_list = _chara_data.CHARA_DATA[chara_id] 152 | winner_judger.set_correct_chara_id(ev.group_id, chara_id) 153 | for i in range(TURN_NUMBER): 154 | desc_index = index_list[i] 155 | await bot.send(ev, f'提示{i+1}/{TURN_NUMBER}:\n她的{desc_lable[desc_index]}是 {chara_desc_list[desc_index]}{desc_suffix[desc_index]}') 156 | await asyncio.sleep(ONE_TURN_TIME) 157 | if winner_judger.get_winner(ev.group_id) != '': 158 | winner_judger.turn_off(ev.group_id) 159 | return 160 | msg_part = '很遗憾,没有人答对~' 161 | name, cqcode = get_cqcode(winner_judger.get_correct_chara_id(ev.group_id)) 162 | msg = f'正确答案是: {name}{cqcode}\n{msg_part}' 163 | winner_judger.turn_off(ev.group_id) 164 | await bot.send(ev, msg) 165 | except Exception as e: 166 | winner_judger.turn_off(ev.group_id) 167 | await bot.send(ev, '错误:\n' + str(e)) 168 | 169 | 170 | @sv.on_message() 171 | async def on_input_chara_name(bot, ev: CQEvent): 172 | try: 173 | if winner_judger.get_on_off_status(ev.group_id): 174 | s = ev.message.extract_plain_text() 175 | cid = chara.name2id(s) 176 | if cid != chara.UNKNOWN and cid == winner_judger.get_correct_chara_id(ev.group_id) and winner_judger.get_winner(ev.group_id) == '': 177 | winner_judger.record_winner(ev.group_id, ev.user_id) 178 | winning_counter = WinningCounter() 179 | winning_counter._record_winning(ev.group_id, ev.user_id) 180 | winning_count = winning_counter._get_winning_number(ev.group_id, ev.user_id) 181 | user_card_dict = await get_user_card_dict(bot, ev.group_id) 182 | user_card = uid2card(ev.user_id, user_card_dict) 183 | msg_part = f'{escape(user_card)}猜对了,真厉害!TA已经猜对{winning_count}次了~\n(此轮游戏将在几秒后自动结束,请耐心等待)' 184 | name, cqcode = get_cqcode(winner_judger.get_correct_chara_id(ev.group_id)) 185 | msg = f'正确答案是: {name}{cqcode}\n{msg_part}' 186 | await bot.send(ev, msg) 187 | except Exception as e: 188 | await bot.send(ev, '错误:\n' + str(e)) 189 | -------------------------------------------------------------------------------- /pcrmemorygames/AtlasMinigameSrtPanel_shrink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pcrmemorygames/AtlasMinigameSrtPanel_shrink.png -------------------------------------------------------------------------------- /pcrmemorygames/Background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pcrmemorygames/Background.png -------------------------------------------------------------------------------- /pcrmemorygames/__init__.py: -------------------------------------------------------------------------------- 1 | # Refer to code of priconne game in HoshinoBot by @Ice-Cirno 2 | # Under GPL-3.0 License 3 | 4 | import os 5 | import sqlite3 6 | 7 | 8 | class Dao: 9 | def __init__(self, db_path): 10 | self.db_path = db_path 11 | os.makedirs(os.path.dirname(db_path), exist_ok=True) 12 | self._create_table() 13 | 14 | def connect(self): 15 | return sqlite3.connect(self.db_path) 16 | 17 | def _create_table(self): 18 | with self.connect() as conn: 19 | conn.execute( 20 | "CREATE TABLE IF NOT EXISTS score_record " 21 | "(gid INT NOT NULL, uid INT NOT NULL, score INT NOT NULL, PRIMARY KEY(gid, uid))" 22 | ) 23 | 24 | def get_score(self, gid, uid): 25 | with self.connect() as conn: 26 | r = conn.execute( 27 | "SELECT score FROM score_record WHERE gid=? AND uid=?", (gid, uid) 28 | ).fetchone() 29 | return r[0] if r else 0 30 | 31 | def add_score_increment(self, gid, uid, score_increment): 32 | score = self.get_score(gid, uid) 33 | score += score_increment 34 | with self.connect() as conn: 35 | conn.execute( 36 | "INSERT OR REPLACE INTO score_record (gid, uid, score) VALUES (?, ?, ?)", 37 | (gid, uid, score), 38 | ) 39 | return score 40 | 41 | def get_ranking(self, gid): 42 | with self.connect() as conn: 43 | r = conn.execute( 44 | "SELECT uid, score FROM score_record WHERE gid=? ORDER BY score DESC LIMIT 10", 45 | (gid,), 46 | ).fetchall() 47 | return r 48 | 49 | 50 | class GameMaster: 51 | def __init__(self, db_path): 52 | self.db_path = db_path 53 | self.playing = {} 54 | 55 | def is_playing(self, gid): 56 | return gid in self.playing 57 | 58 | def start_game(self, gid): 59 | return Game(gid, self) 60 | 61 | def get_game(self, gid): 62 | return self.playing[gid] if gid in self.playing else None 63 | 64 | @property 65 | def db(self): 66 | return Dao(self.db_path) 67 | 68 | 69 | class Game: 70 | def __init__(self, gid, game_master): 71 | self.gid = gid 72 | self.gm = game_master 73 | self.answer = -1 74 | self.winner = [] 75 | self.loser = [] 76 | 77 | def __enter__(self): 78 | self.gm.playing[self.gid] = self 79 | return self 80 | 81 | def __exit__(self, type_, value, trace): 82 | del self.gm.playing[self.gid] 83 | 84 | def record_winner(self, uid): 85 | if uid not in self.winner: 86 | self.winner.append(uid) 87 | if uid in self.loser: 88 | self.loser.remove(uid) 89 | 90 | def record_loser(self, uid): 91 | if uid in self.winner: 92 | self.winner.remove(uid) 93 | if uid not in self.loser: 94 | self.loser.append(uid) 95 | 96 | def update_score(self): 97 | for i, uid in enumerate(self.winner): 98 | self.gm.db.add_score_increment(self.gid, uid, 2 if i==0 else 1) 99 | for uid in self.loser: 100 | self.gm.db.add_score_increment(self.gid, uid, -1) 101 | 102 | def get_first_winner_score(self): 103 | return self.gm.db.get_score(self.gid, self.winner[0]) if self.winner else -999 -------------------------------------------------------------------------------- /pcrmemorygames/_jielong_data.py: -------------------------------------------------------------------------------- 1 | # POSITION数据来自https://redive.estertion.win/%E4%B8%80%E4%B8%AA%E9%A1%B6%E4%BF%A9.htm 2 | # EXPLANATION数据参考了https://randosoru.me/minigame/, 转为简体并加以适当删减改编 3 | POSITION = {"00000":[0,1888,160,160],"10001":[1296,1240,160,160],"10002":[1458,1240,160,160],"10003":[1620,1240,160,160],"10004":[1782,1240,160,160],"10005":[810,1078,160,160],"10006":[810,916,160,160],"10007":[810,592,160,160],"10008":[1458,1078,160,160],"10009":[810,430,160,160],"10010":[810,268,160,160],"10011":[810,106,160,160],"10012":[972,1078,160,160],"10013":[1134,1078,160,160],"10014":[1296,1078,160,160],"10015":[1134,1240,160,160],"10016":[810,754,160,160],"10017":[972,1240,160,160],"10018":[1782,1402,160,160],"10019":[1296,1402,160,160],"10020":[1458,1402,160,160],"10021":[1620,1402,160,160],"10022":[810,1402,160,160],"10023":[972,1402,160,160],"10024":[1134,1402,160,160],"10025":[648,1240,160,160],"10026":[648,106,160,160],"10027":[648,1078,160,160],"10028":[648,916,160,160],"10029":[648,754,160,160],"10030":[648,592,160,160],"10031":[648,430,160,160],"10032":[648,268,160,160],"10033":[810,1240,160,160],"10034":[1782,1078,160,160],"10035":[1296,754,160,160],"10036":[972,916,160,160],"10037":[1782,754,160,160],"10038":[1296,592,160,160],"10039":[1296,430,160,160],"10040":[1296,268,160,160],"10041":[1296,106,160,160],"10042":[1458,592,160,160],"10043":[1782,592,160,160],"10044":[1620,106,160,160],"10045":[1458,430,160,160],"10046":[1458,268,160,160],"10047":[1458,106,160,160],"10048":[1620,430,160,160],"10049":[1782,430,160,160],"10050":[1620,268,160,160],"10051":[1620,754,160,160],"10052":[1620,592,160,160],"10053":[1458,754,160,160],"10054":[1296,916,160,160],"10055":[972,754,160,160],"10056":[972,592,160,160],"10057":[972,430,160,160],"10058":[972,268,160,160],"10059":[972,106,160,160],"10060":[1134,916,160,160],"10061":[1458,916,160,160],"10062":[1134,106,160,160],"10063":[1620,916,160,160],"10064":[1782,916,160,160],"10065":[1134,754,160,160],"10066":[1134,592,160,160],"10067":[1134,430,160,160],"10068":[1134,268,160,160],"10069":[648,1402,160,160],"10070":[1620,1078,160,160],"10071":[486,106,160,160],"10072":[324,1726,160,160],"10073":[1296,1888,160,160],"10074":[1458,1888,160,160],"10075":[1620,1888,160,160],"10076":[1782,1888,160,160],"10077":[162,1726,160,160],"20001":[162,1564,160,160],"20002":[162,1240,160,160],"20003":[162,106,160,160],"20004":[162,1078,160,160],"20005":[162,916,160,160],"20006":[162,754,160,160],"20007":[162,592,160,160],"20008":[162,430,160,160],"20009":[162,268,160,160],"20010":[1134,1888,160,160],"20011":[162,1402,160,160],"20012":[972,1888,160,160],"20013":[0,754,160,160],"20014":[0,1726,160,160],"20015":[0,1564,160,160],"20016":[0,1402,160,160],"20017":[0,1240,160,160],"20018":[0,1078,160,160],"20019":[0,916,160,160],"20020":[0,592,160,160],"20021":[648,1888,160,160],"20022":[0,430,160,160],"20023":[0,268,160,160],"20024":[0,106,160,160],"20025":[162,1888,160,160],"20026":[324,1888,160,160],"20027":[486,1888,160,160],"20028":[810,1888,160,160],"20029":[486,1726,160,160],"20030":[324,268,160,160],"20031":[648,1726,160,160],"20032":[648,1564,160,160],"20033":[810,1564,160,160],"20034":[972,1564,160,160],"20035":[1134,1564,160,160],"20036":[1296,1564,160,160],"20037":[1458,1564,160,160],"20038":[1782,1564,160,160],"20039":[486,430,160,160],"20040":[486,1402,160,160],"20041":[486,1240,160,160],"20042":[486,1078,160,160],"20043":[486,916,160,160],"20044":[486,754,160,160],"20045":[486,592,160,160],"20046":[486,1564,160,160],"20047":[1620,1564,160,160],"20048":[324,106,160,160],"20049":[1782,1726,160,160],"20050":[810,1726,160,160],"20051":[972,1726,160,160],"20052":[1134,1726,160,160],"20053":[1296,1726,160,160],"20054":[1458,1726,160,160],"20055":[1620,1726,160,160],"20056":[324,1564,160,160],"20057":[324,430,160,160],"20058":[324,1402,160,160],"20059":[324,1240,160,160],"20060":[324,1078,160,160],"20061":[324,916,160,160],"20062":[324,754,160,160],"20063":[324,592,160,160],"20064":[486,268,160,160],"20065":[1782,268,160,160]} 4 | EXPLANATION = {'10001': '苹果', 5 | '10002': '黑猩猩', 6 | '10003': '小号', 7 | '10004': '菠萝', 8 | '10005': '红宝石', 9 | '10006': '啤酒', 10 | '10007': '弓箭', 11 | '10008': '音符', 12 | '10009': '颜料', 13 | '10010': '剑', 14 | '10011': '眼镜', 15 | '10012': '放大镜', 16 | '10013': '肉', 17 | '10014': '月亮', 18 | '10015': '蝙蝠', 19 | '10016': '席兹', 20 | '10017': '鞋子', 21 | '10018': '猫咪', 22 | '10019': '金鱼', 23 | '10020': '老鼠', 24 | '10021': '鸟巢', 25 | '10022': '帆船', 26 | '10023': '牛奶', 27 | '10024': '巧克力', 28 | '10025': '橡果', 29 | '10026': '草莓', 30 | '10027': '老虎', 31 | '10028': '兔子', 32 | '10029': '炸弹', 33 | '10030': '西瓜', 34 | '10031': '马', 35 | '10032': '椅子', 36 | '10033': '海滩', 37 | '10034': '寄居蟹骑士', 38 | '10035': '竖笛', 39 | '10036': '魔导书', 40 | '10037': '鱼', 41 | '10038': '圣诞树', 42 | '10039': '棒棒糖', 43 | '10040': '小槌', 44 | '10041': '星星', 45 | '10042': '帽子', 46 | '10043': '咖啡', 47 | '10044': '狗', 48 | '10045': '旗子', 49 | '10046': '蛋糕', 50 | '10047': '麦克风', 51 | '10048': '口红', 52 | '10049': '镜饼', 53 | '10050': '妖精', 54 | '10051': '雨伞', 55 | '10052': '骰子', 56 | '10053': '情书', 57 | '10054': '葡萄', 58 | '10055': '绵羊', 59 | '10056': '豆腐', 60 | '10057': '鸭子', 61 | '10058': '蜻蜓', 62 | '10059': '叶子', 63 | '10060': '钻戒', 64 | '10061': '绳子', 65 | '10062': '盾牌', 66 | '10063': '金币', 67 | '10064': '布丁', 68 | '10065': '项链', 69 | '10066': '稻穗', 70 | '10067': '酱油', 71 | '10068': '鲷鱼烧工厂', 72 | '10069': '吊灯', 73 | '10070': '盐', 74 | '10071': '漩涡', 75 | '10072': '钓竿', 76 | '10073': '锅子', 77 | '10074': '跷跷板', 78 | '10075': '书法', 79 | '10076': '锻炼', 80 | '10077': '幽灵', 81 | '20001': '日和莉', 82 | '20002': '优衣', 83 | '20003': '怜', 84 | '20004': '未奏希', 85 | '20005': '茉莉', 86 | '20006': '茜里', 87 | '20007': '宫子', 88 | '20008': '雪', 89 | '20009': '杏奈', 90 | '20010': '真步', 91 | '20011': '璃乃', 92 | '20012': '初音', 93 | '20013': '七七香', 94 | '20014': '霞', 95 | '20015': '美里', 96 | '20016': '铃奈', 97 | '20017': '香织', 98 | '20018': '伊绪', 99 | '20019': '美美', 100 | '20020': '胡桃', 101 | '20021': '依里', 102 | '20022': '绫音', 103 | '20023': '铃莓', 104 | '20024': '铃', 105 | '20025': '惠理子', 106 | '20026': '咲恋', 107 | '20027': '望', 108 | '20028': '妮诺', 109 | '20029': '忍', 110 | '20030': '秋乃', 111 | '20031': '真阳', 112 | '20032': '尤加利', 113 | '20033': '镜华', 114 | '20034': '智', 115 | '20035': '栞', 116 | '20036': '碧', 117 | '20037': '千歌', 118 | '20038': '真琴', 119 | '20039': '伊莉亚', 120 | '20040': '空花', 121 | '20041': '珠希', 122 | '20042': '纯', 123 | '20043': '美冬', 124 | '20044': '静流', 125 | '20045': '美咲', 126 | '20046': '深月', 127 | '20047': '莉玛', 128 | '20048': '莫妮卡', 129 | '20049': '纺希', 130 | '20050': '步未', 131 | '20051': '流夏', 132 | '20052': '吉塔', 133 | '20053': '佩可莉姆', 134 | '20054': '可可萝', 135 | '20055': '凯露', 136 | '20056': '矛依未', 137 | '20057': '亚里莎', 138 | '20058': '似似花', 139 | '20059': '克莉丝提娜', 140 | '20060': '祈梨', 141 | '20061': '嘉夜', 142 | '20062': '帆稀', 143 | '20063': '克萝依', 144 | '20064': '琪爱儿', 145 | '20065': '优妮', 146 | } 147 | -------------------------------------------------------------------------------- /pcrmemorygames/game_util.py: -------------------------------------------------------------------------------- 1 | import os 2 | from PIL import Image 3 | 4 | from hoshino.typing import MessageSegment 5 | from ._jielong_data import POSITION, EXPLANATION 6 | 7 | 8 | JUMP_ID = '-1' 9 | UNKNOWN_ID = '00000' 10 | VALID_IDS = [id for id in POSITION if id != UNKNOWN_ID] 11 | SUB_PIC_SIZE = 80 12 | FILE_FOLDER_PATH = os.path.dirname(__file__) 13 | BASE_PIC_PATH = os.path.join(FILE_FOLDER_PATH,'AtlasMinigameSrtPanel_shrink.png') 14 | BACKGROUND_PIC_PATH = os.path.join(FILE_FOLDER_PATH,'Background.png') 15 | 16 | 17 | def get_sub_pic_from_id(id, img=Image.open(BASE_PIC_PATH)): 18 | return img.crop((POSITION[id][0] / 2, POSITION[id][1] / 2, POSITION[id][0] / 2 + POSITION[id][2] / 2, 19 | POSITION[id][1] / 2 + POSITION[id][3] / 2)) 20 | 21 | 22 | def generate_full_pic(row_num, col_num, ids, base=None): 23 | if not base: 24 | base = Image.new('RGBA', (row_num * SUB_PIC_SIZE, col_num * SUB_PIC_SIZE), (255, 255, 255, 255)) 25 | else: 26 | base = Image.open(BACKGROUND_PIC_PATH) 27 | img = Image.open(BASE_PIC_PATH) 28 | for index, id in enumerate(ids): 29 | if id == JUMP_ID: 30 | continue 31 | row_index = index // col_num 32 | col_index = index % col_num 33 | cropped = get_sub_pic_from_id(id, img) 34 | base.paste(cropped, (col_index * SUB_PIC_SIZE, row_index * SUB_PIC_SIZE)) 35 | return base 36 | 37 | 38 | def generate_at_message_segment(ulist): 39 | return ''.join([str(MessageSegment.at(uid)) for uid in ulist]) -------------------------------------------------------------------------------- /pcrmemorygames/pcr_neurasthenia.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import random 4 | 5 | from aiocqhttp.message import escape 6 | from hoshino import Service, util 7 | from hoshino.typing import MessageSegment, CQEvent 8 | from . import game_util, GameMaster 9 | 10 | 11 | sv = Service('pcr-neurasthenia', bundle='pcr娱乐', help_=''' 12 | [神经衰弱] 开启一局公主连结主题的神经衰弱小游戏 13 | [神经衰弱排行] 查看神经衰弱小游戏群排行 14 | '''.strip()) 15 | 16 | 17 | # 神经衰弱(Neurasthenia)小游戏相关参数 18 | ROW_NUM = 4 19 | COL_NUM = 4 20 | SHOW_TIME = 6 21 | ANSWER_TIME = 15 22 | TOTAL_PIC_NUM = ROW_NUM * COL_NUM 23 | DB_PATH = os.path.expanduser("~/.hoshino/pcr_neurasthenia.db") 24 | assert TOTAL_PIC_NUM % 2 == 0 25 | 26 | gm = GameMaster(DB_PATH) 27 | 28 | 29 | @sv.on_fullmatch(("神经衰弱排行", "神经衰弱排行榜", "神经衰弱群排行")) 30 | async def neurasthenia_group_ranking(bot, ev: CQEvent): 31 | ranking = gm.db.get_ranking(ev.group_id) 32 | msg = ["【神经衰弱小游戏排行榜】"] 33 | for i, item in enumerate(ranking): 34 | uid, score = item 35 | m = await bot.get_group_member_info(self_id=ev.self_id, group_id=ev.group_id, user_id=uid) 36 | name = m["card"] or m["nickname"] or str(uid) 37 | msg.append(f"第{i + 1}名: {escape(name)}, 总分: {score}分") 38 | await bot.send(ev, "\n".join(msg)) 39 | 40 | 41 | @sv.on_fullmatch('神经衰弱') 42 | async def neurasthenia_game(bot, ev: CQEvent): 43 | if gm.is_playing(ev.group_id): 44 | await bot.finish(ev, "游戏仍在进行中…") 45 | with gm.start_game(ev.group_id) as game: 46 | chosen_ids = random.sample(game_util.VALID_IDS, TOTAL_PIC_NUM // 2) 47 | chosen_ids.extend(chosen_ids) 48 | random.shuffle(chosen_ids) 49 | # 记忆阶段 50 | await bot.send(ev, f'记忆阶段: ({SHOW_TIME}s后我会撤回图片哦~)') 51 | pic = MessageSegment.image(util.pic2b64(game_util.generate_full_pic(ROW_NUM, COL_NUM, chosen_ids))) 52 | msg = await bot.send(ev, pic) 53 | await asyncio.sleep(SHOW_TIME) 54 | await bot.delete_msg(message_id=msg['message_id']) 55 | await asyncio.sleep(2) 56 | # 回答阶段 57 | displayed_sub_pic_index = random.randint(0, TOTAL_PIC_NUM - 1) 58 | chosen_id = chosen_ids[displayed_sub_pic_index] 59 | explanation = game_util.EXPLANATION[chosen_id] 60 | ids = [game_util.JUMP_ID if i != displayed_sub_pic_index else id for i, id in enumerate(chosen_ids)] 61 | pic = MessageSegment.image(util.pic2b64(game_util.generate_full_pic(ROW_NUM, COL_NUM, ids, True))) 62 | await bot.send(ev, f'请告诉我另一个"{explanation}"所在位置的编号~ ({ANSWER_TIME}s后公布答案){pic}') 63 | game.answer = [i for i, id in enumerate(chosen_ids) if i != displayed_sub_pic_index and id == chosen_id][0] + 1 64 | await asyncio.sleep(ANSWER_TIME) 65 | # 结算 66 | game.update_score() 67 | msg_part1 = f'{MessageSegment.at(game.winner[0])}首先答对,真厉害~ 加2分! 当前总分为{game.get_first_winner_score()}分' if game.winner else '' 68 | msg_part2 = f'{game_util.generate_at_message_segment(game.winner[1:])}也答对了, 加1分~' if game.winner[1:] else '' 69 | msg_part3 = f'{game_util.generate_at_message_segment(game.loser)}答错了, 扣1分o(╥﹏╥)o' if game.loser else '' 70 | msg_part4 = '咦, 这轮游戏没人参与, 看来题目可能有点难...' if not (msg_part1 or msg_part2 or msg_part3) else "" 71 | msg_part = '\n'.join([s for s in [msg_part1, msg_part2, msg_part3, msg_part4] if s]) 72 | await bot.send(ev, f'正确答案是: {game.answer}{MessageSegment.image(util.pic2b64(game_util.generate_full_pic(ROW_NUM, COL_NUM, chosen_ids)))}{msg_part}') 73 | 74 | 75 | @sv.on_message() 76 | async def on_input_index(bot, ev: CQEvent): 77 | game = gm.get_game(ev.group_id) 78 | if not game or game.answer == -1 or not ev.message.extract_plain_text().isdigit(): 79 | return 80 | if int(ev.message.extract_plain_text()) == game.answer: 81 | game.record_winner(ev.user_id) 82 | else: 83 | game.record_loser(ev.user_id) -------------------------------------------------------------------------------- /pcrmemorygames/pcr_perfect_match.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os 3 | import random 4 | 5 | from aiocqhttp.message import escape 6 | from hoshino import Service, util 7 | from hoshino.typing import MessageSegment, CQEvent 8 | from . import game_util, GameMaster 9 | 10 | 11 | sv = Service('pcr-perfect-match', bundle='pcr娱乐', help_=''' 12 | [完美配对] 开启一局公主连结主题的完美配対小游戏 13 | [完美配对排行] 查看完美配对小游戏群排行 14 | '''.strip()) 15 | 16 | 17 | # 完美配对(Perfect Match)小游戏相关参数 18 | ROW_NUM = 4 19 | COL_NUM = 4 20 | HIDDEN_NUM = 8 21 | TURN_NUM = 3 22 | BASIC_SHOW_TIME = 9 23 | ANSWER_TIME = 15 24 | TOTAL_PIC_NUM = ROW_NUM * COL_NUM 25 | DB_PATH = os.path.expanduser("~/.hoshino/pcr_perfect_match.db") 26 | assert HIDDEN_NUM * 2 <= TOTAL_PIC_NUM 27 | 28 | gm = GameMaster(DB_PATH) 29 | 30 | 31 | @sv.on_fullmatch(("完美配对排行", "完美配对排行榜", "完美配对群排行")) 32 | async def perfect_match_group_ranking(bot, ev: CQEvent): 33 | ranking = gm.db.get_ranking(ev.group_id) 34 | msg = ["【完美配对小游戏排行榜】"] 35 | for i, item in enumerate(ranking): 36 | uid, score = item 37 | m = await bot.get_group_member_info(self_id=ev.self_id, group_id=ev.group_id, user_id=uid) 38 | name = m["card"] or m["nickname"] or str(uid) 39 | msg.append(f"第{i + 1}名: {escape(name)}, 总分: {score}分") 40 | await bot.send(ev, "\n".join(msg)) 41 | 42 | 43 | @sv.on_fullmatch('完美配对') 44 | async def perfect_match(bot, ev: CQEvent): 45 | if gm.is_playing(ev.group_id): 46 | await bot.finish(ev, "游戏仍在进行中…") 47 | with gm.start_game(ev.group_id) as game: 48 | chosen_ids = random.sample(game_util.VALID_IDS, TOTAL_PIC_NUM) 49 | pushed_index = set() 50 | # 发送若干轮的隐藏部分子图的合成图,保证所有子图至少都被发送一次。发送完毕后等待若干秒再撤回图片 51 | for i in range(TURN_NUM): 52 | await bot.send(ev, f'记忆阶段{i+1}/{TURN_NUM}: ({BASIC_SHOW_TIME - i * 2}s后我会撤回图片哦~)') 53 | if i < TURN_NUM-1: 54 | shown_index = random.sample(range(TOTAL_PIC_NUM), TOTAL_PIC_NUM - HIDDEN_NUM) 55 | ids = [chosen_ids[i] if i in shown_index else game_util.UNKNOWN_ID for i in range(TOTAL_PIC_NUM)] 56 | pushed_index = pushed_index.union(set(shown_index)) 57 | else: 58 | remnant_index = set(range(TOTAL_PIC_NUM)) - pushed_index 59 | shown_index = list(remnant_index) 60 | shown_index.extend(random.sample(pushed_index, TOTAL_PIC_NUM - HIDDEN_NUM - len(remnant_index))) 61 | ids = [chosen_ids[i] if i in shown_index else game_util.UNKNOWN_ID for i in range(TOTAL_PIC_NUM)] 62 | pic = MessageSegment.image(util.pic2b64(game_util.generate_full_pic(ROW_NUM, COL_NUM, ids))) 63 | msg = await bot.send(ev, pic) 64 | await asyncio.sleep(BASIC_SHOW_TIME - i * 2) 65 | await bot.delete_msg(message_id=msg['message_id']) 66 | await asyncio.sleep(3) 67 | # 开始答题 68 | correct_index = random.randint(0, TOTAL_PIC_NUM - 1) 69 | correct_id = chosen_ids[correct_index] 70 | game.answer = correct_index + 1 71 | correct_pic_img = MessageSegment.image(util.pic2b64(game_util.get_sub_pic_from_id(correct_id))) 72 | answer_number_img = MessageSegment.image(f'file:///{os.path.abspath(game_util.BACKGROUND_PIC_PATH)}') 73 | await bot.send(ev, f'还记得"{game_util.EXPLANATION[correct_id]}"的位置吗?{correct_pic_img}请告诉我它的编号~ ({ANSWER_TIME}s后公布答案){answer_number_img}') 74 | await asyncio.sleep(ANSWER_TIME) 75 | # 结算 76 | game.update_score() 77 | msg_part1 = f'{MessageSegment.at(game.winner[0])}首先答对,真厉害~ 加2分! 当前总分为{game.get_first_winner_score()}分' if game.winner else '' 78 | msg_part2 = f'{game_util.generate_at_message_segment(game.winner[1:])}也答对了, 加1分~' if game.winner[1:] else '' 79 | msg_part3 = f'{game_util.generate_at_message_segment(game.loser)}答错了, 扣1分o(╥﹏╥)o' if game.loser else '' 80 | msg_part4 = '咦, 这轮游戏没人参与, 看来题目可能有点难...' if not (msg_part1 or msg_part2 or msg_part3) else "" 81 | msg_part = '\n'.join([s for s in [msg_part1, msg_part2, msg_part3, msg_part4] if s]) 82 | await bot.send(ev, f'正确答案是: {game.answer}{MessageSegment.image(util.pic2b64(game_util.generate_full_pic(ROW_NUM, COL_NUM, chosen_ids)))}{msg_part}') 83 | 84 | 85 | @sv.on_message() 86 | async def on_input_index(bot, ev: CQEvent): 87 | game = gm.get_game(ev.group_id) 88 | if not game or game.answer == -1 or not ev.message.extract_plain_text().isdigit(): 89 | return 90 | if int(ev.message.extract_plain_text()) == game.answer: 91 | game.record_winner(ev.user_id) 92 | else: 93 | game.record_loser(ev.user_id) 94 | -------------------------------------------------------------------------------- /pcrmiddaymusic/_song_data.py: -------------------------------------------------------------------------------- 1 | # 歌曲简介: [歌曲来源, 歌曲id, 前缀说明, 歌曲名称, 演唱者, B站视频BV号(只使用其封面), 活动介绍(可为空)] 2 | SONG_DATA = { 3 | "初音的礼物大作战ed": ['qq', 215041130, "第一次活动: ", "Smiley Contrast", "大桥彩香/小清水亜美", "BV1WC4y1p72H", "初音为了鼓励在牧场努力工作的栞,准备送给栞一件惊喜大礼。初音的礼物大作战就此开幕!"], 4 | "小小甜心冒险家ed": ['qq', 217508272, "第二次活动: ", "Little Adventure", "诸星堇/小倉唯/日高里菜", "BV1qZ4y1H7Aa", "想让镜华辅导学习的未奏希,找到了正在精灵之森游玩的镜华与美美。在镜华的辅导下开始学习了,但是……"], 5 | "吸血鬼猎人with伊莉亚ed": ['qq', 235068773, "第三次活动: ", "Peaceful*ちゃんぷるー", "内田真礼/高森奈津美", "BV1At4y1X7Rd", "为了打败传说中意图统治世界的吸血鬼,真步和香织,踏上了成为吸血鬼猎人的旅途!"], 6 | "危险假日!海边的美食家公主ed": ['qq', 213442914, "第四次活动: ", "えがおのマイホーム", "悠木碧/堀江由衣", "BV1mZ4y1K7Ut", "在繁华街的抽奖中「美食殿堂」的成员们获得了「魅力南国之旅」的招待券。这本该成为一个最棒的假期,然而……"], 7 | "珠希与美冬的无人岛0金币生活ed": ['qq', 221048633, "第五次活动: ", "キンキラ☆ハピネス!", "松嵜麗/田所梓/今井麻美/沼倉愛美", "BV1aW411Q7zp", "在流传着「实现愿望之岛」的海岸边,珠希与美冬突然困意袭来。等到醒来的时候,已经身处无人岛?"], 8 | "黑铁的亡灵ed": ['qq', 221048634, "第六次活动: ", "Aloofness Code", "川澄綾子/高桥智秋/茅原实里/下田麻美", "BV1Ht411y7AL", "纯想要解决王宫的骚乱,却迟迟不能查明凶手。敌人究竟是谁?王宫骑士团,四人的战斗即将开始!"], 9 | "不给布丁就捣蛋!约定的万圣节派对ed": ['qq', 228368072, "第七次活动: ", "もっと!ふたりのパ〜ティ〜ナイト", "大坪由佳/雨宮天", "BV1iE411b7fQ", "在万圣节的夜里,从街上感受到强大灵力的忍,与宫子出发前往调查。而那却是,恐怖一夜的开端……"], 10 | "暮光破坏者ed": ['qq', 232903630, "第八次活动: ", "サイツヨでしょ、でしょ", "髙野麻美/佳村遥", "BV1Yb411T7En", "将各自的目的藏于心中,前往同各方向的流夏、杏奈及七七香与潜伏的巨大邪恶之战即将拉开序幕!"], 11 | "忘却的颂歌ed": ['qq', 228368073, "第九次活动: ", "Ding Dong Holy Night♪", "芹澤優/植田佳奈", "BV1ZJ41167We", "黑暗悄悄逼近圣诞节前热闹的街道,而当黑暗笼罩了千歌及「咲恋救济院」时,狂乱的圣诞节即将掀开序幕……"], 12 | "新春破晓之星大危机ed": ['qq', 244056944, "第十次活动: ", "TwinkleStars", "種田梨沙/早见沙织/东山奈央", "BV1c4411M7WN", "在除夕迫在眉睫的日子里,因为想留下今年最后的回忆而接受委托的3人,为了准备新年而前往街上采买时……"], 13 | "情人节之战!正中红心的甜蜜战斗ed": ['qq', 232903629, "第十一次活动: ", "SUPER CHOCOLATE", "生天目仁美/阿澄佳奈", "BV1dp4y1D79H", "交织了各式各样感情的情人节……而现在,惠理子与静流间炽热的巧克力战争即将掀开序幕!"], 14 | "王都的名侦探 叹息的追缉者ed": ['qq', 235068774, "第十二次活动: ", "未解決な想い", "小松未可子/水濑祈", "BV1of4y1Q74p", "因马车比赛而热闹非凡的城镇发生了多起悬疑事件,无法视作是偶然的意外——藏在暗地里的恶意……王都的名侦探,将挑战这前所未有的艰难事件!"], 15 | "在阿斯特朗盛开的双轮之花ed": ['163', 1380874627, "第十三次活动(玛娜莉亚魔法学院联动): ", "Crossing Destiny", "日笠陽子/福原綾香", "BV1gK4y1x75x", "当安与古蕾娅正在玛娜莉亚魔法学院帮忙露做功课时,突然被一道不可思议的光包围了……双轮之花,即将在这个异世界盛开!"], 16 | "将军道中记 白翼的武士ed": ['qq', 244056941, "第十四次活动: ", "白翼のグローリエ", "大空直美/佐藤聡美/長妻樹里/大関英里/辻あゆみ", "BV1m4411y74C", "求救的呼喊声,今日也回响在八百八町中……纯白之翼将要除恶,我若不做将军、谁做将军!"], 17 | "Re:从零开始聚集的异世界餐桌ed": ['bili', "BV1Bz411z7Cp", "第十五次活动(Re:0联动): ", "Re:lation", "高桥李依/水濑祈/村川梨衣", "BV1Bz411z7Cp", "由异世界来到了兰德索尔的爱蜜莉雅等人与「美食殿堂」的成员们一同追查一起失踪事件,但是……"], 18 | "铃奈的彩虹舞台ed": ['qq', 248110954, "第十六次活动: ", "背伸びFirst Kiss", "伊藤静/久野美咲/上坂堇", "BV1Xt41157gz", "那里会是座雨未曾停歇的南国之岛……贯穿下着悲伤之泪的雨云的虹桥,宣告着最棒的假期将要揭开序幕!"], 19 | "盛夏的真步真步王国!浪花拍岸的灵魂之夏ed": ['qq', 248110953, "第十七次活动: ", "We Are Golden", "内田真礼/小松未可子/高森奈津美", "BV1qJ411j72y", "真步带着她最珍视的玩偶们,与公会成员们造访了夏季的海边。本应一起度过一段快乐的时光,可玩偶们却突然失去了踪影……?"], 20 | "森林的胆小鬼与神圣学院的问题儿童ed": ['qq', 248113890, "第十八次活动: ", "なかよしセンセーション", "小原好美/佐仓绫音/種崎敦美", "BV1oz4y1D7R5", "碧作为交换生造访了圣德蕾莎女子学院,而在那里等着她的,是充满个性的问题儿童们!"], 21 | "小小勇敢的万圣节之夜ed": ['qq', 248113891, "第十九次活动: ", "トリックホリック(Trick Holic)", "日高里菜/小倉唯/诸星堇", "BV1j7411Z7Rz", "小小甜心一行人原本打算到幽灵鬼屋去要糖果,却反而被关在了屋内……怀着小小勇气的冒险谭,即将展开!"], 22 | "龙的探索者们ed": ['qq', 261407114, "第二十次活动: ", "in flames", "藤田茜/小市眞琴/大西沙織", "BV1gE411c7bg", "为了暗中调查公会「龙族巢穴」而联袂前往的智与茉莉,她们到达的是……充满谜团的未开发之地,艾尔皮斯岛!"], 23 | "礼物大恐慌!兰德索尔的圣诞老人们ed": ['qq', 261407130, "第二十一次活动: ", "Call Me Darling!", "日笠陽子/福原綾香/木戸衣吹", "BV1Tg4y1i7Ym", "仅有一人能够当上圣诞老人。而成为了圣诞老人候补的伊莉亚等人为了将喜悦分送给城里的人们,在兰德索尔中来回穿梭着……"], 24 | "狂奔!兰德索尔的公会竞速赛ed": ['qq', 267175838, "第二十二次活动: ", "SAI*KOUスタートダッシュ", "M・A・O/立花理香/悠木碧/伊藤美来", "BV17T4y1u7Pr", "赌上豪华奖品与福气公会的称号,「美食殿堂」与铃莓将携手挑战「新春兰德索尔的公会竞速赛」!"], 25 | "二人是魔法少女Misty&Purely ed": ['qq', 267175904, "第二十三次活动: ", "木もれびモンタージュ", "水濑祈/小清水亜美", "BV1Ea4y1v79c", "巨大的灾难悄悄逼近了兰德索尔。就在此时,有两名少女挺身而出。以爱与希望变身吧!魔法侦探&魔法猎人!"], 26 | "星光公主Re:M@ster!ed": ['bili', "BV1yE411V7YP", "第二十四次活动(偶像大师灰姑娘女孩星光舞台联动): ", "Great Journey", "大桥彩音/福原绫香/原纱友里", "BV1yE411V7YP", "公会「new generations」的三位成员,突然出现在「美食殿堂」一行人面前。以最顶尖偶像为目标,冒险即将揭开序幕!"], 27 | "恩赐的财团与神圣学院的问题儿童ed": ['qq', 269759310, "第二十五次活动: ", "青春スピナー", "佐仓绫音/種崎敦美/小原好美", "BV1VT4y1E7zV", "「圣德蕾莎女子学院(好朋友社)」的三人参加了全新的特别讲座。在成功的报酬的面前,那名为上进心的私心此刻正熊熊燃烧!"], 28 | "牧场的四农士,贫穷牧场奋斗记!ed": ['qq', 269759305, "第二十六次活动: ", "Heartful Place", "新田恵海/小岩井小鸟", "BV1654y1v7eo", "面对突如其来的公会征收骚动,「伊丽莎白牧场」的成员们团结一心挺身奋战!"], 29 | "不可思议之国的璃乃ed": ['qq', 275615975, "第二十七次活动: ", "フェアリーテイルは夢の中", "阿澄佳奈", "BV1WK4y1t7cU", "璃乃等人被书卷进了不可思议的国度,为了守护面临毁灭危机的绘本世界,璃乃等人身为「救世主」的战役即将展开!"], 30 | "七夕剑客旅情谭ed": ['qq', 275615978, "第二十八次活动: ", "黄昏太平旅路唄", "佐藤利奈", "BV13D4y1S72P", ""], 31 | "美里夏日应援!逐梦的盛夏棒球队ed": ['bili', "BV1ah411Z7Dm", "第二十九次活动: ", "あの夏のメモリー", "国府田麻理子", "BV1Rk4y1m74f", ""], 32 | "快乐互换的天使们ed": ['bili', "BV1jk4y1y71x", "第三十次活动: ", "ねぇねぇPlease!", "原紗友里/浅倉杏美", "BV11V41127dj", ""], 33 | "呐喊!绝叫!万圣鬼节ed": ['bili', "BV19i4y1E7Fs", "第三十一次活动: ", "Paradox", "早见沙织/木户衣吹", "BV19i4y1E7Fs", ""], 34 | "小望角色歌": ['qq', 215041131, "", "君の笑顔が見たいから", "日笠陽子", "BV1Ni4y187WU", ""], 35 | "千歌角色歌": ['qq', 214032974, "", "風への誓い", "福原綾香", "BV1v4411a7bx", ""], 36 | "纺希角色歌": ['qq', 217508273, "", "アマノジャクハート!", "木戸衣吹", "BV1X54y1q7bF", ""], 37 | "凯露角色歌": ['qq', 231005151, "主线第八章ed, ", "Absolute Secret", "立花理香", "BV1rQ4y1M7sB", ""], 38 | "慈乐之音live": ['qq', 214032971, "主线第九章ed, ", "Shining Future", "日笠陽子/福原綾香/木戸衣吹", "BV1XJ411u7Hu", ""], 39 | "前作op": ['163', 1309896539, "主线第十三章ed, ", "つなぐもの", "種田梨沙/東山奈央/早見沙織", "BV1kE411e7Xi", ""], 40 | "游戏第一部op": ['qq', 231005146, "", "Lost Princess", "M・A・O/伊藤美来/立花理香", "BV12J411w7EM", ""], 41 | "游戏第一部ed": ['qq', 231005147, "", "Connecting Happy!!", "M・A・O/伊藤美来/立花理香", "BV1J64y1T7gr", ""], 42 | "游戏第二部op": ['qq', 256334989, "", "Mirage Game", "M・A・O/伊藤美来/立花理香/種田梨沙/东山奈央/早見沙織/近藤玲奈", "BV1qz411B7ZG", ""], 43 | "游戏第二部ed": ['qq', 256335010, "", "Yes! Precious Harmony!", "M・A・O/伊藤美来/立花理香", "BV1v7411J7ub", ""], 44 | } -------------------------------------------------------------------------------- /pcrmiddaymusic/pcrmiddaymusic.py: -------------------------------------------------------------------------------- 1 | import asyncio, json, os, random 2 | 3 | import hoshino 4 | from hoshino import Service, aiorequests 5 | from . import _song_data 6 | from hoshino.typing import CQEvent, MessageSegment 7 | 8 | sv = Service('pcr-midday-music', bundle='pcr娱乐', help_=''' 9 | 每日午间自动推送pcr相关音乐, 也可直接在群内发送"来点音乐"请求pcr歌曲 10 | '''.strip()) 11 | 12 | FILE_FOLDER_PATH = os.path.dirname(__file__) 13 | 14 | config_using = set() 15 | CONFIG_PATH = os.path.join(FILE_FOLDER_PATH,'pushed_music.json') 16 | 17 | 18 | class Config: 19 | def __init__(self, gid, config_path): 20 | self.gid = str(gid) 21 | self.config_path = config_path 22 | 23 | def __enter__(self): 24 | config_using.add(self.gid) 25 | return self 26 | 27 | def __exit__(self, type, value, trace): 28 | if self.gid in config_using: 29 | config_using.remove(self.gid) 30 | 31 | def load(self): 32 | try: 33 | if os.path.exists(self.config_path): 34 | with open(self.config_path, 'r', encoding='utf8') as config_file: 35 | return json.load(config_file) 36 | else: 37 | return {} 38 | except: 39 | return {} 40 | 41 | def save(self, config): 42 | try: 43 | with open(self.config_path, 'w', encoding='utf8') as config_file: 44 | json.dump(config, config_file, ensure_ascii=False, indent=4) 45 | return True 46 | except: 47 | return False 48 | 49 | def load_pushed_music(self): 50 | config = self.load() 51 | return config[self.gid] if self.gid in config else [] 52 | 53 | def save_pushed_music(self, pushed_music): 54 | config = self.load() 55 | config[self.gid] = pushed_music 56 | return self.save(config) 57 | 58 | 59 | async def get_pic_url(bv): 60 | url = f'https://api.bilibili.com/x/web-interface/view?bvid={bv}' 61 | resp = await aiorequests.get(url) 62 | content = await resp.json() 63 | return content['data']['pic'] 64 | 65 | 66 | async def get_song_info_from_song(song): 67 | song_data = _song_data.SONG_DATA[song] 68 | pic_url = await get_pic_url(song_data[5]) if song_data[5] else "" 69 | img = MessageSegment.image(pic_url) if pic_url else "" 70 | msg_part = '' if song_data[6] == '' else '\n-------------------------------------------\n' 71 | song_info = song_data[2] + song + str(img) + '歌曲名: ' + song_data[3] + '\n' + '歌手: ' + song_data[4] + msg_part + \ 72 | song_data[6] 73 | return song_info, song_data 74 | 75 | 76 | async def get_next_song(gid): 77 | with Config(gid, CONFIG_PATH) as config: 78 | pushed_music = config.load_pushed_music() 79 | song_dict = _song_data.SONG_DATA 80 | not_pushed_music = set(song_dict.keys()) - set(pushed_music) 81 | if not_pushed_music: 82 | song = random.choice(list(not_pushed_music)) 83 | else: 84 | song = random.choice(list(song_dict.keys())) 85 | pushed_music = [] 86 | song_info, song_data = await get_song_info_from_song(song) 87 | pushed_music.append(song) 88 | config.save_pushed_music(pushed_music) 89 | return song_info, song_data 90 | 91 | 92 | def get_music_from_song_data(song_data): 93 | if song_data[0] == 'bili': 94 | return MessageSegment.share(url='https://www.bilibili.com/video/' + song_data[1], title=song_data[3], 95 | content=song_data[4], 96 | image_url="http://i0.hdslb.com/bfs/archive/b28c463d04db58f6eb79e238757b78ab1f609ec0.png") 97 | elif song_data[0] == 'qq': 98 | return MessageSegment(type_='music', 99 | data={ 100 | 'type': song_data[0], 101 | 'id': str(song_data[1]), 102 | 'content': song_data[4] 103 | }) 104 | else: 105 | return MessageSegment.music(type_=song_data[0], id_=song_data[1]) 106 | 107 | 108 | def keyword_search(keyword): 109 | song_dict = _song_data.SONG_DATA 110 | result = [] 111 | for song in song_dict: 112 | if keyword in song or keyword in song_dict[song][3]: 113 | result.append(song) 114 | return result 115 | 116 | 117 | @sv.on_prefix(('来点音乐', '来首音乐')) 118 | async def music_push(bot, ev: CQEvent): 119 | if ev.group_id in config_using: 120 | return 121 | s = ev.message.extract_plain_text() 122 | if s: 123 | available_songs = keyword_search(s) 124 | if not available_songs: 125 | await bot.send(ev, f'未找到含有关键词"{s}"的歌曲...') 126 | return 127 | elif len(available_songs) > 1: 128 | msg_part = '\n'.join(['• ' + song for song in available_songs]) 129 | await bot.send(ev, f'从曲库中找到了这些:\n{msg_part}\n您想找的是哪首呢~') 130 | return 131 | else: 132 | song_info, song_data = await get_song_info_from_song(available_songs[0]) 133 | else: 134 | song_info, song_data = await get_next_song(ev.group_id) 135 | await bot.send(ev, song_info) 136 | await bot.send(ev, get_music_from_song_data(song_data)) 137 | 138 | 139 | @sv.scheduled_job('cron', hour=12, minute=9) 140 | async def music_daily_push(): 141 | bot = hoshino.get_bot() 142 | glist = await sv.get_enable_groups() 143 | info_head = '今日份的午间音乐广播~' 144 | for gid, selfids in glist.items(): 145 | song_info, song_data = await get_next_song(gid) 146 | sid = random.choice(selfids) 147 | await bot.send_group_msg(self_id=sid, group_id=gid, message=info_head+song_info) 148 | await bot.send_group_msg(self_id=sid, group_id=gid, message=get_music_from_song_data(song_data)) 149 | await asyncio.sleep(2) -------------------------------------------------------------------------------- /pcrsealkiller/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | 5 | class Config: 6 | threshold = {} 7 | 8 | def __init__(self, config_path): 9 | self.config_path = config_path 10 | self.load_config() 11 | 12 | def load_config(self): 13 | try: 14 | if os.path.exists(self.config_path): 15 | with open(self.config_path, 'r', encoding='utf8') as config_file: 16 | self.threshold = json.load(config_file) 17 | else: 18 | self.threshold = {} 19 | except: 20 | self.threshold = {} 21 | 22 | def save_config(self): 23 | with open(self.config_path, 'w', encoding='utf8') as config_file: 24 | json.dump(self.threshold, config_file, ensure_ascii=False, indent=4) 25 | 26 | def set_threshold(self, gid, threshold): 27 | self.threshold[gid] = threshold 28 | self.save_config() 29 | 30 | def delete_threshold(self, gid): 31 | if gid in self.threshold: 32 | del self.threshold[gid] 33 | self.save_config() 34 | 35 | -------------------------------------------------------------------------------- /pcrsealkiller/_opencv_util.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | 3 | 4 | # 寻找第一个上边缘,即黑色像素点个数大于阈值的行 5 | def find_first_edge_index(row_pixel_nums, pixel_threshold): 6 | for i in range(len(row_pixel_nums)): 7 | if row_pixel_nums[i] > pixel_threshold: 8 | return i 9 | return -1 10 | 11 | 12 | # 判断在第一个上边缘之上是否存在若干含有少量黑色像素点的行(即"NEW"图标的上半部分) 13 | def is_new_gacha_above_edge(row_pixel_nums, edge_index, pixel_threshold, num_threshold): 14 | return len([i for i in row_pixel_nums[0:edge_index] if 0 < i < pixel_threshold]) > num_threshold 15 | 16 | 17 | # 判断整数列表中是否存在连续的required_amount个0,如果有,则返回连续0初始位置的index 18 | def find_continuous_zeros(nlist, required_amount): 19 | i = 0 20 | while i < len(nlist): 21 | continuous_zeros_count = 0 22 | for j in range(len(nlist)-i): 23 | if nlist[i+j] == 0: 24 | continuous_zeros_count += 1 25 | else: 26 | break 27 | if continuous_zeros_count >= required_amount: 28 | return i 29 | i += continuous_zeros_count + 1 30 | return -1 31 | 32 | 33 | # 判断十连抽得到的10张新卡组成的2*5方阵的两个上边缘之上的空白处有没有少量黑色像素点(即"NEW"图标的上半部分) 34 | def is_new_gacha(row_pixel_nums): 35 | max_row_pixel = max(row_pixel_nums) 36 | first_edge_index = find_first_edge_index(row_pixel_nums, max_row_pixel * 0.5) 37 | if is_new_gacha_above_edge(row_pixel_nums, first_edge_index, max_row_pixel * 0.1, 5): 38 | return True 39 | residual_row_pixel_nums = row_pixel_nums[first_edge_index:] 40 | residual_row_pixel_nums = residual_row_pixel_nums[find_continuous_zeros(residual_row_pixel_nums, 5):] 41 | second_edge_index = find_first_edge_index(residual_row_pixel_nums, max_row_pixel * 0.5) 42 | return is_new_gacha_above_edge(residual_row_pixel_nums, second_edge_index, max_row_pixel * 0.1, 5) 43 | 44 | 45 | def check_new_gacha(gacha_screenshot_path): 46 | try: 47 | # 读取图片并二值化 48 | image = cv2.imread(gacha_screenshot_path) 49 | img_gray = cv2.cvtColor(image.copy(), cv2.COLOR_BGR2GRAY) 50 | image_binary = cv2.threshold(img_gray, 200, 255, cv2.THRESH_BINARY)[1] 51 | contours = cv2.findContours(image_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] 52 | # 寻找最大矩形轮廓 53 | max_area = 0 54 | for c in contours: 55 | peri = cv2.arcLength(c, True) 56 | approx = cv2.approxPolyDP(c, 0.02 * peri, True) 57 | if len(approx) == 4: 58 | if cv2.contourArea(c) > max_area: 59 | max_area = cv2.contourArea(c) 60 | max_approx = approx 61 | x = [max_approx[0][0][0], max_approx[1][0][0], max_approx[2][0][0], max_approx[3][0][0]] 62 | y = [max_approx[0][0][1], max_approx[1][0][1], max_approx[2][0][1], max_approx[3][0][1]] 63 | x.sort() 64 | y.sort() 65 | xlow = max(x[0], x[1]) 66 | xhigh = min(x[2], x[3]) 67 | ylow = max(y[0], y[1]) 68 | yhigh = min(y[2], y[3]) 69 | # 裁剪原图,得到主要部分 70 | image_cropped = image[ylow:yhigh, xlow:xhigh] 71 | # 对主要部分进行Otsu二值化滤去背景 72 | img_gray = cv2.cvtColor(image_cropped, cv2.COLOR_BGR2GRAY) 73 | img_Otsu = cv2.threshold(img_gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] 74 | image_binary = cv2.threshold(img_Otsu, 60, 255, cv2.THRESH_BINARY_INV)[1] 75 | # 统计各行黑色像素点个数,并判断是否存在"NEW" 76 | row_pixel_nums = [] 77 | for i in range(image_binary.shape[0]): 78 | row_pixel_nums.append(int(sum(image_binary[i, :]) / 255)) 79 | return is_new_gacha(row_pixel_nums), False 80 | except: 81 | return False, True 82 | -------------------------------------------------------------------------------- /pcrsealkiller/sealkiller.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pcrsealkiller/sealkiller.jpg -------------------------------------------------------------------------------- /pcrsealkiller/sealkiller.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import math 3 | import os 4 | import re 5 | 6 | import aiohttp 7 | 8 | from hoshino import Service, util, priv 9 | from . import Config 10 | from hoshino.typing import CQEvent, MessageSegment 11 | 12 | sv = Service('pcr-seal-killer', bundle='pcr娱乐', help_=''' 13 | 自动击杀晒卡海豹,请给机器人管理员或者群主,配置指令如下: 14 | 启用海豹杀手 [海豹判定阈值]:如果不输入参数,默认阈值是100 15 | 禁用海豹杀手:关闭海豹杀手服务,减轻机器人运行开销 16 | '''.strip()) 17 | 18 | GACHA_KEYWORDS = ['所持角色交换Pt', '持有的角色交換Pt', '所持キャラ交換Pt', '持有的角色交换Pt', '所持キャラ交换Pt', '所持CSPキャラ交換Pt'] 19 | RUN_PATH = os.getcwd() 20 | FILE_FOLDER_PATH = os.path.dirname(__file__) 21 | RELATIVE_PATH = os.path.relpath(FILE_FOLDER_PATH, RUN_PATH) 22 | CONFIG_PATH = os.path.join(FILE_FOLDER_PATH,'config.json') 23 | PIC_PATH = os.path.join(FILE_FOLDER_PATH,'sealkiller.jpg') 24 | DEFAULT_GACHA_THRESHOLD = 100 # 海豹判定阈值, 如果抽卡次数小于这个阈值,则被判定为海豹 25 | STRICT_MODE = True # 开启严格模式后,如果未发现"NEW"而抽卡次数小于阈值,仍会撤回消息,但是不禁言(宁可错杀也不可放过海豹) 26 | USE_OPENCV = True # 是否使用Opencv提高识别精确度 27 | 28 | 29 | gacha_threshold = Config(CONFIG_PATH) 30 | ocred_images = {} 31 | if USE_OPENCV: 32 | opencv_util = importlib.import_module(RELATIVE_PATH.replace(os.sep,'.') + '._opencv_util') 33 | 34 | 35 | async def is_image_gif_or_meme(bot, img): # 原有基础上添加表情包过滤功能,感谢HoshinoBot群友们的创意 36 | r = await bot.call_action(action='get_image', file=img) 37 | return r['filename'].endswith('gif') or r['size'] < 50000 38 | 39 | 40 | async def is_possible_gacha_image(bot, ev, img): 41 | is_gif_or_meme = await is_image_gif_or_meme(bot, img) 42 | is_ocred = ev.group_id in ocred_images and img in ocred_images[ev.group_id] 43 | return not (is_gif_or_meme or is_ocred) 44 | 45 | 46 | def record_ocr(gid, img): 47 | if gid not in ocred_images: 48 | ocred_images[gid] = [] 49 | if img not in ocred_images[gid]: 50 | ocred_images[gid].append(img) 51 | 52 | 53 | def get_gacha_amount(ocr_result): 54 | string = re.search('[0-9]+.\+[0-9]+', str(ocr_result)) 55 | if not string: # OCR未识别到抽卡次数 56 | return 0 57 | gacha_amount = re.match('[0-9]+', string.group(0)).group(0) 58 | if len(gacha_amount) > 3: # OCR识别到多余数字时 59 | gacha_amount = gacha_amount[math.floor(len(gacha_amount)/2):] 60 | return int(gacha_amount) if gacha_amount.isdigit() else 0 61 | 62 | async def judge_bot_auth(bot, ev): 63 | bot_info = await bot.get_group_member_info(group_id=ev.group_id, user_id=ev.self_id) 64 | if not bot_info['role'] == 'member': 65 | return True 66 | return False 67 | 68 | async def download(url, path): 69 | timeout = aiohttp.ClientTimeout(total=60) 70 | async with aiohttp.ClientSession(timeout=timeout) as session: 71 | async with session.get(url) as resp: 72 | content = await resp.read() 73 | with open(path, 'wb') as f: 74 | f.write(content) 75 | 76 | 77 | # 返回: 是否需要撤回 是否需要禁言 78 | async def check_image(bot, ev, img): 79 | try: 80 | r = await bot.call_action(action='.ocr_image', image=img) 81 | except: 82 | return False, False 83 | kw = is_gacha_screenshot(r) 84 | if not kw: 85 | record_ocr(ev.group_id, img) 86 | return False, False 87 | else: 88 | if not is_new_gacha(r, get_text_coordinate_y(r, kw)): 89 | if not USE_OPENCV: 90 | if not STRICT_MODE: 91 | record_ocr(ev.group_id, img) 92 | return False, False 93 | else: 94 | gacha_amount = get_gacha_amount(r) 95 | if not gacha_amount or gacha_amount < int(gacha_threshold.threshold[str(ev.group_id)]): 96 | return True, False 97 | else: 98 | record_ocr(ev.group_id, img) 99 | return False, False 100 | else: 101 | image_path = f'{FILE_FOLDER_PATH}{img}.jpg' 102 | image_info = await bot.call_action(action='get_image', file=img) 103 | await download(image_info['url'], image_path) 104 | new_gacha, error = opencv_util.check_new_gacha(image_path) 105 | if os.path.exists(image_path): 106 | os.remove(image_path) 107 | if new_gacha: 108 | gacha_amount = get_gacha_amount(r) 109 | if not gacha_amount: 110 | return True, False 111 | elif gacha_amount < int(gacha_threshold.threshold[str(ev.group_id)]): 112 | return True, True 113 | else: 114 | record_ocr(ev.group_id, img) 115 | return False, False 116 | else: 117 | if not error: 118 | record_ocr(ev.group_id, img) 119 | return False, False 120 | else: 121 | if not STRICT_MODE: 122 | record_ocr(ev.group_id, img) 123 | return False, False 124 | else: 125 | gacha_amount = get_gacha_amount(r) 126 | if not gacha_amount or gacha_amount < int(gacha_threshold.threshold[str(ev.group_id)]): 127 | return True, False 128 | else: 129 | record_ocr(ev.group_id, img) 130 | return False, False 131 | else: 132 | gacha_amount = get_gacha_amount(r) 133 | if not gacha_amount: 134 | return True, False 135 | elif gacha_amount < int(gacha_threshold.threshold[str(ev.group_id)]): 136 | return True, True 137 | else: 138 | record_ocr(ev.group_id, img) 139 | return False, False 140 | 141 | 142 | def is_gacha_screenshot(ocr_result): 143 | ocr_result_string = str(ocr_result) 144 | for keyword in GACHA_KEYWORDS: 145 | if keyword in ocr_result_string: 146 | return keyword 147 | return '' 148 | 149 | 150 | def get_text_coordinate_y(ocr_result, text): 151 | text_list = ocr_result['texts'] 152 | for t in text_list: 153 | if t['text'] == text: 154 | return t['coordinates'][0]['y'] 155 | 156 | 157 | def is_new_gacha(ocr_result, max_text_coordinate_y): 158 | text_list = ocr_result['texts'] 159 | for t in text_list: 160 | if t['text'] == 'NEW' and t['coordinates'][0]['y'] < max_text_coordinate_y: 161 | return True 162 | return False 163 | 164 | 165 | @sv.on_prefix(('启用海豹杀手', '启动海豹杀手')) 166 | async def enable_sealkiller(bot, ev: CQEvent): 167 | if not priv.check_priv(ev, priv.ADMIN): 168 | await bot.finish(ev, '抱歉,您非管理员,无此指令使用权限') 169 | s = ev.message.extract_plain_text() 170 | if s: 171 | if s.isdigit() and 00", (gid, uid) 51 | ).fetchall() 52 | return {c[0]: c[1] for c in r} if r else {} 53 | 54 | def get_surplus_cards(self, gid, uid): 55 | with self.connect() as conn: 56 | r = conn.execute( 57 | "SELECT cid, num FROM card_record WHERE gid=? AND uid=? AND num>1", (gid, uid) 58 | ).fetchall() 59 | return {c[0]: (c[1]-1) for c in r} if r else {} 60 | 61 | def get_group_ranking(self, gid, uid): 62 | with self.connect() as conn: 63 | r = conn.execute( 64 | "SELECT uid FROM card_record WHERE gid=? AND num>0", (gid,) 65 | ).fetchall() 66 | if not r: 67 | return -1 68 | cards_num = Counter([s[0] for s in r]) 69 | if uid not in cards_num: 70 | return -1 71 | user_card_num = cards_num[uid] 72 | return sum(n > user_card_num for n in cards_num.values()) + 1 73 | 74 | def exist_check(self,key): 75 | try: 76 | key = str(key) 77 | with self.connect() as conn: 78 | conn.execute("INSERT INTO limiter (key,num,date) VALUES (?, 0,-1)",(key,),) 79 | return 80 | except: 81 | return 82 | 83 | def get_num(self,key): 84 | self.exist_check(key) 85 | key = str(key) 86 | with self.connect() as conn: 87 | r = conn.execute( 88 | "SELECT num FROM limiter WHERE key=? ", (key,) 89 | ).fetchall() 90 | r2 = r[0] 91 | return r2[0] 92 | 93 | def clear_key(self,key): 94 | key = str(key) 95 | self.exist_check(key) 96 | with self.connect() as conn: 97 | conn.execute("UPDATE limiter SET num=0 WHERE key=?",(key,),) 98 | return 99 | 100 | def increment_key(self,key,num): 101 | self.exist_check(key) 102 | key = str(key) 103 | with self.connect() as conn: 104 | conn.execute("UPDATE limiter SET num=num+? WHERE key=?",(num,key,)) 105 | return 106 | 107 | def get_date(self,key): 108 | self.exist_check(key) 109 | key = str(key) 110 | with self.connect() as conn: 111 | r = conn.execute( 112 | "SELECT date FROM limiter WHERE key=? ", (key,) 113 | ).fetchall() 114 | r2 = r[0] 115 | return r2[0] 116 | 117 | def set_date(self,date,key): 118 | print(date) 119 | self.exist_check(key) 120 | key = str(key) 121 | with self.connect() as conn: 122 | conn.execute("UPDATE limiter SET date=? WHERE key=?",(date,key,),) 123 | return 124 | 125 | class ExchangeRequest: 126 | def __init__(self, sender_uid, card1_id, card1_name, target_uid, card2_id, card2_name): 127 | self.sender_uid = sender_uid 128 | self.card1_id = card1_id 129 | self.card1_name = card1_name 130 | self.target_uid = target_uid 131 | self.card2_id = card2_id 132 | self.card2_name = card2_name 133 | self.request_time = datetime.now() 134 | 135 | 136 | class ExchangeRequestMaster: 137 | def __init__(self, max_valid_time): 138 | self.last_exchange_request = {} 139 | self.max_valid_time = max_valid_time 140 | 141 | def add_exchange_request(self, gid, uid, request: ExchangeRequest): 142 | self.last_exchange_request[(gid, uid)] = request 143 | 144 | def get_last_exchange_request_time(self, gid, uid): 145 | return self.last_exchange_request[(gid, uid)].request_time if (gid, uid) in self.last_exchange_request else datetime(2020, 4, 17, 0, 0, 0, 0) 146 | 147 | def has_exchange_request_to_confirm(self, gid, uid): 148 | now_time = datetime.now() 149 | delta_time = now_time - self.get_last_exchange_request_time(gid, uid) 150 | return delta_time.total_seconds() <= self.max_valid_time 151 | 152 | def get_exchange_request(self, gid, uid) -> ExchangeRequest: 153 | return self.last_exchange_request[(gid, uid)] 154 | 155 | def delete_exchange_request(self, gid, uid): 156 | if (gid, uid) in self.last_exchange_request: 157 | del self.last_exchange_request[(gid, uid)] 158 | 159 | DB_PATH = os.path.expanduser("~/.hoshino/poke_man_pcr.db") 160 | db = CardRecordDAO(DB_PATH) 161 | 162 | class DailyAmountLimiter(DailyNumberLimiter): 163 | def __init__(self, types, max_num, reset_hour): 164 | super().__init__(max_num) 165 | self.reset_hour = reset_hour 166 | self.type=types 167 | 168 | def check(self, key) -> bool: 169 | now = datetime.now(self.tz) 170 | key = list(key) 171 | key.append(self.type) 172 | key = tuple(key) 173 | day = (now - timedelta(hours=self.reset_hour)).day 174 | if day != db.get_date(key): 175 | db.set_date(day,key) 176 | db.clear_key(key) 177 | return bool(db.get_num(key) < self.max) 178 | 179 | def get_num(self, key): 180 | key = list(key) 181 | key.append(self.type) 182 | key = tuple(key) 183 | return db.get_num(key) 184 | 185 | def increase(self, key, num=1): 186 | key = list(key) 187 | key.append(self.type) 188 | key = tuple(key) 189 | db.increment_key(key,num) 190 | 191 | def reset(self, key): 192 | key = list(key) 193 | key.append(self.type) 194 | key = tuple(key) 195 | db.clear_key(key) 196 | -------------------------------------------------------------------------------- /pokemanpcr/image/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/background.png -------------------------------------------------------------------------------- /pokemanpcr/image/frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/frame.png -------------------------------------------------------------------------------- /pokemanpcr/image/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/new.png -------------------------------------------------------------------------------- /pokemanpcr/image/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/normal.png -------------------------------------------------------------------------------- /pokemanpcr/image/pokecriticalstrike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/pokecriticalstrike.png -------------------------------------------------------------------------------- /pokemanpcr/image/quantity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/quantity.png -------------------------------------------------------------------------------- /pokemanpcr/image/rare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/rare.png -------------------------------------------------------------------------------- /pokemanpcr/image/superiorsuperrare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/superiorsuperrare.png -------------------------------------------------------------------------------- /pokemanpcr/image/superrare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GWYOG/GWYOG-Hoshino-plugins/92f55a899b62669a7d6db66c755eb3581a134bfe/pokemanpcr/image/superrare.png -------------------------------------------------------------------------------- /pokemanpcr/poke_man_pcr.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import base64 4 | from PIL import Image, ImageFont, ImageDraw 5 | 6 | import hoshino 7 | from hoshino import Service, util 8 | from hoshino.modules.priconne import chara, _pcr_data 9 | from hoshino.typing import MessageSegment, NoticeSession, CQEvent 10 | from . import * 11 | from ...util import FreqLimiter 12 | from io import BytesIO 13 | 14 | 15 | __BASE = os.path.split(os.path.realpath(__file__)) 16 | FRAME_DIR_PATH = os.path.join(__BASE[0], 'image') 17 | DIR_PATH = os.path.join(os.path.expanduser( 18 | hoshino.config.RES_DIR), 'img', 'priconne', 'unit') 19 | DB_PATH = os.path.expanduser("~/.hoshino/poke_man_pcr.db") 20 | POKE_GET_CARDS = 0.75 # 每一戳的卡片掉落几率 21 | POKE_DAILY_LIMIT = 3 # 机器人每天掉落卡片的次数 22 | RARE_PROBABILITY = 0.17 # 戳一戳获得稀有卡片的概率 23 | SUPER_RARE_PROBABILITY = 0.03 # 戳一戳获得超稀有卡片的概率 24 | REQUEST_VALID_TIME = 60 # 换卡请求的等待时间 25 | POKE_TIP_LIMIT = 1 # 到达每日掉落上限后的短时最多提示次数 26 | TIP_CD_LIMIT = 10*60 # 每日掉落上限提示冷却时间 27 | POKE_COOLING_TIME = 3 # 增加冷却时间避免连续点击 28 | GIVE_DAILY_LIMIT = 3 # 每人每天最多接受几次赠卡 29 | RESET_HOUR = 0 # 每日戳一戳、赠送等指令使用次数的重置时间,0代表凌晨0点,1代表凌晨1点,以此类推 30 | COL_NUM = 17 # 查看仓库时每行显示的卡片个数 31 | OMIT_THRESHOLD = 20 # 当获得卡片数超过这个阈值时,不再显示获得卡片的具体名称,只显示获得的各个稀有度的卡片数目 32 | # 填写不希望被加载的卡片文件名,以逗号分隔。如['icon_unit_100161.png'], 表示不加载六星猫拳的头像 33 | BLACKLIST_CARD = ['icon_unit_100031.png'] 34 | # 献祭卡片时的获得不同稀有度卡片的概率,-1,0,1表示被献祭卡片的三种稀有度,后面长度为3的列表表示献祭获得卡片三种不同稀有度的概率,要求加和为1 35 | MIX_PROBABILITY = {str(list((-1, -1))): [0.8, 0.194, 0.006], str(list((-1, 0))): [0.44, 0.5, 0.06], str(list((-1, 1))): [0.55, 0.3, 0.1], 36 | str(list((0, 0))): [0.1, 0.8, 0.1], str(list((0, 1))): [0.3, 0.5, 0.2], str(list((1, 1))): [0.15, 0.25, 0.6]} 37 | # 一键合成概率 38 | OK_MIX_PROBABILITY = {str(list((-1, -1))): [0.846, 0.15, 0.004], str(list((-1, 0))): [0.56, 0.4, 0.04], str(list((-1, 1))): [0.68, 0.24, 0.08], 39 | str(list((0, 0))): [0.33, 0.6, 0.07], str(list((0, 1))): [0.44, 0.4, 0.16], str(list((1, 1))): [0.2, 0.3, 0.5]} 40 | 41 | PRELOAD = True # 是否启动时直接将所有图片加载到内存中以提高查看仓库的速度(增加约几M内存消耗) 42 | 43 | sv = Service('poke-man-pcr', bundle='pcr娱乐', help_=''' 44 | 戳一戳机器人, 她可能会送你公主连结卡片哦~ 45 | 查看仓库 [@某人](这是可选参数): 查看某人的卡片仓库和收集度排名,不加参数默认查看自己的仓库 46 | 合成 [卡片1昵称] [卡片2昵称]: 献祭两张卡片以获得一张新的卡片 47 | 一键合成 [稀有度1] [稀有度2] [合成轮数](这是可选参数,不填则合成尽可能多的轮数): 一键进行若干轮"稀有度1"和"稀有度2"的卡片合成。注意: 使用一键合成指令获得稀有或超稀有卡的几率略低于使用合成指令 48 | 赠送 [@某人] [赠送的卡片名]: 将自己的卡片赠予别人 49 | 交换 [卡片1昵称] [@某人] [卡片2昵称]: 向某人发起卡片交换请求,用自己的卡片1交换他的卡片2 50 | 确认交换: 收到换卡请求后一定时间内输入这个指令可完成换卡 51 | '''.strip()) 52 | poke_tip_cd_limiter = FreqLimiter(TIP_CD_LIMIT) 53 | daily_tip_limiter = DailyAmountLimiter("tip",POKE_TIP_LIMIT, RESET_HOUR) 54 | daily_limiter = DailyAmountLimiter("poke",POKE_DAILY_LIMIT, RESET_HOUR) 55 | daily_give_limiter = DailyAmountLimiter("give",GIVE_DAILY_LIMIT, RESET_HOUR) 56 | cooling_time_limiter = FreqLimiter(POKE_COOLING_TIME) 57 | exchange_request_master = ExchangeRequestMaster(REQUEST_VALID_TIME) 58 | db = CardRecordDAO(DB_PATH) 59 | font = ImageFont.truetype('arial.ttf', 16) 60 | card_ids = [] 61 | card_file_names_all = [] 62 | star2rarity = {'1': -1, '3': 0, '6': 1} # 角色头像星级->卡片稀有度 63 | rarity_desc2rarity = {'普通': -1, '稀有': 0, '超稀有': 1} # 稀有度文字描述->卡片稀有度 64 | cards = {'1': [], '3': [], '6': []} # 1,3,6表示不同星级的角色头像 65 | chara_ids = {'1': [], '3': [], '6': []} 66 | 67 | # 资源预检 68 | image_cache = {} 69 | image_list = os.listdir(DIR_PATH) 70 | for image in image_list: 71 | if not image.startswith('icon_unit_') or image in BLACKLIST_CARD: 72 | continue 73 | # 图像缓存 74 | if PRELOAD: 75 | image_path = os.path.join(DIR_PATH, image) 76 | img = Image.open(image_path) 77 | image_cache[image] = img.convert('RGBA') if img.mode != 'RGBA' else img 78 | chara_id = int(image[10:14]) 79 | if chara_id == 1000 or chara_id not in _pcr_data.CHARA_NAME: 80 | continue 81 | star = image[14] 82 | if star not in star2rarity or image[15] != '1': 83 | continue 84 | cards[star].append(image) 85 | chara_ids[star].append(chara_id) 86 | card_ids.append(30000 + star2rarity[star] * 1000 + chara_id) 87 | card_file_names_all.append(image) 88 | # 边框缓存 89 | frame_names = ['superrare.png', 'rare.png', 'normal.png'] 90 | frames = {} 91 | frames_aplha = {} 92 | for frame_name in frame_names: 93 | frame = Image.open(FRAME_DIR_PATH + f'/{frame_name}') 94 | frame = frame.resize((80, 80), Image.ANTIALIAS) 95 | r, g, b, a = frame.split() 96 | frames[frame_name] = frame 97 | frames_aplha[frame_name] = a 98 | 99 | 100 | def get_pic(pic_path, card_num, rarity): 101 | if PRELOAD: 102 | # 拆分路径和文件名 103 | pic_name = os.path.split(pic_path)[1] 104 | img = image_cache[pic_name] 105 | else: 106 | img = Image.open(pic_path) 107 | img = img.resize((80, 80), Image.ANTIALIAS) 108 | return draw_num_text(add_rarity_frame(img, rarity), card_num, True, (0, 0, 0), 0, 0) 109 | 110 | 111 | def get_grey_pic(pic_path, rarity): 112 | if PRELOAD: 113 | # 拆分路径和文件名 114 | pic_name = os.path.split(pic_path)[1] 115 | img = image_cache[pic_name] 116 | else: 117 | img = Image.open(pic_path) 118 | img = img.resize((80, 80), Image.ANTIALIAS) 119 | img = add_rarity_frame(img, rarity) 120 | img = img.convert('L') 121 | return img 122 | 123 | 124 | def add_rarity_frame(img, rarity): 125 | if rarity == 1: 126 | frame_file_name = frame_names[0] 127 | elif rarity == 0: 128 | frame_file_name = frame_names[1] 129 | else: 130 | frame_file_name = frame_names[2] 131 | img.paste(frames[frame_file_name], (0, 0), 132 | mask=frames_aplha[frame_file_name]) 133 | return img 134 | 135 | 136 | def add_card_amount(img, card_amount): 137 | quantity_base = Image.open(FRAME_DIR_PATH + '/quantity.png') 138 | img.paste(quantity_base, (53, 54), mask=quantity_base.split()[3]) 139 | return draw_num_text(img, card_amount, False, (255, 255, 255), 2, 1) 140 | 141 | 142 | def add_icon(base, icon_name, x, y): 143 | icon = Image.open(FRAME_DIR_PATH + f'/{icon_name}') 144 | base.paste(icon, (x, y), mask=icon.split()[3]) 145 | return base 146 | 147 | 148 | def draw_num_text(img, num, draw_base_color, color, offset_x, offset_y): 149 | draw = ImageDraw.Draw(img) 150 | n = num if num < 100 else num 151 | text = f'×{n}' 152 | if len(text) == 2: 153 | offset_r = 0 154 | offset_t = 0 155 | else: 156 | offset_r = 10 157 | offset_t = 9 158 | if draw_base_color: 159 | draw.rectangle((59 - offset_r, 60, 75, 77), fill=(255, 255, 255)) 160 | draw.rectangle((59 - offset_r, 60, 77, 75), fill=(255, 255, 255)) 161 | draw.text((60-offset_t+offset_x, 60+offset_y), text, fill=color, font=font) 162 | return img 163 | 164 | 165 | def get_random_cards_list(super_rare_prob, rare_prob): 166 | r = random.random() 167 | if r < super_rare_prob: 168 | cards_list = cards['6'] 169 | elif r < super_rare_prob + rare_prob: 170 | cards_list = cards['3'] 171 | else: 172 | cards_list = cards['1'] 173 | return cards_list 174 | 175 | 176 | def get_random_cards(origin_cards, row_num, col_num, amount, bonus, get_random_cards_func=get_random_cards_list, *args): 177 | size = 80 178 | margin = 7 179 | margin_offset_x = 6 180 | margin_offset_y = 6 181 | cards_amount = [] 182 | extra_bonus = False 183 | for i in range(amount): 184 | a = roll_extra_bonus() if bonus else 1 185 | cards_amount.append(a) 186 | if a != 1: 187 | extra_bonus = True 188 | offset_y = 18 if extra_bonus else 0 189 | offset_critical_strike = 7 if extra_bonus else 0 190 | size_x, size_y = (col_num * size + (col_num+1) * margin + 2 * margin_offset_x, offset_y + 191 | row_num * size + (row_num+1) * margin + 2 * margin_offset_y + offset_critical_strike) 192 | base = Image.new('RGBA', (size_x, size_y), (255, 255, 255, 255)) 193 | frame = Image.open(FRAME_DIR_PATH + '/background.png') 194 | frame = frame.resize((size_x, size_y - offset_y), Image.ANTIALIAS) 195 | base.paste(frame, (0, offset_y), mask=frame.split()[3]) 196 | if extra_bonus: 197 | base = add_icon(base, 'pokecriticalstrike.png', 198 | int(size_x/2) - 71, int(offset_y/2) - 2) 199 | card_counter = {} 200 | rarity_counter = {1: [0, 0], 0: [0, 0], -1: [0, 0]} 201 | card_descs = [] 202 | rarity_desc = {1: '超稀有', 0: '稀有', -1: '普通'} 203 | for i in range(amount): 204 | random_card = random.choice(get_random_cards_func(*args)) 205 | card_id, rarity = get_card_id_by_file_name(random_card) 206 | new_string = ' 【NEW】' if card_id not in origin_cards and card_id not in card_counter else '' 207 | card_amount = cards_amount[i] 208 | card_counter[card_id] = card_amount if card_id not in card_counter else card_counter[card_id] + card_amount 209 | card_desc = f'{rarity_desc[rarity]}「{get_chara_name(card_id)[1]}」×{card_amount}{new_string}' 210 | card_descs.append(card_desc) 211 | rarity_counter[rarity][0] += 1 212 | rarity_counter[rarity][1] += 1 if new_string else 0 213 | if PRELOAD: 214 | img = image_cache[random_card] 215 | else: 216 | img = Image.open(DIR_PATH + f'/{random_card}') 217 | img = img.convert('RGBA') if img.mode != 'RGBA' else img 218 | row_index = i // col_num 219 | col_index = i % col_num 220 | img = img.resize((size, size), Image.ANTIALIAS) 221 | img = add_rarity_frame(img, rarity) 222 | if card_amount > 1: 223 | img = add_card_amount(img, card_amount) 224 | coor_x, coor_y = (margin + margin_offset_x + col_index * (size + margin), margin + 225 | margin_offset_y + offset_y + offset_critical_strike + row_index * (size + margin)) 226 | base.paste(img, (coor_x, coor_y), mask=img.split()[3]) 227 | if card_id not in origin_cards: 228 | base = add_icon(base, 'new.png', coor_x + size - 27, coor_y - 5) 229 | # 当获得的卡片数过多时,只显示各稀有度获得的卡片数量 230 | if amount > OMIT_THRESHOLD: 231 | card_descs = [] 232 | rarity_desc = {1: '超稀有', 0: '稀有卡', -1: '普通卡'} 233 | for rarity in rarity_counter: 234 | if rarity_counter[rarity][0] > 0: 235 | msg_part = f' (【NEW】x{rarity_counter[rarity][1]})' if rarity_counter[rarity][1] else '' 236 | card_descs.append( 237 | f'【{rarity_desc[rarity]}】x{rarity_counter[rarity][0]}{msg_part}') 238 | return card_counter, card_descs, MessageSegment.image(util.pic2b64(base)) 239 | 240 | 241 | # 输入'[稀有度前缀][角色昵称]'格式的卡片名, 例如'黑猫','稀有黑猫','超稀有黑猫', 输出角色昵称标准化后的结果如'「凯露」','稀有「凯露」','超稀有「凯露」' 242 | def get_card_name_with_rarity(card_name): 243 | if card_name.startswith('超稀有'): 244 | chara_suffix = card_name[0:3] 245 | chara_nickname = card_name[3:] 246 | elif card_name.startswith('稀有'): 247 | chara_suffix = card_name[0:2] 248 | chara_nickname = card_name[2:] 249 | else: 250 | chara_suffix = '普通' 251 | chara_nickname = card_name[2:] if card_name.startswith( 252 | '普通') else card_name 253 | chara_name = chara.fromname(chara_nickname).name 254 | return f'{chara_suffix}「{chara_name}」' 255 | 256 | 257 | # 由卡片id(形如3xxxx)提取稀有度前缀和角色名 258 | def get_chara_name(card_id): 259 | chara_id = card_id % 10000 260 | if 3000 > chara_id > 2000: 261 | chara_id -= 1000 262 | rarity_desc = '【超稀有】的' 263 | elif 2000 > chara_id > 1000 or chara_id > 3000: 264 | rarity_desc = '【稀有】的' 265 | else: 266 | chara_id += 1000 267 | rarity_desc = '【普通】的' 268 | return rarity_desc, chara.fromid(chara_id).name 269 | 270 | 271 | # 由'[稀有度前缀][角色昵称]'格式的卡片名, 返回卡片id(形如3xxxx),如果卡片不存在则返回0 272 | def get_card_id_by_card_name(card_name): 273 | if card_name.startswith('超稀有'): 274 | rarity = 1 275 | star = '6' 276 | chara_name_no_prefix = card_name[3:] 277 | elif card_name.startswith('稀有'): 278 | rarity = 0 279 | star = '3' 280 | chara_name_no_prefix = card_name[2:] 281 | else: 282 | rarity = -1 283 | star = '1' 284 | chara_name_no_prefix = card_name[2:] if card_name.startswith( 285 | '普通') else card_name 286 | chara_id = chara.name2id(chara_name_no_prefix) 287 | return (30000 + rarity * 1000 + chara_id) if chara_id != chara.UNKNOWN and chara_id in chara_ids[star] else 0 288 | 289 | 290 | # 单次戳机器人获得的卡片数量 291 | def roll_cards_amount(): 292 | roll = random.random() 293 | if roll <= 0.01: 294 | CARDS_EVERY_POKE = 10 # 大暴击! 295 | elif 0.01 < roll <= 0.1: 296 | CARDS_EVERY_POKE = 5 297 | elif 0.1 < roll <= 0.3: 298 | CARDS_EVERY_POKE = 4 299 | elif 0.3 < roll <= 0.7: 300 | CARDS_EVERY_POKE = 3 301 | elif 0.7 < roll <= 0.9: 302 | CARDS_EVERY_POKE = 2 303 | else: 304 | CARDS_EVERY_POKE = 1 305 | return CARDS_EVERY_POKE 306 | 307 | 308 | def roll_extra_bonus(): 309 | roll = random.random() 310 | if roll < 0.01: 311 | amount = 3 312 | elif roll < 0.1: 313 | amount = 2 314 | else: 315 | amount = 1 316 | return amount 317 | 318 | 319 | def get_card_id_by_file_name(image_file_name): 320 | chara_id = int(image_file_name[10:14]) 321 | rarity = star2rarity[image_file_name[14]] 322 | return 30000 + rarity * 1000 + chara_id, rarity 323 | 324 | 325 | def get_card_rarity(card_id): 326 | if 33000 > card_id > 32000: 327 | return 1 328 | elif card_id < 31000: 329 | return -1 330 | else: 331 | return 0 332 | 333 | 334 | def normalize_digit_format(n): 335 | return f'0{n}' if n < 10 else f'{n}' 336 | 337 | 338 | @sv.on_notice('notify.poke') 339 | async def poke_back(session: NoticeSession): 340 | uid = session.ctx['user_id'] 341 | at_user = MessageSegment.at(session.ctx['user_id']) 342 | guid = session.ctx['group_id'], session.ctx['user_id'] 343 | if not cooling_time_limiter.check(uid): 344 | return 345 | cooling_time_limiter.start_cd(uid) 346 | if session.ctx['target_id'] != session.event.self_id: 347 | return 348 | if not daily_limiter.check(guid) and not daily_tip_limiter.check(guid): 349 | poke_tip_cd_limiter.start_cd(guid) 350 | if not daily_limiter.check(guid) and poke_tip_cd_limiter.check(guid): 351 | daily_tip_limiter.increase(guid) 352 | await session.send(f'{at_user}你今天戳得已经够多的啦,再戳也不会有奇怪的东西掉下来的~') 353 | return 354 | daily_tip_limiter.reset(guid) 355 | if not daily_limiter.check(guid) or random.random() > POKE_GET_CARDS: 356 | poke = MessageSegment(type_='poke', 357 | data={ 358 | 'qq': str(session.ctx['user_id']), 359 | }) 360 | await session.send(poke) 361 | else: 362 | amount = roll_cards_amount() 363 | col_num = math.ceil(amount / 2) 364 | row_num = 2 if amount != 1 else 1 365 | card_counter, card_descs, card = get_random_cards(db.get_cards_num(session.ctx['group_id'], session.ctx['user_id']), row_num, col_num, 366 | amount, True, get_random_cards_list, SUPER_RARE_PROBABILITY, RARE_PROBABILITY) 367 | dash = '----------------------------------------' 368 | msg_part = '\n'.join(card_descs) 369 | await session.send(f'别戳了别戳了o(╥﹏╥)o{card}{at_user}这些卡送给你了, 让我安静会...\n{dash}\n获得了:\n{msg_part}') 370 | for card_id in card_counter.keys(): 371 | db.add_card_num( 372 | session.ctx['group_id'], session.ctx['user_id'], card_id, card_counter[card_id]) 373 | daily_limiter.increase(guid) 374 | 375 | 376 | @sv.on_prefix(('献祭', '合成', '融合')) 377 | async def mix_card(bot, ev: CQEvent): 378 | # 参数识别 379 | s = ev.message.extract_plain_text() 380 | args = s.split(' ') 381 | if len(args) != 2: 382 | await bot.finish(ev, '请输入想要合成的两张卡, 以空格分隔') 383 | card1_id = get_card_id_by_card_name(args[0]) 384 | card2_id = get_card_id_by_card_name(args[1]) 385 | if not card1_id: 386 | await bot.finish(ev, f'错误: 无法识别{args[0]}, 若为稀有或超稀有卡请在名称前加上"稀有"或"超稀有"') 387 | if not card2_id: 388 | await bot.finish(ev, f'错误: 无法识别{args[1]}, 若为稀有或超稀有卡请在名称前加上"稀有"或"超稀有"') 389 | card1_num = db.get_card_num(ev.group_id, ev.user_id, card1_id) 390 | card2_num = db.get_card_num(ev.group_id, ev.user_id, card2_id) 391 | if card1_id == card2_id: 392 | if card1_num < 2: 393 | await bot.finish(ev, f'{get_card_name_with_rarity(args[0])}卡数量不足, 无法合成') 394 | else: 395 | if card1_num == 0: 396 | await bot.finish(ev, f'{get_card_name_with_rarity(args[0])}卡数量不足, 无法合成') 397 | if card2_num == 0: 398 | await bot.finish(ev, f'{get_card_name_with_rarity(args[1])}卡数量不足, 无法合成') 399 | # 开始献祭 400 | [normal_prob, rare_prob, super_rare_prob] = MIX_PROBABILITY[str( 401 | sorted(list((get_card_rarity(card1_id), get_card_rarity(card2_id)))))] 402 | card_counter, card_descs, card = get_random_cards(db.get_cards_num( 403 | ev.group_id, ev.user_id), 1, 1, 1, False, get_random_cards_list, super_rare_prob, rare_prob) 404 | card_id = list(card_counter.keys())[0] 405 | rarity_desc, chara_name = get_chara_name(card_id) 406 | db.add_card_num(ev.group_id, ev.user_id, card1_id, -1) 407 | db.add_card_num(ev.group_id, ev.user_id, card2_id, -1) 408 | db.add_card_num(ev.group_id, ev.user_id, card_id) 409 | await bot.send(ev, f'将两张卡片进行了融合……然后{card}获得了{rarity_desc}「{chara_name}」×1~', at_sender=True) 410 | 411 | 412 | @sv.on_prefix(('一键献祭', '一键合成', '一键融合', '全部献祭', '全部合成', '全部融合')) 413 | async def auto_mix_card(bot, ev: CQEvent): 414 | # 参数识别 415 | s = ev.message.extract_plain_text() 416 | args = s.split(' ') 417 | if len(args) == 2 and args[0] in rarity_desc2rarity and args[1] in rarity_desc2rarity: 418 | pass 419 | elif len(args) == 3 and args[0] in rarity_desc2rarity and args[1] in rarity_desc2rarity and args[2].isdigit() and int(args[2]) > 0: 420 | pass 421 | else: 422 | await bot.finish(ev, '参数格式错误, 请按正确格式输入指令参数') 423 | # 自动消耗多余的卡 424 | surplus_cards = db.get_surplus_cards(ev.group_id, ev.user_id) 425 | surplus_cards = {card_id: card_amount for card_id, 426 | card_amount in surplus_cards.items() if card_id in card_ids} 427 | if args[0] == args[1]: 428 | rarity = rarity_desc2rarity[args[0]] 429 | rarity1, rarity2 = rarity, rarity 430 | available_cards = {card_id: card_amount for card_id, card_amount in surplus_cards.items( 431 | ) if get_card_rarity(card_id) == rarity} 432 | available_card_amount = sum(available_cards.values()) 433 | if len(args) == 3 and int(args[2])*2 > available_card_amount: 434 | await bot.finish(ev, f'合成失败, 多余的【{args[0]}】卡数量不足{args[2]*2}, 无法一键合成{args[2]}次.') 435 | if len(args) == 2 and available_card_amount < 2: 436 | await bot.finish(ev, f'合成失败, 多余的【{args[0]}】卡数量不足2, 无法一键合成') 437 | mix_rounds = int(args[2]) if len( 438 | args) == 3 else math.floor(available_card_amount/2) 439 | mixed_cards_amount = 0 440 | for card_id in available_cards: 441 | card_amount = available_cards[card_id] 442 | if mixed_cards_amount + card_amount <= 2 * mix_rounds: 443 | db.add_card_num(ev.group_id, ev.user_id, card_id, -card_amount) 444 | mixed_cards_amount += card_amount 445 | else: 446 | db.add_card_num(ev.group_id, ev.user_id, 447 | card_id, -(2*mix_rounds - mixed_cards_amount)) 448 | break 449 | else: 450 | rarity1 = rarity_desc2rarity[args[0]] 451 | rarity2 = rarity_desc2rarity[args[1]] 452 | available_cards1 = {card_id: card_amount for card_id, card_amount in surplus_cards.items( 453 | ) if get_card_rarity(card_id) == rarity1} 454 | available_cards2 = {card_id: card_amount for card_id, card_amount in surplus_cards.items( 455 | ) if get_card_rarity(card_id) == rarity2} 456 | available_card_amount1 = sum(available_cards1.values()) 457 | available_card_amount2 = sum(available_cards2.values()) 458 | if len(args) == 3: 459 | if int(args[2]) > available_card_amount1: 460 | await bot.finish(ev, f'合成失败, 多余的【{args[0]}】卡数量不足{args[2]}, 无法一键合成{args[2]}次.') 461 | if int(args[2]) > available_card_amount2: 462 | await bot.finish(ev, f'合成失败, 多余的【{args[1]}】卡数量不足{args[2]}, 无法一键合成{args[2]}次.') 463 | if len(args) == 2: 464 | if available_card_amount1 < 1: 465 | await bot.finish(ev, f'合成失败, 多余的【{args[0]}】卡数量不足1, 无法一键合成') 466 | if available_card_amount2 < 1: 467 | await bot.finish(ev, f'合成失败, 多余的【{args[1]}】卡数量不足1, 无法一键合成') 468 | mix_rounds = int(args[2]) if len(args) == 3 else min( 469 | available_card_amount1, available_card_amount2) 470 | for available_cards in [available_cards1, available_cards2]: 471 | mixed_cards_amount = 0 472 | for card_id in available_cards: 473 | card_amount = available_cards[card_id] 474 | if mixed_cards_amount + card_amount <= 2 * mix_rounds: 475 | db.add_card_num(ev.group_id, ev.user_id, 476 | card_id, -card_amount) 477 | mixed_cards_amount += card_amount 478 | else: 479 | db.add_card_num(ev.group_id, ev.user_id, 480 | card_id, -(2*mix_rounds - mixed_cards_amount)) 481 | break 482 | # 获得自动合成的卡 483 | [normal_prob, rare_prob, super_rare_prob] = OK_MIX_PROBABILITY[str( 484 | sorted(list((rarity1, rarity2))))] 485 | col_num = math.ceil(math.sqrt(mix_rounds)) 486 | row_num = math.ceil(mix_rounds / col_num) 487 | card_counter, card_descs, card = get_random_cards(db.get_cards_num( 488 | ev.group_id, ev.user_id), row_num, col_num, mix_rounds, False, get_random_cards_list, super_rare_prob, rare_prob) 489 | msg_part = '\n'.join(card_descs) 490 | await bot.send(ev, f'进行了{mix_rounds}轮融合……然后{card}获得了:\n{msg_part}', at_sender=True) 491 | for card_id in card_counter.keys(): 492 | db.add_card_num(ev.group_id, ev.user_id, 493 | card_id, card_counter[card_id]) 494 | 495 | 496 | @sv.on_prefix(('交换', '交易', '互换')) 497 | async def exchange_cards(bot, ev: CQEvent): 498 | # 参数识别 499 | if len(ev.message) != 3: 500 | await bot.finish(ev, '参数格式错误, 请重试') 501 | if ev.message[0].type != 'text' or ev.message[1].type != 'at' or ev.message[2].type != 'text': 502 | await bot.finish(ev, '参数格式错误, 请重试') 503 | target_uid = int(ev.message[1].data['qq']) 504 | card1_name = ev.message[0].data['text'].strip() 505 | card2_name = ev.message[2].data['text'].strip() 506 | card1_id = get_card_id_by_card_name(card1_name) 507 | card2_id = get_card_id_by_card_name(card2_name) 508 | if not card1_id: 509 | await bot.finish(ev, f'错误: 无法识别{get_card_name_with_rarity(card1_name)}, 若为稀有或超稀有卡请在名称前加上"稀有"或"超稀有"') 510 | if not card2_id: 511 | await bot.finish(ev, f'错误: 无法识别{get_card_name_with_rarity(card2_name)}, 若为稀有或超稀有卡请在名称前加上"稀有"或"超稀有"') 512 | card1_num = db.get_card_num(ev.group_id, ev.user_id, card1_id) 513 | card2_num = db.get_card_num(ev.group_id, target_uid, card2_id) 514 | if card1_num == 0: 515 | await bot.finish(ev, f'{MessageSegment.at(ev.user_id)}的{get_card_name_with_rarity(card1_name)}卡数量不足, 无法交换') 516 | if card2_num == 0: 517 | await bot.finish(ev, f'{MessageSegment.at(target_uid)}的{get_card_name_with_rarity(card2_name)}卡数量不足, 无法交换') 518 | # 发起交换请求 519 | if exchange_request_master.has_exchange_request_to_confirm(ev.group_id, target_uid): 520 | await bot.finish(ev, '您发起交易的对象目前正与他人交易中, 请稍等~', at_sender=True) 521 | exchange_request_master.add_exchange_request(ev.group_id, target_uid, ExchangeRequest( 522 | ev.user_id, card1_id, card1_name, target_uid, card2_id, card2_name)) 523 | await bot.send(ev, f'{MessageSegment.at(target_uid)}\n叮~{MessageSegment.at(ev.user_id)}希望用他的{get_card_name_with_rarity(card1_name)}卡交换你的{get_card_name_with_rarity(card2_name)}卡,输入"确认交换"可完成交换({REQUEST_VALID_TIME}s后交换请求失效)') 524 | 525 | 526 | @sv.on_fullmatch(('确认交换', '同意交换')) 527 | async def confirm_exchange(bot, ev: CQEvent): 528 | if not exchange_request_master.has_exchange_request_to_confirm(ev.group_id, ev.user_id): 529 | await bot.finish(ev, '您还没有收到换卡请求~', at_sender=True) 530 | exchange_request = exchange_request_master.get_exchange_request( 531 | ev.group_id, ev.user_id) 532 | exchange_request_master.delete_exchange_request(ev.group_id, ev.user_id) 533 | card1_num = db.get_card_num( 534 | ev.group_id, exchange_request.sender_uid, exchange_request.card1_id) 535 | card2_num = db.get_card_num( 536 | ev.group_id, exchange_request.target_uid, exchange_request.card2_id) 537 | if card1_num == 0: 538 | await bot.finish(ev, f'{MessageSegment.at(exchange_request.sender_uid)}的{get_card_name_with_rarity(exchange_request.card1_name)}卡数量不足, 无法交换') 539 | if card2_num == 0: 540 | await bot.finish(ev, f'{MessageSegment.at(exchange_request.target_uid)}的{get_card_name_with_rarity(exchange_request.card2_name)}卡数量不足, 无法交换') 541 | db.add_card_num(ev.group_id, exchange_request.sender_uid, 542 | exchange_request.card1_id, -1) 543 | db.add_card_num(ev.group_id, exchange_request.target_uid, 544 | exchange_request.card2_id, -1) 545 | db.add_card_num(ev.group_id, exchange_request.sender_uid, 546 | exchange_request.card2_id) 547 | db.add_card_num(ev.group_id, exchange_request.target_uid, 548 | exchange_request.card1_id) 549 | await bot.send(ev, '交换成功!') 550 | 551 | 552 | @sv.on_prefix(('赠送', '白给', '白送')) 553 | async def give(bot, ev: CQEvent): 554 | if len(ev.message) != 2 or ev.message[0].type != 'at' or ev.message[1].type != 'text': 555 | await bot.finish(ev, '参数格式错误, 请重试') 556 | target_uid = int(ev.message[0].data['qq']) 557 | if not daily_give_limiter.check((ev.group_id, target_uid)): 558 | await bot.finish(ev, f'{MessageSegment.at(target_uid)}的今日接受赠送次数已达上限,明天再送给TA吧~') 559 | if target_uid == ev.user_id: 560 | await bot.finish(ev, '不用给自己赠卡~') 561 | card_name = ev.message[1].data['text'].strip() 562 | card_id = get_card_id_by_card_name(card_name) 563 | if not card_id: 564 | await bot.finish(ev, f'错误: 无法识别{get_card_name_with_rarity(card_name)}, 若为稀有或超稀有卡请在名称前加上"稀有"或"超稀有"') 565 | card_num = db.get_card_num(ev.group_id, ev.user_id, card_id) 566 | if card_num < 1: 567 | await bot.finish(ev, f'{get_card_name_with_rarity(card_name)}卡数量不足, 无法赠送') 568 | db.add_card_num(ev.group_id, ev.user_id, card_id, -1) 569 | db.add_card_num(ev.group_id, target_uid, card_id) 570 | daily_give_limiter.increase((ev.group_id, target_uid)) 571 | await bot.send(ev, f'{MessageSegment.at(ev.user_id)}将{get_card_name_with_rarity(card_name)}赠送给了{MessageSegment.at(target_uid)}') 572 | 573 | 574 | @sv.on_prefix('查看仓库') 575 | async def storage(bot, ev: CQEvent): 576 | if len(ev.message) == 1 and ev.message[0].type == 'text' and not ev.message[0].data['text']: 577 | uid = ev.user_id 578 | elif ev.message[0].type == 'at': 579 | uid = int(ev.message[0].data['qq']) 580 | else: 581 | await bot.finish(ev, '参数格式错误, 请重试') 582 | row_nums = {} 583 | for star in cards.keys(): 584 | row_nums[star] = math.ceil(len(cards[star]) / COL_NUM) 585 | row_num = sum(row_nums.values()) 586 | base = Image.open(FRAME_DIR_PATH + '/frame.png') 587 | base = base.resize((40+COL_NUM*80+(COL_NUM-1)*10, 120 + 588 | row_num*80+(row_num-1)*10), Image.ANTIALIAS) 589 | cards_num = db.get_cards_num(ev.group_id, uid) 590 | cards_num = {card_id: card_amount for card_id, 591 | card_amount in cards_num.items() if card_id in card_ids} 592 | row_index_offset = 0 593 | row_offset = 0 594 | for star in cards.keys(): 595 | cards_list = cards[star] 596 | for index, id in enumerate(cards_list): 597 | row_index = index // COL_NUM + row_index_offset 598 | col_index = index % COL_NUM 599 | card_id, rarity = get_card_id_by_file_name(cards_list[index]) 600 | pic_path = DIR_PATH + f'/{cards_list[index]}' 601 | f = get_pic(pic_path, cards_num[card_id], rarity) if card_id in cards_num else get_grey_pic( 602 | pic_path, rarity) 603 | base.paste(f, (30 + col_index * 80 + (col_index - 1) * 10, 604 | row_offset + 40 + row_index * 80 + (row_index - 1) * 10)) 605 | row_index_offset += row_nums[star] 606 | row_offset += 30 607 | ranking = db.get_group_ranking(ev.group_id, uid) 608 | ranking_desc = f'第{ranking}位' if ranking != -1 else '未上榜' 609 | total_card_num = sum(cards_num.values()) 610 | super_rare_card_num = len( 611 | [card_id for card_id in cards_num if get_card_rarity(card_id) == 1]) 612 | super_rare_card_total = len(cards['6']) 613 | rare_card_num = len( 614 | [card_id for card_id in cards_num if get_card_rarity(card_id) == 0]) 615 | rare_card_total = len(cards['3']) 616 | normal_card_num = len(cards_num) - super_rare_card_num - rare_card_num 617 | normal_card_total = len(cards['1']) 618 | buf = BytesIO() 619 | base = base.convert('RGB') 620 | base.save(buf, format='JPEG') 621 | base64_str = f'base64://{base64.b64encode(buf.getvalue()).decode()}' 622 | await bot.send(ev, f'{MessageSegment.at(uid)}的仓库:[CQ:image,file={base64_str}]\n持有卡片数: {total_card_num}\n普通卡收集: {normalize_digit_format(normal_card_num)}/{normalize_digit_format(normal_card_total)}\n稀有卡收集: {normalize_digit_format(rare_card_num)}/{normalize_digit_format(rare_card_total)}\n超稀有收集: {normalize_digit_format(super_rare_card_num)}/{normalize_digit_format(super_rare_card_total)}\n图鉴完成度: {normalize_digit_format(len(cards_num))}/{normalize_digit_format(len(card_file_names_all))}\n当前群排名: {ranking_desc}') 623 | 624 | 625 | # 当增加新角色后不重启hoshino刷新现有缓存 626 | @sv.on_fullmatch('刷新卡片') 627 | async def refresh_unit_cache(bot, event: CQEvent): 628 | cards['1'] = [] 629 | cards['3'] = [] 630 | cards['6'] = [] 631 | 632 | chara_ids['1'] = [] 633 | chara_ids['3'] = [] 634 | chara_ids['6'] = [] 635 | 636 | card_ids.clear() 637 | card_file_names_all.clear() 638 | image_list = os.listdir(DIR_PATH) 639 | for image in image_list: 640 | if not image.startswith('icon_unit_') or image in BLACKLIST_CARD: 641 | continue 642 | # 图像缓存 643 | if PRELOAD: 644 | image_path = os.path.join(DIR_PATH, image) 645 | img = Image.open(image_path) 646 | image_cache[image] = img.convert( 647 | 'RGBA') if img.mode != 'RGBA' else img 648 | chara_id = int(image[10:14]) 649 | if chara_id == 1000 or chara_id not in _pcr_data.CHARA_NAME: 650 | continue 651 | star = image[14] 652 | if star not in star2rarity or image[15] != '1': 653 | continue 654 | cards[star].append(image) 655 | chara_ids[star].append(chara_id) 656 | card_ids.append(30000 + star2rarity[star] * 1000 + chara_id) 657 | card_file_names_all.append(image) 658 | # 边框缓存 659 | for frame_name in frame_names: 660 | frame = Image.open(FRAME_DIR_PATH + f'/{frame_name}') 661 | frame = frame.resize((80, 80), Image.ANTIALIAS) 662 | r, g, b, a = frame.split() 663 | frames[frame_name] = frame 664 | frames_aplha[frame_name] = a 665 | await bot.send(event, '我好了') 666 | --------------------------------------------------------------------------------