├── .eslintignore ├── .eslintrc.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs └── .vuepress │ └── config.js ├── lib ├── comp │ ├── Character.vue │ ├── Result.vue │ ├── ResultChar.vue │ ├── Select.vue │ ├── Settings.vue │ └── index.vue ├── data │ ├── characters.yaml │ ├── games.yaml │ ├── index.js │ ├── rank_cn7.yaml │ └── tags.yaml ├── index.js ├── pages │ └── about.md ├── styles │ └── index.styl └── utils │ ├── BackupTree.ts │ ├── SortNode.ts │ ├── index.ts │ ├── settings.ts │ └── sort.mixin.ts ├── package.json └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | !.vuepress 2 | dist -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | extends: 4 | - plugin:vue/essential 5 | - standard 6 | 7 | rules: 8 | comma-dangle: 9 | - error 10 | - always-multiline 11 | indent: 12 | - error 13 | - 2 14 | - SwitchCase: 1 15 | MemberExpression: off 16 | no-undef: error 17 | no-unused-vars: off 18 | no-return-assign: off 19 | operator-linebreak: 20 | - error 21 | - before 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | yarn-error.log 4 | node_modules 5 | package-lock.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | git: 4 | depth: false 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | node_js: 11 | - node 12 | 13 | before_deploy: 14 | - yarn build 15 | 16 | deploy: 17 | provider: pages 18 | github-token: $GITHUB_TOKEN 19 | local-dir: docs/.vuepress/dist 20 | skip-cleanup: true 21 | on: 22 | branch: master 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Shigma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 东方本命角色测试 2 | 3 | [![Build Status](https://travis-ci.org/uzkk/favorite.svg?branch=master)](https://travis-ci.org/uzkk/favorite) 4 | 5 | - 主站链接:[https://vp.uzkk.net/favorite/](https://vp.uzkk.net/favorite/) 6 | - 分站链接:[https://uzkk.github.io/favorite/](https://uzkk.github.io/favorite/) 7 | 8 | > 注:分站的更新会比主站快一两个版本。 9 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = (context) => ({ 2 | base: '/favorite/', 3 | 4 | title: '二色幽紫蝶', 5 | 6 | description: '东方 Project - 从入坑到入坟', 7 | 8 | theme: 'uzkk', 9 | 10 | plugins: [ 11 | [require('@uzkk/not-found')], 12 | [require('@uzkk/shared-assets')], 13 | [require('../..'), { 14 | base: '/', 15 | }], 16 | ], 17 | 18 | themeConfig: { 19 | search: false, 20 | nav: [ 21 | { text: '主页', link: '/', exact: true }, 22 | { text: '关于', link: '/about.html', exact: false }, 23 | { text: 'GitHub', link: 'https://github.com/uzkk/favorite', exact: false }, 24 | ], 25 | }, 26 | 27 | evergreen: () => !context.isProd, 28 | }) 29 | -------------------------------------------------------------------------------- /lib/comp/Character.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 52 | 53 | 113 | -------------------------------------------------------------------------------- /lib/comp/Result.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 136 | 137 | 156 | -------------------------------------------------------------------------------- /lib/comp/ResultChar.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 19 | 20 | 61 | -------------------------------------------------------------------------------- /lib/comp/Select.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 127 | 128 | 161 | -------------------------------------------------------------------------------- /lib/comp/Settings.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 221 | -------------------------------------------------------------------------------- /lib/comp/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /lib/data/characters.yaml: -------------------------------------------------------------------------------- 1 | - - 1 2 | - 博丽灵梦 3 | - 乐园的巫女 4 | - abcdefghijADFGHIJ 5 | - rank_cn7: 1 6 | - [] 7 | - - 2 8 | - 雾雨魔理沙 9 | - 普通的魔法使 10 | - abcdefghijABDGHIJ 11 | - rank_cn7: 2 12 | - [] 13 | - - 3 14 | - 露米娅 15 | - 宵暗的妖怪 16 | - a 17 | - rank_cn7: 43 18 | - - loli 19 | - baka 20 | - - 4 21 | - 大妖精 22 | - 新月般的妖精 23 | - aB 24 | - rank_cn7: 53 25 | - - loli 26 | - yousei 27 | - baka 28 | - - 5 29 | - 琪露诺 30 | - 湖上的冰精 31 | - adikAB 32 | - rank_cn7: 17 33 | - - loli 34 | - yousei 35 | - baka 36 | - - 6 37 | - 红美铃 38 | - 华人小娘 39 | - aA 40 | - rank_cn7: 32 41 | - [] 42 | - - 7 43 | - 小恶魔 44 | - 图书馆的恶魔 45 | - a 46 | - rank_cn7: 64 47 | - - uniform 48 | - - 8 49 | - 帕秋莉·诺蕾姬 50 | - 不动的大图书馆 51 | - aA 52 | - rank_cn7: 20 53 | - [] 54 | - - 9 55 | - 十六夜咲夜 56 | - 完美潇洒的从者 57 | - abcdiA 58 | - rank_cn7: 7 59 | - - uniform 60 | - - 10 61 | - 蕾米莉亚·斯卡蕾特 62 | - 永远鲜红的幼月 63 | - acA 64 | - rank_cn7: 4 65 | - - loli 66 | - - 11 67 | - 芙兰朵露·斯卡蕾特 68 | - 恶魔之妹 69 | - a 70 | - rank_cn7: 12 71 | - - loli 72 | - - 12 73 | - 蕾蒂·霍瓦特洛克 74 | - 冬天的遗忘之物 75 | - b 76 | - rank_cn7: 98 77 | - [] 78 | - - 13 79 | - 橙 80 | - 凶兆的黑猫 81 | - b 82 | - rank_cn7: 73 83 | - - loli 84 | - beast 85 | - baka 86 | - - 14 87 | - 爱丽丝·玛格特洛依德 88 | - 七色的人偶师 89 | - bAJ 90 | - rank_cn7: 11 91 | - [] 92 | - - 15 93 | - 莉莉白 94 | - 告知春天的妖精 95 | - bdkB 96 | - rank_cn7: 61 97 | - - loli 98 | - yousei 99 | - - 16 100 | - 露娜萨·普莉兹姆利巴 101 | - 骚灵小提琴手 102 | - bd 103 | - rank_cn7: 101 104 | - - loli 105 | - - 17 106 | - 梅露兰·普莉兹姆利巴 107 | - 骚灵小号手 108 | - bd 109 | - rank_cn7: 118 110 | - - loli 111 | - - 18 112 | - 莉莉卡·普莉兹姆利巴 113 | - 骚灵键盘手 114 | - bd 115 | - rank_cn7: 124 116 | - - loli 117 | - - 19 118 | - 魂魄妖梦 119 | - 半分虚幻的庭师 120 | - bcdhA 121 | - rank_cn7: 14 122 | - [] 123 | - - 20 124 | - 西行寺幽幽子 125 | - 华胥的亡灵 126 | - bcA 127 | - rank_cn7: 8 128 | - - bba 129 | - - 21 130 | - 八云蓝 131 | - 策士之九尾 132 | - b 133 | - rank_cn7: 54 134 | - - bba 135 | - beast 136 | - - 22 137 | - 八云紫 138 | - 幻想的境界 139 | - bcA 140 | - rank_cn7: 6 141 | - - bba 142 | - - 23 143 | - 伊吹萃香 144 | - 小小的百鬼夜行 145 | - A 146 | - rank_cn7: 27 147 | - [] 148 | - - 24 149 | - 莉格露·奈特巴格 150 | - 暗中蠢动的光虫 151 | - c 152 | - rank_cn7: 102 153 | - - baka 154 | - - 25 155 | - 米斯蒂娅·萝蕾拉 156 | - 夜雀妖怪 157 | - cd 158 | - rank_cn7: 66 159 | - - loli 160 | - baka 161 | - - 26 162 | - 上白泽慧音 163 | - 知识与历史的半兽 164 | - c 165 | - rank_cn7: 42 166 | - [] 167 | - - 27 168 | - 因幡帝 169 | - 幸运的白兔 170 | - cd 171 | - rank_cn7: 67 172 | - - loli 173 | - beast 174 | - - 28 175 | - 铃仙·优昙华院·因幡 176 | - 狂气的月兔 177 | - cdjA 178 | - rank_cn7: 15 179 | - - beast 180 | - uniform 181 | - - 29 182 | - 八意永琳 183 | - 蓬莱的药贩 184 | - c 185 | - rank_cn7: 45 186 | - - bba 187 | - - 30 188 | - 蓬莱山辉夜 189 | - 永远的公主殿下 190 | - c 191 | - rank_cn7: 19 192 | - [] 193 | - - 31 194 | - 藤原妹红 195 | - 红色自警队 196 | - cA 197 | - rank_cn7: 9 198 | - [] 199 | - - 32 200 | - 铃仙 II 号 201 | - 月之玉兔 202 | - D 203 | - alias_cn: Reisen 204 | - - beast 205 | - uniform 206 | - - 33 207 | - 绵月依姬 208 | - 神灵凭附的月之公主 209 | - D 210 | - rank_cn7: 96 211 | - [] 212 | - - 34 213 | - 绵月丰姬 214 | - 连系海与山的月之公主 215 | - D 216 | - rank_cn7: 107 217 | - [] 218 | - - 35 219 | - 射命丸文 220 | - 传统的幻想书屋 221 | - dkAB 222 | - rank_cn7: 10 223 | - [] 224 | - - 36 225 | - 梅蒂欣·梅兰可莉 226 | - 小小的甜蜜毒药 227 | - d 228 | - rank_cn7: 87 229 | - - loli 230 | - - 37 231 | - 风见幽香 232 | - 四季的鲜花之主 233 | - dIJ 234 | - rank_cn7: 26 235 | - - bba 236 | - - 38 237 | - 小野塚小町 238 | - 三途河畔的摆渡人 239 | - dA 240 | - rank_cn7: 70 241 | - [] 242 | - - 39 243 | - 四季映姬·亚玛萨那度 244 | - 乐园的最高审判长 245 | - d 246 | - rank_cn7: 21 247 | - - loli 248 | - - 40 249 | - 桑妮·米尔克 250 | - 闪耀的日之光 251 | - BD 252 | - alias_cn: 桑尼米尔克 253 | - - loli 254 | - yousei 255 | - baka 256 | - - 41 257 | - 斯塔·萨菲雅 258 | - 倾泻而下的星之光 259 | - BD 260 | - alias_cn: 斯塔萨菲雅 261 | - - loli 262 | - yousei 263 | - baka 264 | - - 42 265 | - 露娜·切尔德 266 | - 静谧的月之光 267 | - BD 268 | - alias_cn: 露娜切尔德 269 | - - loli 270 | - yousei 271 | - baka 272 | - - 43 273 | - 稗田阿求 274 | - 九代御阿礼少女 275 | - DE 276 | - rank_cn7: 44 277 | - - loli 278 | - - 44 279 | - 森近霖之助 280 | - 不动的古道具店 281 | - D 282 | - rank_cn7: 58 283 | - [] 284 | - - 45 285 | - 秋静叶 286 | - 寂寞与终焉的象征 287 | - e 288 | - rank_cn7: 85 289 | - - loli 290 | - - 46 291 | - 秋穰子 292 | - 丰裕与收成的象征 293 | - e 294 | - rank_cn7: 92 295 | - - loli 296 | - - 47 297 | - 键山雏 298 | - 秘神流雏 299 | - e 300 | - rank_cn7: 34 301 | - [] 302 | - - 48 303 | - 河城荷取 304 | - 超妖怪弹头 305 | - eA 306 | - rank_cn7: 56 307 | - - loli 308 | - - 49 309 | - 犬走椛 310 | - 下位哨戒天狗 311 | - e 312 | - rank_cn7: 47 313 | - - beast 314 | - - 50 315 | - 东风谷早苗 316 | - 祭祀风的人类 317 | - efghjA 318 | - rank_cn7: 13 319 | - [] 320 | - - 51 321 | - 八坂神奈子 322 | - 山坂与湖水的化身 323 | - e 324 | - rank_cn7: 65 325 | - - bba 326 | - - 52 327 | - 洩矢诹访子 328 | - 土著神的顶点 329 | - eA 330 | - rank_cn7: 30 331 | - - loli 332 | - - 53 333 | - 琪斯美 334 | - 可怕的水井妖怪 335 | - f 336 | - rank_cn7: 127 337 | - - loli 338 | - - 54 339 | - 黑谷山女 340 | - 昏暗洞窟中明亮的网 341 | - f 342 | - rank_cn7: 109 343 | - [] 344 | - - 55 345 | - 水桥帕露西 346 | - 地壳下的嫉妒心 347 | - f 348 | - rank_cn7: 46 349 | - [] 350 | - - 56 351 | - 星熊勇仪 352 | - 人所谈论的怪力乱神 353 | - f 354 | - rank_cn7: 81 355 | - [] 356 | - - 57 357 | - 古明地觉 358 | - 连怨灵都恐惧的少女 359 | - f 360 | - rank_cn7: 5 361 | - - loli 362 | - - 58 363 | - 火焰猫燐 364 | - 地狱的车祸 365 | - f 366 | - rank_cn7: 48 367 | - - beast 368 | - - 59 369 | - 灵乌路空 370 | - 难以驾驭的神之火 371 | - fA 372 | - rank_cn7: 41 373 | - - baka 374 | - - 60 375 | - 古明地恋 376 | - 闭合的恋之瞳 377 | - fA 378 | - rank_cn7: 3 379 | - - loli 380 | - - 61 381 | - 永江衣玖 382 | - 美丽的绯之衣 383 | - A 384 | - rank_cn7: 71 385 | - [] 386 | - - 62 387 | - 比那名居天子 388 | - 非想非非想天之女 389 | - A 390 | - rank_cn7: 16 391 | - [] 392 | - - 63 393 | - 娜兹玲 394 | - 探宝的小小大将 395 | - g 396 | - rank_cn7: 93 397 | - - loli 398 | - beast 399 | - - 64 400 | - 多多良小伞 401 | - 愉快的遗忘之伞 402 | - g 403 | - rank_cn7: 38 404 | - - loli 405 | - - 65 406 | - 云居一轮 407 | - 守护与被守护的大轮 408 | - gA 409 | - rank_cn7: 108 410 | - [] 411 | - - 66 412 | - 村纱水蜜 413 | - 水难事故的念缚灵 414 | - g 415 | - rank_cn7: 74 416 | - - uniform 417 | - - 67 418 | - 寅丸星 419 | - 毗沙门天的弟子 420 | - g 421 | - rank_cn7: 112 422 | - [] 423 | - - 68 424 | - 圣白莲 425 | - 被封印的大魔法使 426 | - gA 427 | - rank_cn7: 35 428 | - - bba 429 | - - 69 430 | - 封兽鵺 431 | - 未确认幻想飞行少女 432 | - g 433 | - rank_cn7: 50 434 | - [] 435 | - - 70 436 | - 幽谷响子 437 | - 诵经的山彦 438 | - h 439 | - rank_cn7: 89 440 | - - loli 441 | - beast 442 | - baka 443 | - - 71 444 | - 宫古芳香 445 | - 忠实的尸体 446 | - h 447 | - rank_cn7: 95 448 | - - baka 449 | - - 72 450 | - 霍青娥 451 | - 穿墙的邪仙 452 | - h 453 | - rank_cn7: 57 454 | - [] 455 | - - 73 456 | - 苏我屠自古 457 | - 神明后裔的亡灵 458 | - h 459 | - rank_cn7: 88 460 | - [] 461 | - - 74 462 | - 物部布都 463 | - 古代日本的尸解仙 464 | - hA 465 | - rank_cn7: 55 466 | - [] 467 | - - 75 468 | - 丰聪耳神子 469 | - 圣德道士 470 | - hA 471 | - rank_cn7: 33 472 | - [] 473 | - - 76 474 | - 二岩猯藏 475 | - 佐渡的二岩 476 | - hA 477 | - rank_cn7: 69 478 | - - bba 479 | - beast 480 | - - 77 481 | - 姬海棠果 482 | - 当代的念写记者 483 | - B 484 | - rank_cn7: 82 485 | - - uniform 486 | - - 78 487 | - 茨木华扇 488 | - 独臂有角的仙人 489 | - AD 490 | - rank_cn7: 49 491 | - [] 492 | - - 79 493 | - 玛艾露贝莉·赫恩 494 | - 化猫之幻 495 | - E 496 | - alias_cn: 玛艾露贝莉·赫恩(梅莉) 497 | - [] 498 | - - 80 499 | - 宇佐见莲子 500 | - 月之妖鸟 501 | - E 502 | - rank_cn7: 22 503 | - - uniform 504 | - - 83 505 | - 魅魔 506 | - 将命运托付给久远之梦的精神 507 | - FGHJ 508 | - rank_cn7: 68 509 | - - bba 510 | - - 86 511 | - 神玉 512 | - 门番 513 | - F 514 | - rank_cn7: 165 515 | - [] 516 | - - 87 517 | - 幽玄魔眼 518 | - 邪恶之眼 519 | - F 520 | - rank_cn7: 156 521 | - [] 522 | - - 88 523 | - 依莉斯 524 | - 无罪的恶魔 525 | - F 526 | - rank_cn7: 141 527 | - - loli 528 | - - 89 529 | - 萨丽爱尔 530 | - 死天使 531 | - F 532 | - rank_cn7: 106 533 | - [] 534 | - - 90 535 | - 菊理 536 | - 地狱之月 537 | - F 538 | - rank_cn7: 175 539 | - [] 540 | - - 91 541 | - 矜羯罗 542 | - 星幽剑士 543 | - F 544 | - rank_cn7: 139 545 | - - bba 546 | - - 92 547 | - 里香 548 | - 战车少女 549 | - G 550 | - rank_cn7: 126 551 | - [] 552 | - - 93 553 | - 明罗 554 | - 武士 555 | - G 556 | - rank_cn7: 128 557 | - [] 558 | - - 94 559 | - 爱莲 560 | - 在工作的人身上梦见恋爱的魔女 561 | - H 562 | - rank_cn7: 116 563 | - - loli 564 | - - 95 565 | - 小兔姬 566 | - 在弹幕中梦见美的公主殿下 567 | - H 568 | - rank_cn7: 125 569 | - [] 570 | - - 96 571 | - 卡娜·安娜贝拉尔 572 | - 失去梦的少女骚灵 573 | - H 574 | - rank_cn7: 103 575 | - [] 576 | - - 97 577 | - 朝仓理香子 578 | - 探寻梦想的科学家 579 | - H 580 | - rank_cn7: 135 581 | - - uniform 582 | - - 98 583 | - 北白河千百合 584 | - 超越时空的梦幻居民 585 | - H 586 | - rank_cn7: 130 587 | - - uniform 588 | - - 99 589 | - 冈崎梦美 590 | - 梦幻传说 591 | - H 592 | - rank_cn7: 59 593 | - [] 594 | - - 100 595 | - 奥莲姬 596 | - 妖怪 597 | - I 598 | - rank_cn7: 149 599 | - [] 600 | - - 101 601 | - 胡桃 602 | - 吸血少女 603 | - I 604 | - rank_cn7: 119 605 | - - loli 606 | - - 102 607 | - 艾丽 608 | - 洋馆的门番 609 | - I 610 | - rank_cn7: 132 611 | - [] 612 | - - 103 613 | - 梦月 614 | - 女仆幻想 615 | - I 616 | - rank_cn7: 122 617 | - - loli 618 | - uniform 619 | - - 104 620 | - 幻月 621 | - 可爱的恶魔 622 | - I 623 | - rank_cn7: 97 624 | - - loli 625 | - - 105 626 | - 萨拉 627 | - 门扉的看守者 628 | - J 629 | - rank_cn7: 166 630 | - [] 631 | - - 106 632 | - 露易兹 633 | - 魔界人 634 | - J 635 | - rank_cn7: 150 636 | - [] 637 | - - 107 638 | - 雪 639 | - 黑色的魔法使 640 | - J 641 | - rank_cn7: 137 642 | - [] 643 | - - 108 644 | - 舞 645 | - 白色的魔法使 646 | - J 647 | - rank_cn7: 134 648 | - [] 649 | - - 109 650 | - 梦子 651 | - 魔界女仆 652 | - J 653 | - rank_cn7: 121 654 | - - uniform 655 | - - 110 656 | - 神绮 657 | - 魔界之神 658 | - J 659 | - rank_cn7: 52 660 | - - bba 661 | - - 111 662 | - 玄爷 663 | - 飞行龟 664 | - G 665 | - rank_cn7: 144 666 | - [] 667 | - - 112 668 | - 云山 669 | - 见越入道 670 | - gA 671 | - rank_cn7: 133 672 | - [] 673 | - - 113 674 | - 蕾拉·普莉兹姆利巴 675 | - 虹之川的幺女 676 | - b 677 | - rank_cn7: 123 678 | - - loli 679 | - - 116 680 | - 魂魄妖忌 681 | - 妖梦的祖父 682 | - b 683 | - rank_cn7: 114 684 | - [] 685 | - - 119 686 | - 朱鹭子 687 | - 无名的读书妖怪 688 | - D 689 | - alias_cn: 无名的读书妖怪(朱鹭子) 690 | - - loli 691 | - - 120 692 | - 爱丽丝的人偶 693 | - 蓬莱人偶 694 | - b 695 | - alias_cn: 人偶(含上海人偶、哥利亚人偶) 696 | - - loli 697 | - - 121 698 | - 本居小铃 699 | - 识文解意的爱书人 700 | - D 701 | - rank_cn7: 51 702 | - - loli 703 | - - 122 704 | - 秦心 705 | - 表情丰富的扑克脸 706 | - A 707 | - rank_cn7: 18 708 | - [] 709 | - - 123 710 | - 宇佐见堇子 711 | - 秘封俱乐部初代会长 712 | - AB 713 | - rank_cn7: 40 714 | - [] 715 | - - 124 716 | - 若鹭姬 717 | - 栖息于淡水的人鱼 718 | - i 719 | - rank_cn7: 91 720 | - - baka 721 | - - 125 722 | - 赤蛮奇 723 | - 辘轳首的怪奇 724 | - i 725 | - rank_cn7: 78 726 | - - baka 727 | - - 126 728 | - 今泉影狼 729 | - 竹林的狼人 730 | - i 731 | - rank_cn7: 80 732 | - - beast 733 | - baka 734 | - - 127 735 | - 九十九弁弁 736 | - 古琵琶的付丧神 737 | - i 738 | - rank_cn7: 86 739 | - [] 740 | - - 128 741 | - 九十九八桥 742 | - 古琴的付丧神 743 | - i 744 | - rank_cn7: 111 745 | - [] 746 | - - 129 747 | - 鬼人正邪 748 | - 逆袭的天邪鬼 749 | - iB 750 | - rank_cn7: 24 751 | - [] 752 | - - 130 753 | - 少名针妙丸 754 | - 辉光之针的利立浦特 755 | - iA 756 | - rank_cn7: 37 757 | - - loli 758 | - - 131 759 | - 堀川雷鼓 760 | - 梦幻的打击乐手 761 | - i 762 | - rank_cn7: 60 763 | - - uniform 764 | - - 132 765 | - 清兰 766 | - 浅葱色的 Eagle Rabbit 767 | - j 768 | - rank_cn7: 110 769 | - - beast 770 | - - 133 771 | - 铃瑚 772 | - 橘色的 Eagle Rabbit 773 | - j 774 | - rank_cn7: 117 775 | - - beast 776 | - - 134 777 | - 哆来咪·苏伊特 778 | - 梦之支配者 779 | - jA 780 | - rank_cn7: 36 781 | - - beast 782 | - - 135 783 | - 稀神探女 784 | - 带来口舌之祸的女神 785 | - j 786 | - rank_cn7: 28 787 | - [] 788 | - - 136 789 | - 克劳恩皮丝 790 | - 地狱的妖精 791 | - j 792 | - rank_cn7: 39 793 | - - yousei 794 | - baka 795 | - - 137 796 | - 纯狐 797 | - 无名的存在 798 | - j 799 | - rank_cn7: 25 800 | - - bba 801 | - - 138 802 | - 赫卡提亚·拉碧斯拉祖利 803 | - 地狱的女神 804 | - j 805 | - rank_cn7: 63 806 | - - bba 807 | - - 139 808 | - 易者 809 | - 占卜师 810 | - D 811 | - alias_cn: 占卜师(易者) 812 | - [] 813 | - - 143 814 | - 爱塔妮缇·拉尔瓦 815 | - 接近神的蝶之妖精 816 | - k 817 | - alias_cn: 爱塔妮缇拉尔瓦 818 | - - loli 819 | - yousei 820 | - - 144 821 | - 坂田合欢乃 822 | - 跨越浮世门关的山姥 823 | - k 824 | - rank_cn7: 115 825 | - [] 826 | - - 145 827 | - 高丽野阿吽 828 | - 醉心于神佛的守护神兽 829 | - k 830 | - rank_cn7: 75 831 | - - beast 832 | - - 146 833 | - 矢田寺成美 834 | - 垂迹森林的魔法地藏 835 | - k 836 | - rank_cn7: 76 837 | - [] 838 | - - 147 839 | - 丁礼田舞 840 | - 过于危险的背景舞者 841 | - k 842 | - rank_cn7: 94 843 | - [] 844 | - - 148 845 | - 尔子田里乃 846 | - 过于危险的背景舞者 847 | - k 848 | - rank_cn7: 99 849 | - [] 850 | - - 149 851 | - 摩多罗隐岐奈 852 | - 究极的绝对秘神 853 | - k 854 | - rank_cn7: 31 855 | - - bba 856 | - - 150 857 | - 依神紫苑 858 | - 最凶最恶的双子之姐 859 | - A 860 | - rank_cn7: 23 861 | - [] 862 | - - 151 863 | - 依神女苑 864 | - 最凶最恶的双子之妹 865 | - A 866 | - rank_cn7: 62 867 | - [] 868 | - - 152 869 | - 留琴 870 | - 博丽神社的机械女仆 871 | - H 872 | - rank_cn7: 147 873 | - - uniform 874 | - - 153 875 | - 冴月麟 876 | - 神秘代码 877 | - a 878 | - rank_cn7: 72 879 | - - loli 880 | - - 154 881 | - 命莲 882 | - 飞仓的圣僧 883 | - g 884 | - rank_cn7: 140 885 | - [] 886 | - - 155 887 | - 邪眼西格玛 888 | - 可爱的 ICBM 889 | - G 890 | - rank_cn7: 157 891 | - [] 892 | - - 156 893 | - 咪咪号 894 | - 少女的梦之战车 895 | - H 896 | - rank_cn7: 177 897 | - [] -------------------------------------------------------------------------------- /lib/data/games.yaml: -------------------------------------------------------------------------------- 1 | integer: 2 | - tag: a 3 | name: 东方红魔乡 4 | - tag: b 5 | name: 东方妖妖梦 6 | - tag: c 7 | name: 东方永夜抄 8 | - tag: d 9 | name: 东方花映冢 10 | - tag: e 11 | name: 东方风神录 12 | - tag: f 13 | name: 东方地灵殿 14 | - tag: g 15 | name: 东方星莲船 16 | - tag: h 17 | name: 东方神灵庙 18 | - tag: i 19 | name: 东方辉针城 20 | - tag: j 21 | name: 东方绀珠传 22 | - tag: k 23 | name: 东方天空璋 24 | 25 | others: 26 | - tag: A 27 | name: 格斗作 28 | - tag: B 29 | name: 外传 STG 30 | - tag: D 31 | name: 官方书籍 32 | - tag: E 33 | name: 音乐 CD 34 | 35 | old: 36 | - tag: F 37 | name: 东方灵异传 38 | - tag: G 39 | name: 东方封魔录 40 | - tag: H 41 | name: 东方梦时空 42 | - tag: I 43 | name: 东方幻想乡 44 | - tag: J 45 | name: 东方怪绮谈 46 | -------------------------------------------------------------------------------- /lib/data/index.js: -------------------------------------------------------------------------------- 1 | import RANK_CN7 from './rank_cn7' 2 | 3 | import SortNode from '../utils/SortNode' 4 | import characters from './characters' 5 | import games from './games' 6 | import tags from './tags' 7 | 8 | export const faces = { 9 | default: '正常', 10 | smiling: '笑脸', 11 | trauma: '疮痍', 12 | } 13 | 14 | const rankMap = {} 15 | const charMap = {} 16 | characters.forEach((rawChar) => { 17 | const char = new SortNode(...rawChar) 18 | charMap[char.name] = char 19 | rankMap[char.meta.alias_cn || char.name] = char.name 20 | }) 21 | 22 | const charList = Object.keys(charMap) 23 | 24 | function filterRanks (ranks) { 25 | return ranks.map((name) => { 26 | if (!(name in rankMap)) return 27 | return rankMap[name] 28 | }).filter(n => n) 29 | } 30 | 31 | export const ranks = { 32 | cn7: filterRanks(RANK_CN7), 33 | } 34 | 35 | export { characters, games, tags, charMap, charList } 36 | -------------------------------------------------------------------------------- /lib/data/rank_cn7.yaml: -------------------------------------------------------------------------------- 1 | # http://touhou.vote/v7/?m=chara&type=simple 2 | 3 | - 博丽灵梦 4 | - 雾雨魔理沙 5 | - 古明地恋 6 | - 蕾米莉亚·斯卡蕾特 7 | - 古明地觉 8 | - 八云紫 9 | - 十六夜咲夜 10 | - 西行寺幽幽子 11 | - 藤原妹红 12 | - 射命丸文 13 | - 爱丽丝·玛格特洛依德 14 | - 芙兰朵露·斯卡蕾特 15 | - 东风谷早苗 16 | - 魂魄妖梦 17 | - 铃仙·优昙华院·因幡 18 | - 比那名居天子 19 | - 琪露诺 20 | - 秦心 21 | - 蓬莱山辉夜 22 | - 帕秋莉·诺蕾姬 23 | - 四季映姬·亚玛萨那度 24 | - 宇佐见莲子 25 | - 依神紫苑 26 | - 鬼人正邪 27 | - 纯狐 28 | - 风见幽香 29 | - 伊吹萃香 30 | - 稀神探女 31 | - 玛艾露贝莉·赫恩(梅莉) 32 | - 洩矢诹访子 33 | - 摩多罗隐岐奈 34 | - 红美铃 35 | - 丰聪耳神子 36 | - 键山雏 37 | - 圣白莲 38 | - 哆来咪·苏伊特 39 | - 少名针妙丸 40 | - 多多良小伞 41 | - 克劳恩皮丝 42 | - 宇佐见堇子 43 | - 灵乌路空 44 | - 上白泽慧音 45 | - 露米娅 46 | - 稗田阿求 47 | - 八意永琳 48 | - 水桥帕露西 49 | - 犬走椛 50 | - 火焰猫燐 51 | - 茨木华扇 52 | - 封兽鵺 53 | - 本居小铃 54 | - 神绮 55 | - 大妖精 56 | - 八云蓝 57 | - 物部布都 58 | - 河城荷取 59 | - 霍青娥 60 | - 森近霖之助 61 | - 冈崎梦美 62 | - 堀川雷鼓 63 | - 莉莉白 64 | - 依神女苑 65 | - 赫卡提亚·拉碧斯拉祖利 66 | - 小恶魔 67 | - 八坂神奈子 68 | - 米斯蒂娅·萝蕾拉 69 | - 因幡帝 70 | - 魅魔 71 | - 二岩猯藏 72 | - 小野塚小町 73 | - 永江衣玖 74 | - 冴月麟 75 | - 橙 76 | - 村纱水蜜 77 | - 高丽野阿吽 78 | - 矢田寺成美 79 | - 露娜切尔德 80 | - 赤蛮奇 81 | - 占卜师(易者) 82 | - 今泉影狼 83 | - 星熊勇仪 84 | - 姬海棠果 85 | - 爱塔妮缇拉尔瓦 86 | - 斯塔萨菲雅 87 | - 秋静叶 88 | - 九十九弁弁 89 | - 梅蒂欣·梅兰可莉 90 | - 苏我屠自古 91 | - 幽谷响子 92 | - 桑尼米尔克 93 | - 若鹭姬 94 | - 秋穰子 95 | - 娜兹玲 96 | - 丁礼田舞 97 | - 宫古芳香 98 | - 绵月依姬 99 | - 幻月 100 | - 蕾蒂·霍瓦特洛克 101 | - 尔子田里乃 102 | - 人偶(含上海人偶、哥利亚人偶) 103 | - 露娜萨·普莉兹姆利巴 104 | - 莉格露·奈特巴格 105 | - 卡娜·安娜贝拉尔 106 | - 长相酷似周杰伦的参拜客 107 | - 毛玉 108 | - 萨丽爱尔 109 | - 绵月丰姬 110 | - 云居一轮 111 | - 黑谷山女 112 | - 清兰 113 | - 九十九八桥 114 | - 寅丸星 115 | - 无名的读书妖怪(朱鹭子) 116 | - 魂魄妖忌 117 | - 坂田合欢乃 118 | - 爱莲 119 | - 铃瑚 120 | - 梅露兰·普莉兹姆利巴 121 | - 胡桃 122 | - 大鲶鱼 123 | - 梦子 124 | - 梦月 125 | - 蕾拉·普莉兹姆利巴 126 | - 莉莉卡·普莉兹姆利巴 127 | - 小兔姬 128 | - 里香 129 | - 琪斯美 130 | - 明罗 131 | - 妖精(含持花妖精,僵尸妖精) 132 | - 北白河千百合 133 | - 假扮魔理沙的妖狐 134 | - 艾丽 135 | - 云山 136 | - 舞 137 | - 朝仓理香子 138 | - STG作品中没有名称的角色 139 | - 雪 140 | - 非想天则 141 | - 矜羯罗 142 | - 命莲 143 | - 依莉斯 144 | - 龙神 145 | - 秦河胜 146 | - 玄爷 147 | - Reisen 148 | - 嫦娥 149 | - 留琴 150 | - UFO 151 | - 奥莲姬 152 | - 露易兹 153 | - 求闻史纪中的受访人群 154 | - 阴阳玉 155 | - 苏格拉底 156 | - 月夜见 157 | - 酒吧老板 158 | - 幽玄魔眼 159 | - 邪眼西格玛 160 | - 岩笠 161 | - 久米 / 竿打 162 | - 万岁乐 163 | - 妖怪兔 164 | - 绿巨人 165 | - 阿菊 166 | - 黄帝 167 | - 神玉 168 | - 萨拉 169 | - 伊豆能卖 170 | - 邪龙 171 | - 怨灵 172 | - 河童(含山童) 173 | - 兜风四人组 174 | - 导航 175 | - 彭祖 176 | - 尼西号 177 | - 菊理 178 | - Flower~战车 179 | - 咪咪号 180 | - 玛○奇 181 | - 木花咲耶姬 182 | - 天照大御神 183 | - 野槌 184 | - 酒虫 185 | - 怨灵少女 186 | - 读书狐狸 187 | - 白仙和尚 188 | - 幽灵 189 | - 狸猫 190 | - 杀死大老爷的侍童 191 | - 八尺大人 192 | - 石长姬 193 | - 石凝老命 194 | - 天宇受卖命 195 | - 火雷神 196 | - 瑞江浦岛子 197 | - 前鬼 / 后鬼 198 | - 雷兽(务光) 199 | - 运松翁 200 | - 管狐 201 | - 水鬼鬼神长 202 | - 人面犬 203 | - 烟烟罗 204 | - 卓柏卡布拉 205 | - 沓颊 206 | - 稻荷大人 207 | - 佑天上人 208 | - 野铁炮 209 | - 地精 210 | - 座敷童子 211 | - 盐家老板 212 | - 马凭 213 | - 竹扫帚付丧神 214 | - 大老爷 215 | - 酒吧客人 216 | - 百鬼夜行绘卷妖怪(黑鬼) 217 | - 卖脚婆 218 | - 袈裟罗婆娑罗 219 | - 饿神 220 | - 睡鼠 221 | -------------------------------------------------------------------------------- /lib/data/tags.yaml: -------------------------------------------------------------------------------- 1 | loli: 萝莉 2 | bba: BBA 3 | yousei: 妖精 4 | beast: 兽娘 5 | baka: 笨蛋 6 | uniform: 制服 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path') 2 | 3 | // workaround vuepress #1525 4 | const App = require('@vuepress/core/lib/node/App') 5 | App.prototype.addPage = async function (options) { 6 | const Page = require('@vuepress/core/lib/node/Page') 7 | options.permalinkPattern = this.siteConfig.permalink 8 | const page = new Page(options, this) 9 | await page.process({ 10 | markdown: this.markdown, 11 | computed: new this.ClientComputedMixinConstructor(), 12 | enhancers: this.pluginAPI.getOption('extendPageData').items, 13 | }) 14 | const index = this.pages.findIndex(({ path }) => path === page.path) 15 | if (index >= 0) { 16 | this.pages.splice(index, 1, page) 17 | } else { 18 | this.pages.push(page) 19 | } 20 | } 21 | 22 | module.exports = ({ 23 | base = '/favorite/', 24 | }, context) => ({ 25 | name: '@uzkk/favorite', 26 | 27 | plugins: [ 28 | [require('@uzkk/favorite-assets')], 29 | ['@vuepress/register-components', { 30 | components: [ 31 | { name: 'Favorite', path: resolve(__dirname, 'comp') }, 32 | ], 33 | }], 34 | ], 35 | 36 | additionalPages: [{ 37 | title: '本命角色测试', 38 | path: base, 39 | permalink: base, 40 | frontmatter: { 41 | description: '测试你的本命东方 Project 角色', 42 | layout: 'Favorite', 43 | }, 44 | }, { 45 | title: '关于本命角色测试', 46 | path: base + 'about.html', 47 | permalink: base + 'about.html', 48 | frontmatter: { 49 | description: '关于本命角色测试', 50 | layout: 'Post', 51 | aside: false, 52 | }, 53 | filePath: resolve(__dirname, 'pages/about.md'), 54 | }], 55 | 56 | enhanceAppFiles: { 57 | name: 'uzkk-favorite-base.js', 58 | content: `export default ({ Vue }) => { 59 | Vue.prototype.UZKK_FAVORITE_BASE = ${JSON.stringify(base)} 60 | }`, 61 | }, 62 | 63 | chainWebpack (config) { 64 | config.module 65 | .rule('ts') 66 | .test(/\.ts$/) 67 | .exclude 68 | .add(path => !path.startsWith(__dirname)) 69 | .end() 70 | .use('ts-loader') 71 | .loader('ts-loader') 72 | .options({ 73 | configFile: resolve(__dirname, '../tsconfig.json'), 74 | }) 75 | .end() 76 | }, 77 | }) 78 | -------------------------------------------------------------------------------- /lib/pages/about.md: -------------------------------------------------------------------------------- 1 | 这个项目的灵感来源于日文网站 [東方キャラソート](http://readalittle.net/sort/)。在编写时也有许多细节参考了这个网站。我们在此基础上,进行了一些改变: 2 | 3 | 1. 将全部内容翻译成了中文。 4 | 2. 对网站进行了移动端适配。 5 | 3. 增删了一些角色(比如增加了冴月麟,删除了旧作与新作重合的角色)。 6 | 4. 基于原网站缺乏角色在后期作品中出场的记录,我们重新将全部角色进行分类。 7 | 5. 结果页增加了与人气投票的对比,并添加了标签取向判断的功能。 8 | 9 | 下面是大家或许好奇的一些问题,我们将一一解答。 10 | 11 | ## 关于算法 12 | 13 | 这种排序的原理是 [Pairwise Comparison](https://en.wikipedia.org/wiki/Pairwise_comparison)。根据选择的顺序,结果可能会发生不同。排序的过程使用了堆排序。 14 | 15 | 计算标签取向的算法如下:假设你已经选出了你最喜欢的 $m$ 名角色,则将这些角色及人气排名前 $m$ 名取并集(设这个集合的大小为 $n$)。设 $r_i,p_i$ 分别表示第 $i$ 名角色你的排名结果和人气排名的结果(如果未出现在某个排名中,则取 $m+1$),并计算下面的值: 16 | 17 | $$u_i=\tanh{\frac{p_i-r_i}{m}},\ v_i=\frac{1}{1+r_i}$$ 18 | 19 | 从这些角色中取出所有含有第 $k$ 个标签的角色,设对应的下标集为 $\mathcal{I}$,则该标签的参考值为: 20 | 21 | $$w_k=\frac{\sum_{i\in\mathcal{I}}u_i v_i}{\sum_{i=1}^{n}v_i}$$ 22 | 23 | 这种计算方法仅供参考。如果有更好的建议,欢迎向我们提出。 24 | 25 | ## 更新日志 26 | 27 | ### 2019-4-27 28 | 29 | - 添加了“命莲”等三个角色 30 | - 修复了记忆功能对全选角色失效的问题 31 | 32 | ### 2019-04-15 33 | 34 | - 可以选择全部角色进行排序了 35 | - 为十六夜咲夜和部分旧作角色添加了标签 36 | - 优化了结果页面对手机屏幕的适配 37 | 38 | ### 2019-04-12 39 | 40 | - 新增了“疮痍”系列图片 41 | - 优化了偏好参考值的算法 42 | - 结果页面添加了统计信息 43 | 44 | ### 2019-04-11 45 | 46 | - 将外传角色加入到默认配置中 47 | - 添加了根据偏好参考值生成的提示语 48 | - 设置界面增加了记忆功能,进入时将自动恢复到上一次的设置 49 | 50 | ### 2019-04-09 51 | 52 | - 新增了随机选择功能 53 | - 选择界面增加了 50 和 100 角色的选项 54 | - 优化了按钮和单选框在手机界面上的显示 55 | 56 | ### 2019-04-07 57 | 58 | - 重写了计算偏好参考值的逻辑 59 | - 使用 TypeScript 重构了排序部分 60 | - 取消了魂魄妖梦的萝莉标签 61 | 62 | ### 2019-04-05 63 | 64 | - 新增了“笨蛋”和“制服”两个标签 65 | - 将分类“东方文花帖”和“妖精大战争”合并为“外传 STG” 66 | - 修复了一些人名错误(如:爱丽丝·玛格特洛依德) 67 | - 修复了辉针城以后部分角色的出场信息缺失(如:琪露诺) 68 | 69 | ### 2019-04-03 70 | 71 | - 优化了结果界面 72 | - 对网站进行了移动端适配 73 | - 将分类“旧作”进行进一步细分 74 | - 新增了“妖精”和“兽娘”两个标签 75 | 76 | ### 2019-3-25 77 | 78 | - 优化了设置界面 79 | - 支持在选择时回退一步 80 | - 新增了标签系统,包含“萝莉”和“BBA”两个标签 81 | 82 | ### 2019-3-23 83 | 84 | - 初步实现原网站的功能 85 | - 将全部内容翻译成了中文 86 | - 增删了一些角色(比如增加了冴月麟,删除了旧作与新作重合的角色) 87 | 88 | ## 制作人员 89 | 90 | 91 | 92 | 93 | 部分原作没有的称号由 @荭茶 和 @龙翼雨 提供。 94 | 95 | ## 版权与协议 96 | 97 | 命莲、邪眼西格玛及咪咪号的图片采用了画师 [kaoru](https://www.pixiv.net/member_illust.php?id=743845) 的作品,其他角色图片采用了画师 [dairi](https://www.pixiv.net/member_illust.php?id=4920496) 的作品。全部人物来源于东方 Project,版权属于上海爱丽丝幻乐团。 98 | 99 | 源代码已经开源到 [GitHub](https://github.com/uzkk/favorite),遵循 [MIT](https://mit-license.org/) 协议。脚本的使用,修改,复制等是免费的。 100 | 101 |

102 | 点此返回测试页面 103 |

104 | -------------------------------------------------------------------------------- /lib/styles/index.styl: -------------------------------------------------------------------------------- 1 | .favorite .emphasize 2 | color #d00 3 | 4 | .favorite .section 5 | a:hover 6 | text-decoration underline 7 | 8 | h3 9 | font-size 1.2em 10 | 11 | hr 12 | display block 13 | border 0 14 | border-top 2px solid $borderColor 15 | margin 0.5em 0 16 | transform translateY(-1px) 17 | padding 0 18 | 19 | a:hover 20 | text-decoration underline 21 | 22 | hr 23 | display block 24 | border 0 25 | border-top 2px solid $borderColor 26 | margin 0.6em 0 27 | transform translateY(-1px) 28 | padding 0 29 | 30 | > :first-child 31 | margin-top 0 !important 32 | 33 | > :last-child 34 | margin-bottom 0 !important 35 | 36 | p, ul 37 | margin 0.5em 0 38 | line-height 1.6 39 | 40 | p.comment 41 | margin 0.2em 0 42 | 43 | p.title 44 | font-weight bold 45 | 46 | p.list 47 | margin 0 48 | > :first-child 49 | font-weight bold 50 | display inline-block 51 | margin 0.5em 0 52 | &:not(:last-child) > :not(:first-child) 53 | margin-top 0 54 | &:first-child > * 55 | margin-top 0 56 | &:last-child > * 57 | margin-bottom 0 58 | & + p.list 59 | margin-top -0.5em 60 | 61 | ul 62 | padding-inline-start 1.6em 63 | &.inline 64 | display inline-block 65 | 66 | li 67 | list-style-type none 68 | &.inline 69 | display inline-block 70 | &.medium 71 | min-width 9em 72 | &.short 73 | min-width 5em 74 | 75 | &.collapse-view 76 | .collapse-header h3 77 | margin 0 78 | .collapse-content > :first-child 79 | margin-top 1em 80 | 81 | table 82 | max-width 100% 83 | border-collapse collapse 84 | margin 1.5rem auto 0 85 | text-align center 86 | 87 | tr 88 | border-top 1px solid #dfe2e5 89 | &:nth-child(2n) 90 | background-color #f6f8fa 91 | 92 | th, td 93 | border 1px solid #dfe2e5 94 | padding .5em 1em 95 | 96 | .favorite .button-container 97 | margin 1.8em auto 98 | width 30% 99 | max-width 20em 100 | min-width 14em 101 | text-align center 102 | 103 | button 104 | width 100% 105 | display block 106 | margin 0.6em 0 107 | -------------------------------------------------------------------------------- /lib/utils/BackupTree.ts: -------------------------------------------------------------------------------- 1 | type Character = [number, string, string, string, Record, string[]] 2 | 3 | // @ts-ignore 4 | import { characters } from '../data' 5 | import SortNode from './SortNode' 6 | 7 | export default class BackupTree { 8 | id: number = -1 9 | isEven: boolean = false 10 | nodes: BackupTree[] = [] 11 | cTree: SortNode = null 12 | 13 | constructor () {} 14 | 15 | /** 16 | * set root node 17 | */ 18 | public setup (root: SortNode) { 19 | this.cTree = new SortNode(0) 20 | this.nodes = [] 21 | this.init(root) 22 | return this 23 | } 24 | 25 | /** 26 | * generate ID tree from current character ranks 27 | */ 28 | private init (root: SortNode, tree: BackupTree = this) { 29 | for (const child of root.children) { 30 | const node = new BackupTree() 31 | node.id = child.id 32 | node.isEven = child.isEven 33 | tree.nodes.push(node) 34 | this.init(child, node) 35 | } 36 | } 37 | 38 | /** 39 | * restore character ranks from ID tree 40 | */ 41 | public restore (idTree: BackupTree = this, cNode = this.cTree) { 42 | var len = idTree.nodes.length 43 | for (var i = 0; i < len; i++) { 44 | var nodeId = idTree.nodes[i].id 45 | var isEven = idTree.nodes[i].isEven 46 | var cItem = null 47 | for (var j = 0; j < characters.length; j++) { 48 | if (nodeId === characters[j][0]) { 49 | cItem = new SortNode(...characters[j]) 50 | cItem.isEven = isEven 51 | break 52 | } 53 | } 54 | // why the hell is this NOT FOUND ??? 55 | if (cItem === null) { 56 | continue 57 | } 58 | cNode.add(cItem, false) 59 | this.restore(idTree.nodes[i], cItem) 60 | } 61 | return this.cTree 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lib/utils/SortNode.ts: -------------------------------------------------------------------------------- 1 | export default class SortNode { 2 | public isEven: boolean = false 3 | public parent: SortNode = null 4 | public children: SortNode[] = [] 5 | 6 | constructor ( 7 | public id: number = 0, 8 | public name: string = '', 9 | public nick: string = '', 10 | public appearence: string = '', 11 | public meta: Record = {}, 12 | public tags: string[] = [], 13 | ) {} 14 | 15 | /** 16 | * clone a node 17 | */ 18 | clone () { 19 | return new SortNode( 20 | this.id, 21 | this.name, 22 | this.nick, 23 | this.appearence, 24 | this.meta, 25 | this.tags, 26 | ) 27 | } 28 | 29 | /** 30 | * get current rank 31 | */ 32 | rank () { 33 | return this.parent 34 | ? this.isEven 35 | ? this.parent.rank() 36 | : this.level() 37 | : 0 38 | } 39 | 40 | /** 41 | * get node depth 42 | */ 43 | level () { 44 | if (!this.parent) return 0 45 | return this.parent.level() + 1 46 | } 47 | 48 | /** 49 | * add child node 50 | */ 51 | add (child: SortNode, doEvenAction = false) { 52 | // remove node from its parent 53 | if (child.parent) { 54 | child.parent.children.splice(child.parent.children.indexOf(child), 1) 55 | } 56 | 57 | // handle even 58 | if (doEvenAction) { 59 | var copies = this.children.splice(0, this.children.length) 60 | for (var i = copies.length - 1; i >= 0; i--) { 61 | copies[i].parent = child 62 | } 63 | child.children.push(...copies) 64 | } 65 | 66 | this.children.push(child) 67 | child.parent = this 68 | } 69 | 70 | /** 71 | * remove current node 72 | */ 73 | remove () { 74 | while (this.children.length > 0) { 75 | this.parent.add(this.children[0], false) 76 | } 77 | this.parent.children.splice(this.parent.children.indexOf(this), 1) 78 | } 79 | 80 | /** 81 | * 全体结点中搜索 resourceId 并返回 SortObject。没找到则返回 null。 82 | */ 83 | findSortObjectById (resourceId) { 84 | if (this.id === resourceId) { 85 | return this 86 | } 87 | var len = this.children.length 88 | for (var i = 0; i < len; i++) { 89 | var result = this.children[i].findSortObjectById(resourceId) 90 | if (result !== null) { 91 | return result 92 | } 93 | } 94 | return null 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { ranks, charMap, tags, charList } from '../data' 3 | import SortNode from './SortNode' 4 | 5 | function getRank (name: string, ranks: string[]) { 6 | const index = ranks.indexOf(name) 7 | return index === -1 ? ranks.length : index 8 | } 9 | 10 | export function getCharactersInRange (range: string, chars: string[] = charList) { 11 | return chars.filter((name) => { 12 | for (const char of charMap[name].appearence) { 13 | if (range.includes(char)) return true 14 | } 15 | return false 16 | }) 17 | } 18 | 19 | export function getPreference (userRanking: string[], range: string) { 20 | const { length } = userRanking 21 | let denominator = 0 22 | const popRanking = getCharactersInRange(range, ranks.cn7).slice(0, length) 23 | const rankingChars = Array 24 | .from(new Set([...popRanking, ...userRanking])) 25 | .map((name) => { 26 | const userRank = getRank(name, userRanking) 27 | const popRank = getRank(name, popRanking) 28 | const weight = 1 / (2 + userRank) 29 | denominator += weight 30 | return { 31 | node: charMap[name] as SortNode, 32 | value: Math.tanh((popRank - userRank) / length) * weight, 33 | } 34 | }) 35 | 36 | const preference = [] 37 | for (const tag in tags) { 38 | const name = tags[tag] 39 | 40 | const chars = rankingChars.filter(({ node }) => node.tags.includes(tag)) 41 | if (!chars.length) continue 42 | 43 | const value = chars.reduce((sum, { value }) => sum + value, 0) / denominator 44 | preference.push({ tag, name, value }) 45 | } 46 | 47 | return preference 48 | .filter(p => p.value > 0) 49 | .sort((a, b) => a.value > b.value ? -1 : 1) 50 | } 51 | 52 | type Group = [string, number, number] 53 | 54 | function group (length: number, groupLength: number, startIndex: number = 0) { 55 | length -= startIndex 56 | const groups = new Array(Math.ceil(length / groupLength)).fill(undefined) 57 | groups[groups.length - 1] = length % groupLength 58 | return groups.map((_, index): Group => { 59 | if (index < groups.length - 1) { 60 | const start = groupLength * index + startIndex 61 | return ['sm', start, groupLength + start] 62 | } 63 | const end = length + startIndex 64 | return ['sm', end - (length % groupLength || groupLength), end] 65 | }) 66 | } 67 | 68 | const groupMap: Record Group[]> = { 69 | 2 (length) { 70 | switch (length % 2) { 71 | case 0: return group(length, 2) 72 | case 1: return [['lg', 0, 1], ...group(length, 2, 1)] 73 | } 74 | }, 75 | 3 (length) { 76 | switch (length % 3) { 77 | case 0: return [['lg', 0, 1], ['md', 1, 3], ...group(length, 3, 3)] 78 | case 1: return [['lg', 0, 1], ...group(length, 3, 1)] 79 | case 2: return [['lg', 0, 2], ...group(length, 3, 2)] 80 | } 81 | }, 82 | 4 (length) { 83 | if (length === 1) return [['lg', 0, 1]] 84 | switch (length % 4) { 85 | case 0: return [['lg', 0, 1], ['sm', 1, 4], ...group(length, 4, 4)] 86 | case 1: return [['lg', 0, 2], ['sm', 2, 5], ...group(length, 4, 5)] 87 | case 2: return [['lg', 0, 2], ...group(length, 4, 2)] 88 | case 3: return [['lg', 0, 1], ['md', 2, 3], ...group(length, 4, 3)] 89 | } 90 | }, 91 | 5 (length) { 92 | if (length === 1) return [['lg', 0, 1]] 93 | if (length === 2) return [['lg', 0, 2]] 94 | if (length === 3) return [['lg', 0, 1], ['md', 1, 3]] 95 | switch (length % 5) { 96 | case 0: return [['lg', 0, 2], ['md', 2, 5], ...group(length, 5, 5)] 97 | case 1: return [['lg', 0, 1], ['md', 1, 3], ['md', 3, 6], ...group(length, 5, 6)] 98 | case 2: return [['lg', 0, 1], ['md', 1, 4], ['md', 4, 7], ...group(length, 5, 7)] 99 | case 3: return [['lg', 0, 2], ['md', 2, 5], ['md', 5, 8], ...group(length, 5, 8)] 100 | case 4: return [['lg', 0, 1], ['md', 1, 4], ...group(length, 5, 4)] 101 | } 102 | }, 103 | 6 (length) { 104 | if (length === 1) return [['lg', 0, 1]] 105 | if (length === 2) return [['lg', 0, 2]] 106 | if (length === 3) return [['lg', 0, 1], ['md', 1, 3]] 107 | if (length === 4) return [['lg', 0, 1], ['md', 1, 4]] 108 | if (length === 5) return [['lg', 0, 2], ['md', 2, 5]] 109 | switch (length % 6) { 110 | case 0: return [['lg', 0, 1], ['md', 1, 3], ['md', 3, 6], ...group(length, 6, 6)] 111 | case 1: return [['lg', 0, 1], ['md', 1, 4], ['md', 4, 7], ...group(length, 6, 7)] 112 | case 2: return [['lg', 0, 2], ['md', 2, 5], ['md', 5, 8], ...group(length, 6, 8)] 113 | case 3: return [['lg', 0, 2], ['md', 2, 5], ['md', 5, 9], ...group(length, 6, 9)] 114 | case 4: return [['lg', 0, 2], ['md', 2, 5], ['sm', 5, 10], ...group(length, 6, 10)] 115 | case 5: return [['lg', 0, 2], ['md', 2, 6], ['sm', 6, 11], ...group(length, 6, 11)] 116 | } 117 | }, 118 | } 119 | 120 | // .main-container padding 2em (+ scrollbar 1em) = 48px 121 | // .char-view = 16px * (10 + 1.5 * 2) = 208px 122 | // .char-view.lg = 208px * 1.125 = 234px 123 | // .char-view.sm = 208px * 0.75 = 156px 124 | export function groupByWidth (length: number, width: number) { 125 | const groupLength = Math.max(Math.min(Math.floor((width - 49) / 156), 6), 2) 126 | return groupMap[groupLength](length) 127 | } 128 | -------------------------------------------------------------------------------- /lib/utils/settings.ts: -------------------------------------------------------------------------------- 1 | const VERSION = 1 2 | 3 | const getFallback = () => ({ 4 | ranknum: 1, 5 | face: 'default', 6 | range: 'abcdefghijkABDE', 7 | }) 8 | 9 | export function getSettings () { 10 | const fallbackSettings = getFallback() 11 | 12 | if (typeof localStorage === 'undefined') return fallbackSettings 13 | 14 | const oldSettings = localStorage.getItem('uzkk.favorite.settings') 15 | if (!oldSettings) return fallbackSettings 16 | 17 | try { 18 | const { version, settings } = JSON.parse(oldSettings) 19 | if (version === VERSION) { 20 | return { ...fallbackSettings, ...settings } 21 | } else { 22 | return fallbackSettings 23 | } 24 | } catch (error) { 25 | console.warn('An error was encounted when parsing settings:\n' + oldSettings) 26 | return fallbackSettings 27 | } 28 | } 29 | 30 | export function setSettings (settings) { 31 | const userSettings = {} 32 | for (const key in getFallback()) { 33 | userSettings[key] = settings[key] 34 | } 35 | 36 | if (typeof localStorage !== 'undefined') { 37 | try { 38 | const newSettings = { version: VERSION, settings: userSettings } 39 | localStorage.setItem('uzkk.favorite.settings', JSON.stringify(newSettings)) 40 | } catch (error) { 41 | console.warn('An error was encounted when stringifying settings.') 42 | console.warn(error) 43 | } 44 | } 45 | 46 | return userSettings 47 | } 48 | 49 | export function useFallback (settings) { 50 | const fallbackSettings = getFallback() 51 | return Object.assign(settings, fallbackSettings) 52 | } 53 | -------------------------------------------------------------------------------- /lib/utils/sort.mixin.ts: -------------------------------------------------------------------------------- 1 | import BackupTree from './BackupTree' 2 | 3 | export default { 4 | data: () => ({ 5 | currentPair: [], 6 | questionCount: 1, 7 | currentRank: 0, 8 | isPrevious: false, 9 | }), 10 | 11 | methods: { 12 | init () { 13 | this.currentPair = this.ask(this.root) 14 | this.backupTree = new BackupTree().setup(this.root) 15 | }, 16 | backup () { 17 | this.backupPair = this.currentPair.slice() 18 | this.backupTree.setup(this.root) 19 | }, 20 | restore () { 21 | this.currentPair = this.backupPair.slice() 22 | this.backupPair = null 23 | this.currentRank = 0 24 | this.root = this.backupTree.restore() 25 | }, 26 | ask (node, pair) { 27 | if (pair) { 28 | const left = node.findSortObjectById(pair[0].id) 29 | const right = node.findSortObjectById(pair[1].id) 30 | if (left !== null && right !== null) { 31 | return [left, right] 32 | } 33 | } 34 | if (node.children.length === 0) { 35 | return false 36 | } 37 | if (node.children.length === 1) { 38 | this.currentRank = node.level() + 1 39 | return this.ask(node.children[0]) 40 | } 41 | const both = [0, 0] 42 | while (true) { 43 | if (both[0] !== both[1]) { 44 | break 45 | } 46 | for (const i of [0, 1]) { 47 | both[i] = Math.floor(Math.random() * node.children.length) 48 | } 49 | } 50 | return [node.children[both[0]], node.children[both[1]]] 51 | }, 52 | getNextPair (back: boolean) { 53 | if (back) { 54 | this.currentPair = this.ask(this.root, this.currentPair) 55 | } else { 56 | this.currentPair = this.ask(this.root) 57 | } 58 | if (this.currentPair && this.currentPair[1].level() <= this.ranknum) { 59 | this.questionCount += 1 60 | return true 61 | } 62 | return false 63 | }, 64 | moveOn (back: boolean) { 65 | return this.getNextPair(back) 66 | }, 67 | selectChar (index: number) { 68 | this.backup() 69 | this.currentPair[index].add(this.currentPair[1 - index], false) 70 | this.isPrevious = false 71 | this.moveOn(false) 72 | }, 73 | exclude (...indices) { 74 | this.backup() 75 | for (let i of indices) { 76 | this.currentPair[i].remove() 77 | } 78 | this.moveOn(false) 79 | }, 80 | previous () { 81 | this.restore() 82 | this.questionCount -= 2 83 | this.isPrevious = true 84 | this.moveOn(true) 85 | }, 86 | randomPick () { 87 | const index = Math.floor(Math.random() * 2) 88 | this.selectChar(index) 89 | }, 90 | randomPickForAll () { 91 | let index = Math.floor(Math.random() * 2) 92 | this.currentPair[index].add(this.currentPair[1 - index], false) 93 | let pair = this.ask(this.root) 94 | while (pair && pair[1].level() <= this.ranknum) { 95 | this.questionCount += 1 96 | index = Math.floor(Math.random() * 2) 97 | pair[index].add(pair[1 - index], false) 98 | pair = this.ask(this.root) 99 | } 100 | this.moveOn(false) 101 | }, 102 | }, 103 | } 104 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@uzkk/favorite", 3 | "version": "1.3.0", 4 | "main": "lib/index.js", 5 | "author": "Shigma <1700011071@pku.edu.cn>", 6 | "license": "MIT", 7 | "files": [ 8 | "lib", 9 | "tsconfig.json" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/uzkk/favorite.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/uzkk/favorite/issues" 17 | }, 18 | "homepage": "https://vp.uzkk.net/favorite/", 19 | "scripts": { 20 | "build": "vuepress build docs", 21 | "dev": "vuepress dev docs", 22 | "serve": "vuepress serve docs", 23 | "lint": "eslint . --fix" 24 | }, 25 | "devDependencies": { 26 | "@uzkk/shared-assets": "^0.0.4", 27 | "@uzkk/not-found": "^1.0.1", 28 | "eslint": "^5.15.1", 29 | "eslint-config-standard": "^12.0.0", 30 | "eslint-plugin-import": "^2.14.0", 31 | "eslint-plugin-node": "^8.0.0", 32 | "eslint-plugin-promise": "^4.0.1", 33 | "eslint-plugin-standard": "^4.0.0", 34 | "eslint-plugin-vue": "^5.2.2", 35 | "vuepress": "^1.0.0-alpha.47", 36 | "vuepress-theme-uzkk": "^1.0.0-alpha.31" 37 | }, 38 | "dependencies": { 39 | "@uzkk/favorite-assets": "^0.0.8", 40 | "js-yaml-loader": "^1.0.1", 41 | "ts-loader": "^5.3.3", 42 | "typescript": "^3.4.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "allowJs": true, 6 | }, 7 | "include": [ 8 | "lib/utils" 9 | ] 10 | } --------------------------------------------------------------------------------