diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index ca03790..af84a05 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -20,7 +20,7 @@ jobs: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} steps: - - name: Build manifest and ZIP + - name: Verify committed manifests and build ZIP run: | $ErrorActionPreference = 'Stop' $tag = $env:RELEASE_TAG @@ -58,11 +58,14 @@ jobs: throw "extension.js 的版本号必须为 $version" } - python (Join-Path $stage 'tools/generate_manifest.py') ` + # The updater reads raw tag paths, not release attachments. + # Never generate new manifests after the release tag already exists. + python (Join-Path $stage 'tools/verify_manifest.py') ` --root $stage ` - --version $version ` - --out dist/manifest.json - if ($LASTEXITCODE -ne 0) { throw '生成清单失败' } + --version $version + if ($LASTEXITCODE -ne 0) { + throw '标签内清单缺失或过期:请先生成并提交清单,再创建新版本标签' + } $manifestPath = Join-Path $stage 'dist/manifest.json' $manifest = Get-Content $manifestPath -Raw | @@ -98,6 +101,10 @@ jobs: ) for name in component["files"]: archive.write(root / name, folder + name) + for key in ("manifest", "legacy_manifest"): + receipt = descriptor.get(key) + if receipt: + archive.write(root / receipt, folder + receipt) archive.write(root / "dist/manifest.json", folder + "manifest.json") '@ | python - $stage $package if ($LASTEXITCODE -ne 0) { throw '打包失败' } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4896f9c..c37d5be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -76,6 +76,10 @@ jobs: --out dist/manifest.json if ($LASTEXITCODE -ne 0) { throw '生成清单失败' } + python (Join-Path $source 'tools/verify_manifest.py') ` + --root $source --version $version + if ($LASTEXITCODE -ne 0) { throw '发布前清单校验失败' } + $manifestPath = Join-Path $source 'dist/manifest.json' $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json @@ -128,6 +132,10 @@ jobs: Expand-Archive -LiteralPath $snapshot ` -DestinationPath $stage + python (Join-Path $stage 'tools/verify_manifest.py') ` + --root $stage --version $version + if ($LASTEXITCODE -ne 0) { throw '标签源码与清单不一致,停止发布' } + $manifestPath = Join-Path $stage 'dist/manifest.json' $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json @@ -158,6 +166,10 @@ jobs: ) for name in component["files"]: archive.write(root / name, folder + name) + for key in ("manifest", "legacy_manifest"): + receipt = descriptor.get(key) + if receipt: + archive.write(root / receipt, folder + receipt) archive.write(root / "dist/manifest.json", folder + "manifest.json") '@ | python - $stage $package if ($LASTEXITCODE -ne 0) { throw '打包失败' } diff --git a/.gitignore b/.gitignore index 4087208..f6cabe9 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,30 @@ tools/*.mjs /Slay the Spire 2 *.mjs -*.cjs \ No newline at end of file +*.cjs + +# Local updater downloads and runtime logs +/.update_tmp/ +/mengsan/logs/ +*.log + +# Local credentials; keep shareable environment templates +.env +.env.* +!.env.example +!.env.sample +!.env.template + +# Dependency and interpreter caches +node_modules/ +__pycache__/ +*.pyc + +# Editor temporary files and OS metadata +*.tmp +*.bak +*.swp +*.swo +.DS_Store +Thumbs.db +Desktop.ini diff --git a/dist/manifest.json b/dist/manifest.json index 983b44a..94e3016 100644 --- a/dist/manifest.json +++ b/dist/manifest.json @@ -1,18 +1,16 @@ { - "version": "2.0.1.1", - "generated_at": "2026-08-28T19:09:32.815143+00:00", + "version": "2.0.1.1-rc.1", + "generated_at": "2026-10-04T11:24:22.012885+00:00", "algorithm": "sha256", - "file_count": 282, - "folder_sha256": "09e932678b899ba89b5871fde641ab1f4e49d93082802ade6437b98065eca907", + "update_channels": true, + "file_count": 278, + "size_bytes": 55481243, + "folder_sha256": "4e8e4d9e1f1f51c4687aa26b055887479a5ea48d6bfac1a149a3ed10259e8c49", "core": [ "extension.js", "menu.js", "function.js", "update.js", - "mengsan/mode.js", - "mengsan/config.js", - "mengsan/state.js", - "mengsan/style.css", "tianshu/extension.js", "tianshu/config.js", "pve/boss/index.js", @@ -402,32 +400,16 @@ "sha256": "356aa9473ab72ad43850c13451fd15a25c6ce2d93678cd0dcba5568d10443b40" }, "extension.js": { - "size": 14743, - "sha256": "16cbdcc2f8e2cdfab03a0fb6f80de865aa709d7058dbbbbcd5d3ccd523907cd2" + "size": 15464, + "sha256": "f59c38efab9c337b7f8938e1f71034a2e58ea8b5b00470f5ef2ec61e26a9b5e4" }, "function.js": { "size": 33746, "sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909" }, - "mengsan/config.js": { - "size": 2211, - "sha256": "1283490af94dc3429330f6f7c192b1492b52d686018f2d68cb8482c0bc04e549" - }, - "mengsan/mode.js": { - "size": 22867, - "sha256": "88a707ac6b59160e9735770d649d8d1e5c695298628c924306b824cef7e1cc27" - }, - "mengsan/state.js": { - "size": 8175, - "sha256": "1096d195396f5a0fbe15c7d3627834cb3e415a5878c411eb41d9897af9abf46d" - }, - "mengsan/style.css": { - "size": 6859, - "sha256": "fdce7bb71f7290254d984a35415e5012f829ae9992765d1f3282f35528c1b92b" - }, "menu.js": { - "size": 22235, - "sha256": "0920561e2b0214239dc81b1a54c955b96ea0750e57fa42f3b0e8935035c6fc84" + "size": 25104, + "sha256": "d894ef33889ecf1233e1107395456985da4e71a9f679a2c3a493c7c2fa57f8d7" }, "pve/audio/dead/十二生肖/chenlong.mp3": { "size": 68757, @@ -1302,24 +1284,24 @@ "sha256": "15adcfb80bd22035c604703005ae22d1721eeafcc0a1ee09c89c73713cd3d03a" }, "tianshu/bondSkills.js": { - "size": 41604, - "sha256": "13fcda5805a7643144c55ae922f2406db2cba6e9822e086f4763f16063763e01" + "size": 41588, + "sha256": "0b5a1ec26d8a8881057aa1fc8d95fecaf06fa6753d23c99a4978de83f8c44ec6" }, "tianshu/config.js": { "size": 29707, "sha256": "9b331adca63ff4b43dc06f707c3bd728d3ce0269b1c1f4cedec5e42281f13ff0" }, "tianshu/extension.js": { - "size": 70478, - "sha256": "ff88e89a4067f18e10d1fbce72edc45aa89c95ee59e127520f5b2bed93023b98" + "size": 76830, + "sha256": "ae2585a6425cbcdf7cd2dee0f1151f5112fb8cfd4e8f6e089c6827774d6dbf9b" }, "tianshu/shuYing_Skill_Xvxiang1.mp3": { "size": 15451, "sha256": "41914d7d542ddfb93c89aefffc245869d63a9fb2f52293df8e6667009832afd8" }, "update.js": { - "size": 24145, - "sha256": "7594c68dd1308de5fd13ea7e76fde665a296400a7505b32054bf0d149986bb05" + "size": 60005, + "sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7" }, "update.txt": { "size": 3953, @@ -1588,43 +1570,18 @@ }, "extension.js": { "type": "file", - "size": 14743, - "sha256": "16cbdcc2f8e2cdfab03a0fb6f80de865aa709d7058dbbbbcd5d3ccd523907cd2" + "size": 15464, + "sha256": "f59c38efab9c337b7f8938e1f71034a2e58ea8b5b00470f5ef2ec61e26a9b5e4" }, "function.js": { "type": "file", "size": 33746, "sha256": "df1aa3102a214986723af365da1029485680519d355a1ff88973eb36f8a6f909" }, - "mengsan": { - "type": "directory", - "children": { - "config.js": { - "type": "file", - "size": 2211, - "sha256": "1283490af94dc3429330f6f7c192b1492b52d686018f2d68cb8482c0bc04e549" - }, - "mode.js": { - "type": "file", - "size": 22867, - "sha256": "88a707ac6b59160e9735770d649d8d1e5c695298628c924306b824cef7e1cc27" - }, - "state.js": { - "type": "file", - "size": 8175, - "sha256": "1096d195396f5a0fbe15c7d3627834cb3e415a5878c411eb41d9897af9abf46d" - }, - "style.css": { - "type": "file", - "size": 6859, - "sha256": "fdce7bb71f7290254d984a35415e5012f829ae9992765d1f3282f35528c1b92b" - } - } - }, "menu.js": { "type": "file", - "size": 22235, - "sha256": "0920561e2b0214239dc81b1a54c955b96ea0750e57fa42f3b0e8935035c6fc84" + "size": 25104, + "sha256": "d894ef33889ecf1233e1107395456985da4e71a9f679a2c3a493c7c2fa57f8d7" }, "pve": { "type": "directory", @@ -2846,8 +2803,8 @@ "children": { "bondSkills.js": { "type": "file", - "size": 41604, - "sha256": "13fcda5805a7643144c55ae922f2406db2cba6e9822e086f4763f16063763e01" + "size": 41588, + "sha256": "0b5a1ec26d8a8881057aa1fc8d95fecaf06fa6753d23c99a4978de83f8c44ec6" }, "config.js": { "type": "file", @@ -2856,8 +2813,8 @@ }, "extension.js": { "type": "file", - "size": 70478, - "sha256": "ff88e89a4067f18e10d1fbce72edc45aa89c95ee59e127520f5b2bed93023b98" + "size": 76830, + "sha256": "ae2585a6425cbcdf7cd2dee0f1151f5112fb8cfd4e8f6e089c6827774d6dbf9b" }, "shuYing_Skill_Xvxiang1.mp3": { "type": "file", @@ -2868,8 +2825,8 @@ }, "update.js": { "type": "file", - "size": 24145, - "sha256": "7594c68dd1308de5fd13ea7e76fde665a296400a7505b32054bf0d149986bb05" + "size": 60005, + "sha256": "f809d6e3ea5114f78a0773314c23b4120ccbd763b1f2f0f1f400753287af76f7" }, "update.txt": { "type": "file", @@ -3012,5 +2969,15 @@ } } } + }, + "modules": { + "mengsan": { + "title": "梦三模式", + "entry": "mengsan/register.js", + "manifest": "dist/modules/mengsan.json", + "file_count": 457, + "size_bytes": 705652979, + "legacy_manifest": "dist/modules/mengsan.legacy.json" + } } } \ No newline at end of file diff --git a/dist/modules/mengsan.json b/dist/modules/mengsan.json new file mode 100644 index 0000000..32a03e4 --- /dev/null +++ b/dist/modules/mengsan.json @@ -0,0 +1,4739 @@ +{ + "version": "2.0.1.1-rc.1", + "generated_at": "2026-10-04T11:24:22.013474+00:00", + "algorithm": "sha256", + "update_channels": true, + "file_count": 457, + "size_bytes": 705652979, + "folder_sha256": "98ee87f766429648ae7c8f1a33c186107f1b0751b0ad4cf650c1ca308043aba9", + "core": [ + "mengsan/register.js" + ], + "assets": [ + "mengsan/assets/card-overlays/infection-sprite.webp", + "mengsan/assets/cards/mengsan_baozou.png", + "mengsan/assets/cards/mengsan_colorless_alchemize.png", + "mengsan/assets/cards/mengsan_colorless_anointed.png", + "mengsan/assets/cards/mengsan_colorless_automation.png", + "mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png", + "mengsan/assets/cards/mengsan_colorless_beat_down.png", + "mengsan/assets/cards/mengsan_colorless_believe_in_you.png", + "mengsan/assets/cards/mengsan_colorless_bolas.png", + "mengsan/assets/cards/mengsan_colorless_calamity.png", + "mengsan/assets/cards/mengsan_colorless_catastrophe.png", + "mengsan/assets/cards/mengsan_colorless_coordinate.png", + "mengsan/assets/cards/mengsan_colorless_dark_shackles.png", + "mengsan/assets/cards/mengsan_colorless_discovery.png", + "mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png", + "mengsan/assets/cards/mengsan_colorless_entropy.png", + "mengsan/assets/cards/mengsan_colorless_equilibrium.png", + "mengsan/assets/cards/mengsan_colorless_eternal_armor.png", + "mengsan/assets/cards/mengsan_colorless_fasten.png", + "mengsan/assets/cards/mengsan_colorless_finesse.png", + "mengsan/assets/cards/mengsan_colorless_fisticuffs.png", + "mengsan/assets/cards/mengsan_colorless_flash_of_steel.png", + "mengsan/assets/cards/mengsan_colorless_gang_up.png", + "mengsan/assets/cards/mengsan_colorless_gold_axe.png", + "mengsan/assets/cards/mengsan_colorless_hand_of_greed.png", + "mengsan/assets/cards/mengsan_colorless_hidden_gem.png", + "mengsan/assets/cards/mengsan_colorless_huddle_up.png", + "mengsan/assets/cards/mengsan_colorless_impatience.png", + "mengsan/assets/cards/mengsan_colorless_intercept.png", + "mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png", + "mengsan/assets/cards/mengsan_colorless_jackpot.png", + "mengsan/assets/cards/mengsan_colorless_knockdown.png", + "mengsan/assets/cards/mengsan_colorless_lift.png", + "mengsan/assets/cards/mengsan_colorless_master_of_strategy.png", + "mengsan/assets/cards/mengsan_colorless_mayhem.png", + "mengsan/assets/cards/mengsan_colorless_mimic.png", + "mengsan/assets/cards/mengsan_colorless_mind_blast.png", + "mengsan/assets/cards/mengsan_colorless_nostalgia.png", + "mengsan/assets/cards/mengsan_colorless_omnislice.png", + "mengsan/assets/cards/mengsan_colorless_panache.png", + "mengsan/assets/cards/mengsan_colorless_panic_button.png", + "mengsan/assets/cards/mengsan_colorless_prep_time.png", + "mengsan/assets/cards/mengsan_colorless_production.png", + "mengsan/assets/cards/mengsan_colorless_prolong.png", + "mengsan/assets/cards/mengsan_colorless_prowess.png", + "mengsan/assets/cards/mengsan_colorless_purity.png", + "mengsan/assets/cards/mengsan_colorless_rally.png", + "mengsan/assets/cards/mengsan_colorless_rend.png", + "mengsan/assets/cards/mengsan_colorless_restlessness.png", + "mengsan/assets/cards/mengsan_colorless_rolling_boulder.png", + "mengsan/assets/cards/mengsan_colorless_salvo.png", + "mengsan/assets/cards/mengsan_colorless_scrawl.png", + "mengsan/assets/cards/mengsan_colorless_secret_technique.png", + "mengsan/assets/cards/mengsan_colorless_secret_weapon.png", + "mengsan/assets/cards/mengsan_colorless_seeker_strike.png", + "mengsan/assets/cards/mengsan_colorless_shockwave.png", + "mengsan/assets/cards/mengsan_colorless_splash.png", + "mengsan/assets/cards/mengsan_colorless_stratagem.png", + "mengsan/assets/cards/mengsan_colorless_tag_team.png", + "mengsan/assets/cards/mengsan_colorless_the_ball.png", + "mengsan/assets/cards/mengsan_colorless_the_bomb.png", + "mengsan/assets/cards/mengsan_colorless_the_gambit.png", + "mengsan/assets/cards/mengsan_colorless_thinking_ahead.png", + "mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png", + "mengsan/assets/cards/mengsan_colorless_ultimate_defend.png", + "mengsan/assets/cards/mengsan_colorless_ultimate_strike.png", + "mengsan/assets/cards/mengsan_colorless_volley.png", + "mengsan/assets/cards/mengsan_curse_ascenders_bane.png", + "mengsan/assets/cards/mengsan_curse_bad_luck.png", + "mengsan/assets/cards/mengsan_curse_clumsy.png", + "mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png", + "mengsan/assets/cards/mengsan_curse_debt.png", + "mengsan/assets/cards/mengsan_curse_decay.png", + "mengsan/assets/cards/mengsan_curse_doubt.png", + "mengsan/assets/cards/mengsan_curse_enthralled.png", + "mengsan/assets/cards/mengsan_curse_folly.png", + "mengsan/assets/cards/mengsan_curse_greed.png", + "mengsan/assets/cards/mengsan_curse_guilty.png", + "mengsan/assets/cards/mengsan_curse_injury.png", + "mengsan/assets/cards/mengsan_curse_normality.png", + "mengsan/assets/cards/mengsan_curse_poor_sleep.png", + "mengsan/assets/cards/mengsan_curse_regret.png", + "mengsan/assets/cards/mengsan_curse_shame.png", + "mengsan/assets/cards/mengsan_curse_spore_mind.png", + "mengsan/assets/cards/mengsan_curse_writhe.png", + "mengsan/assets/cards/mengsan_dazed_shuying.png", + "mengsan/assets/cards/mengsan_event_abundance.png", + "mengsan/assets/cards/mengsan_event_apotheosis.png", + "mengsan/assets/cards/mengsan_event_apparition.png", + "mengsan/assets/cards/mengsan_event_brightest_flame.png", + "mengsan/assets/cards/mengsan_event_byrd_swoop.png", + "mengsan/assets/cards/mengsan_event_enlightenment.png", + "mengsan/assets/cards/mengsan_event_exterminate.png", + "mengsan/assets/cards/mengsan_event_feeding_frenzy.png", + "mengsan/assets/cards/mengsan_event_mad_science_chaos.png", + "mengsan/assets/cards/mengsan_event_mad_science_choking.png", + "mengsan/assets/cards/mengsan_event_mad_science_curious.png", + "mengsan/assets/cards/mengsan_event_mad_science_energized.png", + "mengsan/assets/cards/mengsan_event_mad_science_expertise.png", + "mengsan/assets/cards/mengsan_event_mad_science_improvement.png", + "mengsan/assets/cards/mengsan_event_mad_science_sapping.png", + "mengsan/assets/cards/mengsan_event_mad_science_violence.png", + "mengsan/assets/cards/mengsan_event_mad_science_wisdom.png", + "mengsan/assets/cards/mengsan_event_maul.png", + "mengsan/assets/cards/mengsan_event_metamorphosis.png", + "mengsan/assets/cards/mengsan_event_neows_fury.png", + "mengsan/assets/cards/mengsan_event_peck.png", + "mengsan/assets/cards/mengsan_event_relax.png", + "mengsan/assets/cards/mengsan_event_squash.png", + "mengsan/assets/cards/mengsan_event_toric_toughness.png", + "mengsan/assets/cards/mengsan_event_whistle.png", + "mengsan/assets/cards/mengsan_event_wish.png", + "mengsan/assets/cards/mengsan_fangxue.png", + "mengsan/assets/cards/mengsan_fangyu.png", + "mengsan/assets/cards/mengsan_feijianhuixuanbiao.png", + "mengsan/assets/cards/mengsan_fennu.png", + "mengsan/assets/cards/mengsan_ic_ashen_strike.png", + "mengsan/assets/cards/mengsan_ic_barricade.png", + "mengsan/assets/cards/mengsan_ic_battle_ready.png", + "mengsan/assets/cards/mengsan_ic_battle_trance.png", + "mengsan/assets/cards/mengsan_ic_blaze.png", + "mengsan/assets/cards/mengsan_ic_bludgeon.png", + "mengsan/assets/cards/mengsan_ic_bodyguard.png", + "mengsan/assets/cards/mengsan_ic_boulder.png", + "mengsan/assets/cards/mengsan_ic_brand.png", + "mengsan/assets/cards/mengsan_ic_break.png", + "mengsan/assets/cards/mengsan_ic_bully.png", + "mengsan/assets/cards/mengsan_ic_burning_pact.png", + "mengsan/assets/cards/mengsan_ic_clash.png", + "mengsan/assets/cards/mengsan_ic_colossus.png", + "mengsan/assets/cards/mengsan_ic_conflagration.png", + "mengsan/assets/cards/mengsan_ic_corruption.png", + "mengsan/assets/cards/mengsan_ic_crimson_mantle.png", + "mengsan/assets/cards/mengsan_ic_cruelty.png", + "mengsan/assets/cards/mengsan_ic_dark_embrace.png", + "mengsan/assets/cards/mengsan_ic_demon_form.png", + "mengsan/assets/cards/mengsan_ic_demonic_shield.png", + "mengsan/assets/cards/mengsan_ic_dismantle.png", + "mengsan/assets/cards/mengsan_ic_dominate.png", + "mengsan/assets/cards/mengsan_ic_drum_of_battle.png", + "mengsan/assets/cards/mengsan_ic_dual_wield.png", + "mengsan/assets/cards/mengsan_ic_eager.png", + "mengsan/assets/cards/mengsan_ic_entrench.png", + "mengsan/assets/cards/mengsan_ic_evil_eye.png", + "mengsan/assets/cards/mengsan_ic_feed.png", + "mengsan/assets/cards/mengsan_ic_feel_no_pain.png", + "mengsan/assets/cards/mengsan_ic_ferocity.png", + "mengsan/assets/cards/mengsan_ic_fiend_fire.png", + "mengsan/assets/cards/mengsan_ic_fight_me.png", + "mengsan/assets/cards/mengsan_ic_flame_barrier.png", + "mengsan/assets/cards/mengsan_ic_forgotten_ritual.png", + "mengsan/assets/cards/mengsan_ic_grapple.png", + "mengsan/assets/cards/mengsan_ic_hellraiser.png", + "mengsan/assets/cards/mengsan_ic_hemokinesis.png", + "mengsan/assets/cards/mengsan_ic_howl_from_beyond.png", + "mengsan/assets/cards/mengsan_ic_impervious.png", + "mengsan/assets/cards/mengsan_ic_infernal_blade.png", + "mengsan/assets/cards/mengsan_ic_inferno.png", + "mengsan/assets/cards/mengsan_ic_inflame.png", + "mengsan/assets/cards/mengsan_ic_juggernaut.png", + "mengsan/assets/cards/mengsan_ic_juggle.png", + "mengsan/assets/cards/mengsan_ic_midnight.png", + "mengsan/assets/cards/mengsan_ic_not_yet.png", + "mengsan/assets/cards/mengsan_ic_offering.png", + "mengsan/assets/cards/mengsan_ic_pacts_end.png", + "mengsan/assets/cards/mengsan_ic_pillage.png", + "mengsan/assets/cards/mengsan_ic_primal_force.png", + "mengsan/assets/cards/mengsan_ic_pyre.png", + "mengsan/assets/cards/mengsan_ic_rage.png", + "mengsan/assets/cards/mengsan_ic_rally.png", + "mengsan/assets/cards/mengsan_ic_relentless_assault.png", + "mengsan/assets/cards/mengsan_ic_rupture.png", + "mengsan/assets/cards/mengsan_ic_second_wind.png", + "mengsan/assets/cards/mengsan_ic_stampede.png", + "mengsan/assets/cards/mengsan_ic_stoke.png", + "mengsan/assets/cards/mengsan_ic_stomp.png", + "mengsan/assets/cards/mengsan_ic_stone_armor.png", + "mengsan/assets/cards/mengsan_ic_taunt.png", + "mengsan/assets/cards/mengsan_ic_tear_asunder.png", + "mengsan/assets/cards/mengsan_ic_thrash.png", + "mengsan/assets/cards/mengsan_ic_twin_strike.png", + "mengsan/assets/cards/mengsan_ic_unleash.png", + "mengsan/assets/cards/mengsan_ic_unshakable.png", + "mengsan/assets/cards/mengsan_ic_uppercut.png", + "mengsan/assets/cards/mengsan_infection_shuying.png", + "mengsan/assets/cards/mengsan_jianbingdaji.png", + "mengsan/assets/cards/mengsan_jianyi.png", + "mengsan/assets/cards/mengsan_pomie.png", + "mengsan/assets/cards/mengsan_qiling.png", + "mengsan/assets/cards/mengsan_quanshenzhuangji.png", + "mengsan/assets/cards/mengsan_rongrongzhiquan.png", + "mengsan/assets/cards/mengsan_shandianpili.png", + "mengsan/assets/cards/mengsan_shuangchongdaji.png", + "mengsan/assets/cards/mengsan_slimed_shuying.png", + "mengsan/assets/cards/mengsan_songjianwushi.png", + "mengsan/assets/cards/mengsan_status_beckon.png", + "mengsan/assets/cards/mengsan_status_burn.png", + "mengsan/assets/cards/mengsan_status_debris.png", + "mengsan/assets/cards/mengsan_status_deprecated_card.png", + "mengsan/assets/cards/mengsan_status_frantic_escape.png", + "mengsan/assets/cards/mengsan_status_soot.png", + "mengsan/assets/cards/mengsan_status_toxic.png", + "mengsan/assets/cards/mengsan_status_void.png", + "mengsan/assets/cards/mengsan_status_wither.png", + "mengsan/assets/cards/mengsan_tiezhanbo.png", + "mengsan/assets/cards/mengsan_touchui.png", + "mengsan/assets/cards/mengsan_tupo.png", + "mengsan/assets/cards/mengsan_wanmeidaji.png", + "mengsan/assets/cards/mengsan_wound_shuying.png", + "mengsan/assets/cards/mengsan_wuzhuang.png", + "mengsan/assets/cards/mengsan_xuanfengzhan.png", + "mengsan/assets/cards/mengsan_xueqiang.png", + "mengsan/assets/cards/mengsan_yuanhen.png", + "mengsan/assets/cards/mengsan_yubeidaji.png", + "mengsan/assets/cards/mengsan_yujin.png", + "mengsan/assets/cards/mengsan_zhanli.png", + "mengsan/assets/cards/mengsan_zhongsha.png", + "mengsan/assets/cards/wound-original.png", + "mengsan/assets/intent/Intent_attack_1.png", + "mengsan/assets/intent/Intent_attack_2.png", + "mengsan/assets/intent/Intent_attack_3.png", + "mengsan/assets/intent/Intent_attack_4.png", + "mengsan/assets/intent/Intent_attack_5.png", + "mengsan/assets/intent/Intent_buff.png", + "mengsan/assets/intent/Intent_card_debuff.png", + "mengsan/assets/intent/Intent_death_blow.png", + "mengsan/assets/intent/Intent_debuff.png", + "mengsan/assets/intent/Intent_defend.png", + "mengsan/assets/intent/Intent_escape.png", + "mengsan/assets/intent/Intent_heal.png", + "mengsan/assets/intent/Intent_sleep.png", + "mengsan/assets/intent/Intent_status_card.png", + "mengsan/assets/intent/Intent_stun.png", + "mengsan/assets/intent/Intent_summon.png", + "mengsan/assets/intent/Intent_unknown.png", + "mengsan/assets/portraits/bygone-effigy-forest-portrait.png", + "mengsan/assets/portraits/bygone-effigy-original.png", + "mengsan/assets/portraits/byrdonis-forest-portrait.png", + "mengsan/assets/portraits/byrdonis-original.png", + "mengsan/assets/portraits/ceremonial-beast-forest-portrait.png", + "mengsan/assets/portraits/ceremonial-beast-original.png", + "mengsan/assets/portraits/cubex-construct-forest-portrait.png", + "mengsan/assets/portraits/cubex-construct-original.webp", + "mengsan/assets/portraits/eye-with-teeth-forest-portrait.png", + "mengsan/assets/portraits/eye-with-teeth.webp", + "mengsan/assets/portraits/flyconid-portrait.png", + "mengsan/assets/portraits/fogmog-and-eye-forest-portrait.png", + "mengsan/assets/portraits/fogmog-forest-portrait.png", + "mengsan/assets/portraits/fogmog.webp", + "mengsan/assets/portraits/fuzzy-wurm-crawler-forest-portrait.png", + "mengsan/assets/portraits/fuzzy-wurm-crawler-original.webp", + "mengsan/assets/portraits/guanyu-portrait.jpg", + "mengsan/assets/portraits/inklet-forest-portrait.png", + "mengsan/assets/portraits/inklet-original.png", + "mengsan/assets/portraits/kin-follower-forest-portrait.png", + "mengsan/assets/portraits/kin-follower-original.webp", + "mengsan/assets/portraits/kin-priest-forest-portrait.png", + "mengsan/assets/portraits/kin-priest-original.png", + "mengsan/assets/portraits/leaf-slime-m-forest-portrait.png", + "mengsan/assets/portraits/leaf-slime-m-original.png", + "mengsan/assets/portraits/leaf-slime-s-forest-portrait.png", + "mengsan/assets/portraits/leaf-slime-s-original.png", + "mengsan/assets/portraits/liubei-portrait.jpg", + "mengsan/assets/portraits/mawler-forest-portrait.png", + "mengsan/assets/portraits/mawler-original.webp", + "mengsan/assets/portraits/nibbit-forest-portrait.png", + "mengsan/assets/portraits/nibbit-original.webp", + "mengsan/assets/portraits/phrog-parasite-forest-portrait.png", + "mengsan/assets/portraits/phrog-parasite-original.png", + "mengsan/assets/portraits/raider-assassin-portrait.png", + "mengsan/assets/portraits/raider-axe-portrait.png", + "mengsan/assets/portraits/raider-brute-portrait.png", + "mengsan/assets/portraits/raider-crossbow-portrait.png", + "mengsan/assets/portraits/raider-tracker-portrait.png", + "mengsan/assets/portraits/shrinker-beetle-forest-portrait.png", + "mengsan/assets/portraits/shrinker-beetle-original.png", + "mengsan/assets/portraits/slithering-strangler-forest-portrait.png", + "mengsan/assets/portraits/slithering-strangler-original.png", + "mengsan/assets/portraits/snapping-jaxfruit-forest-portrait.png", + "mengsan/assets/portraits/snapping-jaxfruit-original.png", + "mengsan/assets/portraits/sushuang-zhangshiping.png", + "mengsan/assets/portraits/twig-slime-m-forest-portrait.png", + "mengsan/assets/portraits/twig-slime-m-original.png", + "mengsan/assets/portraits/twig-slime-s-forest-portrait.png", + "mengsan/assets/portraits/twig-slime-s-original.png", + "mengsan/assets/portraits/vantom-forest-portrait.png", + "mengsan/assets/portraits/vantom-original.png", + "mengsan/assets/portraits/vine-shambler-forest-portrait.png", + "mengsan/assets/portraits/vine-shambler-original-thumbnail.webp", + "mengsan/assets/portraits/wriggler-forest-portrait.png", + "mengsan/assets/portraits/wriggler-original.webp", + "mengsan/assets/portraits/zhangfei-portrait.jpg", + "mengsan/battle/battle-director.js", + "mengsan/battle/battle-flow.js", + "mengsan/battle/battle-loop.js", + "mengsan/battle/battle-resources.js", + "mengsan/battle/battle-session.js", + "mengsan/battle/battle-settlement.js", + "mengsan/battle/combat-rules.js", + "mengsan/battle/death-blow.js", + "mengsan/battle/engine-session.js", + "mengsan/battle/intent-damage.js", + "mengsan/battle/intent-effects.js", + "mengsan/battle/skill-teardown.js", + "mengsan/battle/stun-intent.js", + "mengsan/bonds/battle.js", + "mengsan/bonds/definitions.js", + "mengsan/bonds/intents.js", + "mengsan/bonds/state.js", + "mengsan/cards/affixes.js", + "mengsan/cards/battle-card-copy.js", + "mengsan/cards/battle-card-reclaim.js", + "mengsan/cards/battle-energy.js", + "mengsan/cards/block-draw.js", + "mengsan/cards/card-data.js", + "mengsan/cards/card-definitions.js", + "mengsan/cards/card-pools.js", + "mengsan/cards/classification-types.js", + "mengsan/cards/description.js", + "mengsan/cards/hand-exhaust.js", + "mengsan/cards/hand-upgrade.js", + "mengsan/cards/havoc-play.js", + "mengsan/cards/infection-card.js", + "mengsan/cards/intrinsic-affixes.js", + "mengsan/cards/ironclad-advanced-data.js", + "mengsan/cards/ironclad-data.js", + "mengsan/cards/ironclad-hooks.js", + "mengsan/cards/ironclad.js", + "mengsan/cards/life-loss-block.js", + "mengsan/cards/mode-cards.js", + "mengsan/cards/packs/colorless/data.js", + "mengsan/cards/packs/colorless/index.js", + "mengsan/cards/packs/colorless/rewards.js", + "mengsan/cards/packs/curse/data.js", + "mengsan/cards/packs/data.js", + "mengsan/cards/packs/event/data.js", + "mengsan/cards/packs/index.js", + "mengsan/cards/packs/liubei/advanced-data.js", + "mengsan/cards/packs/liubei/advanced.js", + "mengsan/cards/packs/liubei/data.js", + "mengsan/cards/packs/liubei/index.js", + "mengsan/cards/packs/liubei/legacy.js", + "mengsan/cards/packs/liubei/metadata.js", + "mengsan/cards/packs/liubei/names.js", + "mengsan/cards/packs/liubei/rewards.js", + "mengsan/cards/packs/liubei/runtime.js", + "mengsan/cards/packs/liubei/traits.js", + "mengsan/cards/packs/rewards.js", + "mengsan/cards/packs/shared/data.js", + "mengsan/cards/packs/shared/runtime.js", + "mengsan/cards/packs/status/data.js", + "mengsan/cards/personal-piles.js", + "mengsan/cards/rampage.js", + "mengsan/cards/random-hand-exhaust.js", + "mengsan/cards/rarity-data.js", + "mengsan/cards/rarity.js", + "mengsan/cards/retired-cards.js", + "mengsan/cards/shared-battle-end.js", + "mengsan/cards/shared-hooks.js", + "mengsan/cards/slimed-card.js", + "mengsan/cards/spite.js", + "mengsan/cards/starting-deck.js", + "mengsan/cards/status-cards.js", + "mengsan/cards/strike-counter.js", + "mengsan/cards/temporary-strength.js", + "mengsan/cards/upgrades.js", + "mengsan/cards/whirlwind.js", + "mengsan/cards/wither-card.js", + "mengsan/cards/wound-card.js", + "mengsan/config.js", + "mengsan/content/acts/act1/encounters.js", + "mengsan/content/acts/act1/index.js", + "mengsan/content/acts/act1/inklet-group.js", + "mengsan/content/acts/act1/kin-group.js", + "mengsan/content/acts/act1/map.js", + "mengsan/content/acts/act1/nibbit-pair.js", + "mengsan/content/acts/act1/raider-trio.js", + "mengsan/content/acts/act1/rewards.js", + "mengsan/content/acts/act1/stories/forest-ambush.js", + "mengsan/content/acts/act1/stories/liubei-opening.js", + "mengsan/content/acts/act2/index.js", + "mengsan/content/acts/act2/map.js", + "mengsan/content/acts/act3/index.js", + "mengsan/content/acts/act3/map.js", + "mengsan/content/encounters.js", + "mengsan/content/registry.js", + "mengsan/content/scenario-characters.js", + "mengsan/content/shared/index.js", + "mengsan/content/shared/rewards.js", + "mengsan/mode.js", + "mengsan/monsters/actions.js", + "mengsan/monsters/artifact-status.js", + "mengsan/monsters/beast-intent.js", + "mengsan/monsters/byrdonis-intent.js", + "mengsan/monsters/crawler-intent.js", + "mengsan/monsters/cubex-intent.js", + "mengsan/monsters/effigy-intent.js", + "mengsan/monsters/effigy-slow.js", + "mengsan/monsters/flyconid-intent.js", + "mengsan/monsters/fogmog-battle.js", + "mengsan/monsters/fogmog-intent.js", + "mengsan/monsters/inklet-intent.js", + "mengsan/monsters/jaxfruit-intent.js", + "mengsan/monsters/kin-intent.js", + "mengsan/monsters/leafmedium-intent.js", + "mengsan/monsters/leafslime-goop.js", + "mengsan/monsters/leafslime-intent.js", + "mengsan/monsters/mawler-intent.js", + "mengsan/monsters/monster.js", + "mengsan/monsters/nibbit-intent.js", + "mengsan/monsters/phrog-battle.js", + "mengsan/monsters/phrog-intent.js", + "mengsan/monsters/raider-intent.js", + "mengsan/monsters/shrinker-intent.js", + "mengsan/monsters/shrinker-status.js", + "mengsan/monsters/strangler-constrict.js", + "mengsan/monsters/strangler-intent.js", + "mengsan/monsters/twigmedium-intent.js", + "mengsan/monsters/twigslime-intent.js", + "mengsan/monsters/vantom-intent.js", + "mengsan/monsters/vine-intent.js", + "mengsan/monsters/vine-tangled.js", + "mengsan/progression/dialogue-choice.js", + "mengsan/progression/encounter-progress.js", + "mengsan/progression/map-checkpoint.js", + "mengsan/progression/mode-storage.js", + "mengsan/progression/reward-package.js", + "mengsan/progression/reward.js", + "mengsan/progression/state.js", + "mengsan/progression/story.js", + "mengsan/relics/battle.js", + "mengsan/relics/combat.js", + "mengsan/relics/definitions.js", + "mengsan/relics/rooms.js", + "mengsan/ui/bonds.js", + "mengsan/ui/card-library.js", + "mengsan/ui/dialogue.js", + "mengsan/ui/energy-display.js", + "mengsan/ui/flow-ui.js", + "mengsan/ui/gm-manager.js", + "mengsan/ui/hand/adapter.js", + "mengsan/ui/hand/diagnostics.js", + "mengsan/ui/hand/fan.js", + "mengsan/ui/hand/index.js", + "mengsan/ui/hand/input.js", + "mengsan/ui/hand/overlays.js", + "mengsan/ui/hand/style.css", + "mengsan/ui/intent-display.js", + "mengsan/ui/map/index.js", + "mengsan/ui/map/renderer.js", + "mengsan/ui/navigation.js", + "mengsan/ui/relics.js", + "mengsan/ui/reward-package.js", + "mengsan/ui/reward-ui.js", + "mengsan/ui/style.css", + "mengsan/ui/virtual-card-grid.js" + ], + "remove": [], + "removeDirectories": [], + "files": { + "mengsan/assets/card-overlays/infection-sprite.webp": { + "size": 677174, + "sha256": "96ec222b2cb1a76d524ba5a947a21fb140944409ef20acb453c645cee520fea4" + }, + "mengsan/assets/cards/mengsan_baozou.png": { + "size": 288919, + "sha256": "ccfea8a1595a41528d93de7b90d78302e035dc42a058bd73a3d314e39c777306" + }, + "mengsan/assets/cards/mengsan_colorless_alchemize.png": { + "size": 3127713, + "sha256": "030695f8c986e22f1ec74496da907f050c3439f9dd58db362af08da92af20164" + }, + "mengsan/assets/cards/mengsan_colorless_anointed.png": { + "size": 3086399, + "sha256": "3a68cb906e29c366bfb8beab3958276050e24f9dc77d854ae71debe095446838" + }, + "mengsan/assets/cards/mengsan_colorless_automation.png": { + "size": 3162166, + "sha256": "00b0f61f95fbd9118c3ecce4c68981ad59c970805f3c5fd4e0637ccaa704f0f5" + }, + "mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png": { + "size": 3014849, + "sha256": "038bee1de6075fdcd9a2b1269ba3cd040e76727a6a3b2d36e41e1644bb631adc" + }, + "mengsan/assets/cards/mengsan_colorless_beat_down.png": { + "size": 3261861, + "sha256": "476af9e96f7c3531ea0251ddb4f3ff87801054960ded214aba5f8656473a0473" + }, + "mengsan/assets/cards/mengsan_colorless_believe_in_you.png": { + "size": 3193359, + "sha256": "43519b09e365e4e91f6771fa09943969ea96ffc05300ea17a0019bdc7ab32943" + }, + "mengsan/assets/cards/mengsan_colorless_bolas.png": { + "size": 3203088, + "sha256": "404d32ecb50eee1570ce743c43457c589839c98c7a63b746225bd0d2fd866c8e" + }, + "mengsan/assets/cards/mengsan_colorless_calamity.png": { + "size": 3043506, + "sha256": "2832971475e0cd42f28ce19009b4dc72ac13fdfd21c84bd94ce3ffe0ca801c25" + }, + "mengsan/assets/cards/mengsan_colorless_catastrophe.png": { + "size": 3311872, + "sha256": "ea81434a87f2277cbff5403a46399f7e73d1c7007cf3a60a6521a277c6e9edf6" + }, + "mengsan/assets/cards/mengsan_colorless_coordinate.png": { + "size": 3277184, + "sha256": "f05c1f215783e1fe97f920931afd20a8a4686036088da6f34212231a472db2a2" + }, + "mengsan/assets/cards/mengsan_colorless_dark_shackles.png": { + "size": 3036771, + "sha256": "b43aafcbe4882f3a4ff4881615f409ac193ea35156956ec9f4d2150de7f8c2bb" + }, + "mengsan/assets/cards/mengsan_colorless_discovery.png": { + "size": 3231144, + "sha256": "1d6c186eccdb7d1850c064ad25eb309cba3d8fe32238248813e92c47424945b9" + }, + "mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png": { + "size": 3354223, + "sha256": "8497a4026aac2aa838251157cf53ee3cd5d7cbd420ab436927985c94332033e9" + }, + "mengsan/assets/cards/mengsan_colorless_entropy.png": { + "size": 3347868, + "sha256": "9672138ec7f8f5360efe7198378e2aa36898299c199c41727e25db0efb7a6632" + }, + "mengsan/assets/cards/mengsan_colorless_equilibrium.png": { + "size": 3135158, + "sha256": "109a7369ec0f3a473d8ffec5e527dc1b7c22d929c90547bbc3cc4db333bc8aa6" + }, + "mengsan/assets/cards/mengsan_colorless_eternal_armor.png": { + "size": 3167981, + "sha256": "3b532054c25452f093015ff92cb0bf4e15085baf367f42e56533992e6a6339cf" + }, + "mengsan/assets/cards/mengsan_colorless_fasten.png": { + "size": 3134484, + "sha256": "62c627389524fd5aa90c30e2e37e70365946b61fb162f5f1084cb0a677b7ad33" + }, + "mengsan/assets/cards/mengsan_colorless_finesse.png": { + "size": 3200227, + "sha256": "f0c3ce167fc36bdade027f9c89cfda0aebe2857e0e442ab8da83dac0062cb88b" + }, + "mengsan/assets/cards/mengsan_colorless_fisticuffs.png": { + "size": 2986131, + "sha256": "3424c53ede2dd31def9f4b36290b403e976b00e6d4acf9d1090a1ad3bff50355" + }, + "mengsan/assets/cards/mengsan_colorless_flash_of_steel.png": { + "size": 3254260, + "sha256": "9638fbd12bdae0d856c0d7c514b88668d1925d53ff8e0b18f46cdf6363c6e41d" + }, + "mengsan/assets/cards/mengsan_colorless_gang_up.png": { + "size": 3454683, + "sha256": "151638d8f635a72652e5ed9653647a5c4f24b4285032c17d9d85441f4dd1c424" + }, + "mengsan/assets/cards/mengsan_colorless_gold_axe.png": { + "size": 3241447, + "sha256": "5a83405d841ff7b5c40d0aaf37507b3274ae2e98e75070bec1f742da11bb10e4" + }, + "mengsan/assets/cards/mengsan_colorless_hand_of_greed.png": { + "size": 3204239, + "sha256": "e3f35625547fde12c1646385b1def672d18b6b03373898bb44c3ff535c307811" + }, + "mengsan/assets/cards/mengsan_colorless_hidden_gem.png": { + "size": 3250322, + "sha256": "600be77b409fef66f260c8bdc0c25c46148064977f3cb5c8cb8e023b4da679ed" + }, + "mengsan/assets/cards/mengsan_colorless_huddle_up.png": { + "size": 3133981, + "sha256": "2783d26a011a442f1ee7ba2b8f50bb889886eab91685e5fff9d6d38cb0656da0" + }, + "mengsan/assets/cards/mengsan_colorless_impatience.png": { + "size": 3249719, + "sha256": "00f00214e278a79fe138941153a59843760b2b0f3280d192628619f75f541d79" + }, + "mengsan/assets/cards/mengsan_colorless_intercept.png": { + "size": 3205359, + "sha256": "d2dbb2979f3944cdd18cba6147e989ee3f78f8d6ed91665dfa1b3f7fb97e6352" + }, + "mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png": { + "size": 3402938, + "sha256": "a9b06fe1f21f3a86d5cb16a0b312c3d6788c62b5bbabf75a844de431828ecdd6" + }, + "mengsan/assets/cards/mengsan_colorless_jackpot.png": { + "size": 3222698, + "sha256": "c9f9dbc67abcefd6576ff936ece05c274b4e682cc4b9e252b10feba78c4ba9f8" + }, + "mengsan/assets/cards/mengsan_colorless_knockdown.png": { + "size": 3328270, + "sha256": "402647d62ae54ea742243185bf70e4ad95f87469cffcebb3e35d1826985cfdcb" + }, + "mengsan/assets/cards/mengsan_colorless_lift.png": { + "size": 3150681, + "sha256": "7a57e2e134a0e7579d8ea3d1b65a551b41f5814ad2b57c73a6afd1cf3751d71f" + }, + "mengsan/assets/cards/mengsan_colorless_master_of_strategy.png": { + "size": 3113651, + "sha256": "441e69484c575409c7d8201cc39c21956111b9462aec4aa10405e5f6f3815d46" + }, + "mengsan/assets/cards/mengsan_colorless_mayhem.png": { + "size": 3221855, + "sha256": "a30d6f318aa0969c7867a31669df515f87296c4ec65e7d03bd9aeac1f65ca3f6" + }, + "mengsan/assets/cards/mengsan_colorless_mimic.png": { + "size": 3092367, + "sha256": "23e33b4d6005c9667d54d821114bad372cde5885a3dcd0b36f0373947f518aaa" + }, + "mengsan/assets/cards/mengsan_colorless_mind_blast.png": { + "size": 3146553, + "sha256": "d7596195551d4c555dfd0e0841eea31666256f1a25f0e1126a242c9fb85e3999" + }, + "mengsan/assets/cards/mengsan_colorless_nostalgia.png": { + "size": 3191912, + "sha256": "99dbe6ade64d2f09fabae292dd8b3fa3a603ad55012c4ac7ca5cf61dff4e5ee1" + }, + "mengsan/assets/cards/mengsan_colorless_omnislice.png": { + "size": 3221281, + "sha256": "d64936d9fcdbea09456eca5af79866b679532a77a03ba6f7d179d6c1225963d5" + }, + "mengsan/assets/cards/mengsan_colorless_panache.png": { + "size": 3393936, + "sha256": "154f52ded899d099c10b42a2551526c992c5b8bb013d29d76edac935780e73da" + }, + "mengsan/assets/cards/mengsan_colorless_panic_button.png": { + "size": 3161638, + "sha256": "c88fdd70d1ca36b09e1038b5bb17757621c7cedde849c52e50890c63fb7e6c13" + }, + "mengsan/assets/cards/mengsan_colorless_prep_time.png": { + "size": 3210939, + "sha256": "235350559f4b50b207907ea6217fe5a19381d297425c97862e9cb5efec556dc8" + }, + "mengsan/assets/cards/mengsan_colorless_production.png": { + "size": 3192726, + "sha256": "1290f77a24dc3a7287e61ff7ef70f2c94033915d6996c3e22e039e9669cb8651" + }, + "mengsan/assets/cards/mengsan_colorless_prolong.png": { + "size": 3059311, + "sha256": "316111895375c230029e76f1ef674e7cb08e9e58472a90b4267b182c28e9221e" + }, + "mengsan/assets/cards/mengsan_colorless_prowess.png": { + "size": 3228141, + "sha256": "bb7a67bd845a376174dbb28627bac4771454cb50bbadc24cbc2a26391d9badc8" + }, + "mengsan/assets/cards/mengsan_colorless_purity.png": { + "size": 3190337, + "sha256": "00aad33dbf174fa6e5fe3f0231ee1e1ddcfb0e13011132e2ba34179400ee33c8" + }, + "mengsan/assets/cards/mengsan_colorless_rally.png": { + "size": 3267674, + "sha256": "315913dd9c6106d1a5833eabc5f0caa88ebda9dbed72440aab2fbd46bab14bb4" + }, + "mengsan/assets/cards/mengsan_colorless_rend.png": { + "size": 3293457, + "sha256": "36a641178ee9c13eaf78cc96656e445e5147d09b07ca2b05c82e0a30c440ad26" + }, + "mengsan/assets/cards/mengsan_colorless_restlessness.png": { + "size": 2907076, + "sha256": "e1d87fa3e36dd61fd5b84779f6ef4b0f0341c3216cca763c83e6d9a1dc1fe737" + }, + "mengsan/assets/cards/mengsan_colorless_rolling_boulder.png": { + "size": 3318250, + "sha256": "d9e7faabcde18d42c6dcc4f66ab0e822a930a8fba0e2060f42d1655991cbd253" + }, + "mengsan/assets/cards/mengsan_colorless_salvo.png": { + "size": 3228759, + "sha256": "00da2f4a4bc69909cd89b187b96776d104a667ddf728d330ffd12a0aec4c222e" + }, + "mengsan/assets/cards/mengsan_colorless_scrawl.png": { + "size": 3092946, + "sha256": "72a71d758a258a37437f26106cab486e55c26a451fc19bb1abf885f0da7b8f44" + }, + "mengsan/assets/cards/mengsan_colorless_secret_technique.png": { + "size": 3031198, + "sha256": "20a686c1a98bc730af4ced690487ec6f0d3d140be2b5c474696325a648958f2d" + }, + "mengsan/assets/cards/mengsan_colorless_secret_weapon.png": { + "size": 3100884, + "sha256": "05858a388df685c1dbc2b60e6a3bb6e441d8991d01a3293b0613d8eb71a22bfb" + }, + "mengsan/assets/cards/mengsan_colorless_seeker_strike.png": { + "size": 3226533, + "sha256": "6dac80c8ff7afe7659ad69cb4d2db02c366c0c02e6088e9bd395418e9b74148f" + }, + "mengsan/assets/cards/mengsan_colorless_shockwave.png": { + "size": 3123692, + "sha256": "c4b10848314eeb75e3e4ca588f4a34f886cfbcd8f585d5b68391ff8d3affb16f" + }, + "mengsan/assets/cards/mengsan_colorless_splash.png": { + "size": 3008746, + "sha256": "0a8a4393dc4462de1f33bd03a1173792a3b29d4f92a235dd1b6dd0ecbc41dd6d" + }, + "mengsan/assets/cards/mengsan_colorless_stratagem.png": { + "size": 3124646, + "sha256": "55375fcebdaa50dea41f55ef29d349880beca781d3e583584d9d8e377a498247" + }, + "mengsan/assets/cards/mengsan_colorless_tag_team.png": { + "size": 3222820, + "sha256": "62f3a67a893d485c0a376ed96e7d0a97c69429d7648cd4075f72a8b074878907" + }, + "mengsan/assets/cards/mengsan_colorless_the_ball.png": { + "size": 3304542, + "sha256": "4e3673066a4b80a7c902896fea95f42e9fb9b71a058bb4309ee085df7f0d8a2a" + }, + "mengsan/assets/cards/mengsan_colorless_the_bomb.png": { + "size": 3150065, + "sha256": "b2d0d8799316933ae0bed6cd3b925bf729b448de3daf1d769918b5a41d79de78" + }, + "mengsan/assets/cards/mengsan_colorless_the_gambit.png": { + "size": 3194137, + "sha256": "cd37df03c949469af917d0150f73416cf05cc087dfb67fc9879d69238fdbff74" + }, + "mengsan/assets/cards/mengsan_colorless_thinking_ahead.png": { + "size": 3141303, + "sha256": "7bd6b4edb3e8bb92d8b90773df68570831c1be948a103a426433191dd5135b60" + }, + "mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png": { + "size": 3114116, + "sha256": "e7f9630a4d218eba20870c15d93a96eb2d5bea7f8697c4792832dd99e91e705b" + }, + "mengsan/assets/cards/mengsan_colorless_ultimate_defend.png": { + "size": 3310196, + "sha256": "45d4d4418254f13366deea71137d5b891177b49c981ac9c83293a000f7e8462c" + }, + "mengsan/assets/cards/mengsan_colorless_ultimate_strike.png": { + "size": 3222462, + "sha256": "25652e3b55614c84b25c27ae63605bae88bb0e284d4c6181474d0e2cca9e074a" + }, + "mengsan/assets/cards/mengsan_colorless_volley.png": { + "size": 3086676, + "sha256": "fdd8ca3936a72134d3085b34d69c5bd73b1e80d6f443908aa2bd48fe13b52892" + }, + "mengsan/assets/cards/mengsan_curse_ascenders_bane.png": { + "size": 3156093, + "sha256": "f12a36ffcd6f0dba728fcc7eddfef3323c04a33115bd4c3e53e3a4931b25587d" + }, + "mengsan/assets/cards/mengsan_curse_bad_luck.png": { + "size": 3075399, + "sha256": "3aa36640e3a47477931176fadd7423ab0016b2d0b53e1d293b933f421aa723f4" + }, + "mengsan/assets/cards/mengsan_curse_clumsy.png": { + "size": 3320908, + "sha256": "d4fd3da9b1dc49625f7e2d5d612234cb5f57ab75616ce1c2c5eb609e893dd628" + }, + "mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png": { + "size": 3190971, + "sha256": "d52d05e8ee1c3355ec34440694538049f6c3a9fac591d0a2d44947a9dde2ff7c" + }, + "mengsan/assets/cards/mengsan_curse_debt.png": { + "size": 3206708, + "sha256": "073aa84c0268bffae9cc017222bef8a1a8e8ba364abd6b64c3dee61a382f8c6e" + }, + "mengsan/assets/cards/mengsan_curse_decay.png": { + "size": 3159488, + "sha256": "4e4bb998c9d410bac843201e1161bf85da03797d3e7f46ebb22943e3bf2c45bc" + }, + "mengsan/assets/cards/mengsan_curse_doubt.png": { + "size": 3272171, + "sha256": "aa0e651e89cb3199f9568e5c00d72b842be96104a96698b8b484b8755862cce1" + }, + "mengsan/assets/cards/mengsan_curse_enthralled.png": { + "size": 3093077, + "sha256": "08df7fda97faf5bacbd9402eaccad5f03e16a69364464a6a97ada181e3fa3ac1" + }, + "mengsan/assets/cards/mengsan_curse_folly.png": { + "size": 3100483, + "sha256": "02707297707414f7eca6da31881b57f70d9d9805a06498f2363ec5793a91251b" + }, + "mengsan/assets/cards/mengsan_curse_greed.png": { + "size": 3283765, + "sha256": "0ba7674f5e8a2d07360389f45330666fc793e55c5f77abfd500f513db2bf9fed" + }, + "mengsan/assets/cards/mengsan_curse_guilty.png": { + "size": 3026762, + "sha256": "69bbe906990f27813ac542428549e33c29285cb44bf521515000c1c1d0702e83" + }, + "mengsan/assets/cards/mengsan_curse_injury.png": { + "size": 3232271, + "sha256": "ae043cfe8f0e5fb50ed75287b82494b7b38a918a331b6bf6a017e3283f4c4226" + }, + "mengsan/assets/cards/mengsan_curse_normality.png": { + "size": 3015776, + "sha256": "c2dd8e2fe83165c8129a2ff05ae1b86d5f4cfa258420280ccf49cd0d033f2c01" + }, + "mengsan/assets/cards/mengsan_curse_poor_sleep.png": { + "size": 3195332, + "sha256": "b3934757b27c91e96322227b64bae769addc87338cd9d2b18c0bdf8169165b7f" + }, + "mengsan/assets/cards/mengsan_curse_regret.png": { + "size": 3197963, + "sha256": "72f758eaa98a1fe254f025cf4096807498a8b9536a4cd085a6f8ac6cec9af2c9" + }, + "mengsan/assets/cards/mengsan_curse_shame.png": { + "size": 3159251, + "sha256": "e7090eb518f5ff5c022c77ced36866e3477c62319388feb99045ac0a24fcb289" + }, + "mengsan/assets/cards/mengsan_curse_spore_mind.png": { + "size": 3210425, + "sha256": "00dca10bcf03085d3d343c14fcc3703c5f3b3170ab992e74069a2cd730117177" + }, + "mengsan/assets/cards/mengsan_curse_writhe.png": { + "size": 3277925, + "sha256": "2dbaee5d8d3070f01d6bb0503c3235b6601fe4af7b5217fc4e17271161f7ed10" + }, + "mengsan/assets/cards/mengsan_dazed_shuying.png": { + "size": 3232164, + "sha256": "422244600455c716960bcc5f1aaf028733061b7f5b01e174125b9a46cab200ad" + }, + "mengsan/assets/cards/mengsan_event_abundance.png": { + "size": 3175446, + "sha256": "829339ea6c9f232b2004c179782393036629709e8eec6949cd6aa0d8c1b37950" + }, + "mengsan/assets/cards/mengsan_event_apotheosis.png": { + "size": 3202650, + "sha256": "3e213766a173b3c01004a5faf3b53481233f19375f4281929196d75dff37573e" + }, + "mengsan/assets/cards/mengsan_event_apparition.png": { + "size": 3276116, + "sha256": "07100920c178b5bfd383e5ced6fb9d6353c8d1995c80e9fedf409c7184cb0e0d" + }, + "mengsan/assets/cards/mengsan_event_brightest_flame.png": { + "size": 3088445, + "sha256": "e3abc30102a32ca9f91fb9af30bf3cd2790ff550388090d117e7978e31dede15" + }, + "mengsan/assets/cards/mengsan_event_byrd_swoop.png": { + "size": 3303389, + "sha256": "3769b6f25018f510ad489bfcdc5cb327c02c5aafd0a1b9ca780792b3c0fe13a8" + }, + "mengsan/assets/cards/mengsan_event_enlightenment.png": { + "size": 3306552, + "sha256": "5eb347d0984e569e43b792b044abd478d53d9cb9c9eecc5ba4e97773747a2699" + }, + "mengsan/assets/cards/mengsan_event_exterminate.png": { + "size": 3481990, + "sha256": "a0e53996ab3943760329f1aab87e1bc414964f5b88a19778415beecd61cb98be" + }, + "mengsan/assets/cards/mengsan_event_feeding_frenzy.png": { + "size": 2991504, + "sha256": "e7261af1c355fc358b15a8131d9b60909b6ba30db94b4d6f948802c3e3158c92" + }, + "mengsan/assets/cards/mengsan_event_mad_science_chaos.png": { + "size": 3225466, + "sha256": "2aa820d8c2f380eba5108df919295c4a509c33360bc934e678c72a60fa18d902" + }, + "mengsan/assets/cards/mengsan_event_mad_science_choking.png": { + "size": 3144566, + "sha256": "a8e5960f8d10e1a8ad9e390d309d50525f8831b82d139b3e65fe6caa3653e31d" + }, + "mengsan/assets/cards/mengsan_event_mad_science_curious.png": { + "size": 3128414, + "sha256": "2f42323201b656cfaba157c9f45ba8795518bb5cd5a4a65af9d6d8c5474d7a57" + }, + "mengsan/assets/cards/mengsan_event_mad_science_energized.png": { + "size": 3208057, + "sha256": "3de4101bff5223dd7b115294b67835fc7a849ca667ed729e6b6b8576125ffbcf" + }, + "mengsan/assets/cards/mengsan_event_mad_science_expertise.png": { + "size": 3158770, + "sha256": "a28fbf79892ab046388381a20e435f69bf53a6ba21148a3741c284989742a4b7" + }, + "mengsan/assets/cards/mengsan_event_mad_science_improvement.png": { + "size": 3101461, + "sha256": "f85a01280f85571848917e9375a122e17e7dbe0d6750766cbeeba18ffc85927f" + }, + "mengsan/assets/cards/mengsan_event_mad_science_sapping.png": { + "size": 3167779, + "sha256": "0cff4b294bbbc3c0097fc6036fd1febf49a82cb777b85b1ef4e2b7cf1aa0d344" + }, + "mengsan/assets/cards/mengsan_event_mad_science_violence.png": { + "size": 3217299, + "sha256": "07eb2165f6bda2bfce8c8267c83ff7caefc617105a33b0e99dd8e3c99582a97d" + }, + "mengsan/assets/cards/mengsan_event_mad_science_wisdom.png": { + "size": 3108692, + "sha256": "ff24a151489315ad9cef2bdec1d3adaa0dfd3bc5590870ac333b8af9d019d787" + }, + "mengsan/assets/cards/mengsan_event_maul.png": { + "size": 3146092, + "sha256": "7d66cab5f938b31c6191be72693251fb28b447a6a357f4888af712296d8b6090" + }, + "mengsan/assets/cards/mengsan_event_metamorphosis.png": { + "size": 3255357, + "sha256": "5af1bb8458b9533114d88eef94c91730094fa7d23190406fa86910f5006a97bf" + }, + "mengsan/assets/cards/mengsan_event_neows_fury.png": { + "size": 3270494, + "sha256": "e77228e317921968bc584f015c5cb5a018093eb8e8f8e14f68655d088fbd7175" + }, + "mengsan/assets/cards/mengsan_event_peck.png": { + "size": 3245605, + "sha256": "d7fa9557a82e46219917686a77fe1341cc57f843c166f65c63e8895885f29f88" + }, + "mengsan/assets/cards/mengsan_event_relax.png": { + "size": 3151165, + "sha256": "6cb2956664d30064d741fb76bd1f8444e470acdb3b294b006f64bb4d58ef4e68" + }, + "mengsan/assets/cards/mengsan_event_squash.png": { + "size": 3209656, + "sha256": "a280aa3721788e4fa71c718de80a6ae30658b6557b43a46dd06edf70b2df72a8" + }, + "mengsan/assets/cards/mengsan_event_toric_toughness.png": { + "size": 3097893, + "sha256": "735a500a48f344a755aeb94a2d6f40e2eb1b3692a109c60595d83ebc5635e7b2" + }, + "mengsan/assets/cards/mengsan_event_whistle.png": { + "size": 3288482, + "sha256": "585c5c4a5885c930a1c9f72dbdb5c1d4617d672481bae40f91f44256335474f2" + }, + "mengsan/assets/cards/mengsan_event_wish.png": { + "size": 3052956, + "sha256": "ec05fe126b5b533aba8eb31294ec446a72d7cbe61808e906075ad80271996dbb" + }, + "mengsan/assets/cards/mengsan_fangxue.png": { + "size": 300961, + "sha256": "c8f5ba0b966ba7ddbcac18edb80a5a407ee366f7e65c37ec620e927fabb2fcfd" + }, + "mengsan/assets/cards/mengsan_fangyu.png": { + "size": 284168, + "sha256": "63b6661a86350a635ade93322dfb76569dfc8aa652ff22150c538780bab7becb" + }, + "mengsan/assets/cards/mengsan_feijianhuixuanbiao.png": { + "size": 299751, + "sha256": "c7b5b3c56851459998933d05d939f46c39323feb13c26efbbcb5738d8a910a5d" + }, + "mengsan/assets/cards/mengsan_fennu.png": { + "size": 296860, + "sha256": "81b5c13db2980a7877506eaf9a7b0bbb028a77dda7c18cf1b4c4a402ac0db29e" + }, + "mengsan/assets/cards/mengsan_ic_ashen_strike.png": { + "size": 3380843, + "sha256": "62bb1d4a3cce45797e6ce257b690f127596acee491dd0b3091bf52018b839480" + }, + "mengsan/assets/cards/mengsan_ic_barricade.png": { + "size": 3184834, + "sha256": "39ee5e80832d97b79c82400b6f42c03a8701288447b37b698f7f5db8d80dcba6" + }, + "mengsan/assets/cards/mengsan_ic_battle_ready.png": { + "size": 3106395, + "sha256": "5392a0ace902bdd1487e5ca97ea469ba73efbbabab26f237ce76ef2ae7f4526f" + }, + "mengsan/assets/cards/mengsan_ic_battle_trance.png": { + "size": 3151914, + "sha256": "c327d3554861c1d2dc13c81d8142197bfa07359abac54c8611968168c12256d8" + }, + "mengsan/assets/cards/mengsan_ic_blaze.png": { + "size": 3375895, + "sha256": "75804bfb929e75b4ced9f332807fd74003127b3f83f72ccfe6d4d9f35a26bb13" + }, + "mengsan/assets/cards/mengsan_ic_bludgeon.png": { + "size": 3389225, + "sha256": "79c71b0419b979f7f06ffa12be1761ab41244d33c721c1a679d0448d00b29337" + }, + "mengsan/assets/cards/mengsan_ic_bodyguard.png": { + "size": 3343236, + "sha256": "50da52b4a1ee4537d6328d311bbb4842f7de74796613784f9d6d53f5f3d5f32e" + }, + "mengsan/assets/cards/mengsan_ic_boulder.png": { + "size": 3285153, + "sha256": "bff97d7a6f172b16cb3bb0434face4173bda907355516975efd0b177abdcb727" + }, + "mengsan/assets/cards/mengsan_ic_brand.png": { + "size": 3294776, + "sha256": "8f0da53ec9a4ba94742f7b08aab5c8c065ad7d7f06d0456c7b8beea9edc0156c" + }, + "mengsan/assets/cards/mengsan_ic_break.png": { + "size": 3312419, + "sha256": "59e6a3748d903a508f5a2fb1fe4160848ab301b7f82b43b15c90daa88134c992" + }, + "mengsan/assets/cards/mengsan_ic_bully.png": { + "size": 3295672, + "sha256": "11e917a6a33573ea00f187afb8b18c313887e901f30d8ea09dfb206c6dcd5ff3" + }, + "mengsan/assets/cards/mengsan_ic_burning_pact.png": { + "size": 3261391, + "sha256": "e3163733cefc1f8c99f0d6549329e0fff3fb3a6c15dbb917d455173d39fa6a85" + }, + "mengsan/assets/cards/mengsan_ic_clash.png": { + "size": 3375675, + "sha256": "c61999fb8ee8d70150348630fc773d7391f2239d01289acb1cb98906ad6b29b6" + }, + "mengsan/assets/cards/mengsan_ic_colossus.png": { + "size": 3446064, + "sha256": "95758ea8bc56299aa7f29f48eed911ece7fa11fbbfd77d6ee865e3142002a607" + }, + "mengsan/assets/cards/mengsan_ic_conflagration.png": { + "size": 3383784, + "sha256": "24340736d1292a3e199148b02c6a395d91e94ee541833a7d9acf472285046967" + }, + "mengsan/assets/cards/mengsan_ic_corruption.png": { + "size": 3321262, + "sha256": "040e1f40a9205862b4893fefbbfd0fbde2530983694bafd1509347830b60bf26" + }, + "mengsan/assets/cards/mengsan_ic_crimson_mantle.png": { + "size": 3315436, + "sha256": "7a0b60005554b2ecff4eada7096b5be930146e51769cc6f3a5784fc8f6f84459" + }, + "mengsan/assets/cards/mengsan_ic_cruelty.png": { + "size": 3318267, + "sha256": "d538eac00eeb64bda764b32555207e79de8eb860d4b9e80377f0566534f8dc4a" + }, + "mengsan/assets/cards/mengsan_ic_dark_embrace.png": { + "size": 2966840, + "sha256": "ffdd9cc3fbd16ff58c647d4e05c6e8dd78f348140de2a0651dffe40e4ec49cdd" + }, + "mengsan/assets/cards/mengsan_ic_demon_form.png": { + "size": 3414331, + "sha256": "33b54e87e86d88c50e9995ea6849eed9e2f65dc0a2aa50a8a72b8ddc3b209165" + }, + "mengsan/assets/cards/mengsan_ic_demonic_shield.png": { + "size": 3363214, + "sha256": "1f2ccf30e3534948282ac9cec36aeb1eb8a76161f4970188610f4ebd564a6cf1" + }, + "mengsan/assets/cards/mengsan_ic_dismantle.png": { + "size": 3290705, + "sha256": "1524381ee0fd906e7c287017ac2c07df01a2128837635c958456d90f8a116526" + }, + "mengsan/assets/cards/mengsan_ic_dominate.png": { + "size": 3314562, + "sha256": "1b054572525b494046efc2df72e87446a121bee8084a6ab022f526d8e9f0076f" + }, + "mengsan/assets/cards/mengsan_ic_drum_of_battle.png": { + "size": 3155334, + "sha256": "498552963fda156c5f179fd7a3b4ceb1adaa6e8f9e4e02d864508a95a17c21ce" + }, + "mengsan/assets/cards/mengsan_ic_dual_wield.png": { + "size": 3219368, + "sha256": "a22ebfe2790fe287f16e91f3bf767b1bdd2770c86559ee39060dfac2708dee2e" + }, + "mengsan/assets/cards/mengsan_ic_eager.png": { + "size": 3155388, + "sha256": "30fc98f684f1f4d1e428245b89917b7bb68c037b5d7582a8f7eeab99685d2b5d" + }, + "mengsan/assets/cards/mengsan_ic_entrench.png": { + "size": 3500692, + "sha256": "38f8a2cb023b54a49d9c85bb523c4c7f8c6ea60d6ed30d2e02825358fba5a900" + }, + "mengsan/assets/cards/mengsan_ic_evil_eye.png": { + "size": 3403431, + "sha256": "cbf7f72b018401d6cc208b8f03440417d0943d70e1f91fc3f6ce9f3279f997f0" + }, + "mengsan/assets/cards/mengsan_ic_feed.png": { + "size": 3195433, + "sha256": "e68e07bae5c148ada1a33964265fc9eb9a0d4388fee3e35f6f9b0fef0f53afc8" + }, + "mengsan/assets/cards/mengsan_ic_feel_no_pain.png": { + "size": 3148743, + "sha256": "216044b33d3dcab884597a5382d932b34be68b36700de401952107e96bcdb83c" + }, + "mengsan/assets/cards/mengsan_ic_ferocity.png": { + "size": 3156966, + "sha256": "7b71c93f5fe9a48c63f52c4072c021ff8bd3da26e89a478cdec9ca675ed150c2" + }, + "mengsan/assets/cards/mengsan_ic_fiend_fire.png": { + "size": 3445601, + "sha256": "8e10353d6717f4f649acf821b3e3b4af70a058d40aef7e86ceef3b70fb413d5f" + }, + "mengsan/assets/cards/mengsan_ic_fight_me.png": { + "size": 3312407, + "sha256": "3bd8d2bf23c8fa667cb40297716218bb55f3cceb12e77b841651aebf9c7f5545" + }, + "mengsan/assets/cards/mengsan_ic_flame_barrier.png": { + "size": 3459567, + "sha256": "c73225d6181f96b6d5f35ef306d858c73b60d3af986cf0232e704374413a134a" + }, + "mengsan/assets/cards/mengsan_ic_forgotten_ritual.png": { + "size": 3345779, + "sha256": "8a2b56f31440771c5c853d1dfe9860e507675b9d330ff0a48f7d11762e51f537" + }, + "mengsan/assets/cards/mengsan_ic_grapple.png": { + "size": 3215001, + "sha256": "512ddb86f8ea7489b5b292cdcd4f0d7d892ccd7c4a2bba0e218c06325f23646c" + }, + "mengsan/assets/cards/mengsan_ic_hellraiser.png": { + "size": 3258334, + "sha256": "b0a15fd06317803c7f29537a03527d41ae430f53595a7ff92ba491cdb068b76e" + }, + "mengsan/assets/cards/mengsan_ic_hemokinesis.png": { + "size": 3582165, + "sha256": "e7392585c268ccca314b9be969951fc803e011beaf2cc3c72d578c0561b674d8" + }, + "mengsan/assets/cards/mengsan_ic_howl_from_beyond.png": { + "size": 3196502, + "sha256": "801203f8103404b55c861450d104866975eb2dd379233c8d19de370a83c4ea9d" + }, + "mengsan/assets/cards/mengsan_ic_impervious.png": { + "size": 3398556, + "sha256": "45366fc69d3c6b7b91fe15611c76f693fe2f445406e53ef582b3379447759f18" + }, + "mengsan/assets/cards/mengsan_ic_infernal_blade.png": { + "size": 3081535, + "sha256": "10cfbfdd41d76c924f4c6fa74f57dc6546c181f26602c091e602ad0234b4ee07" + }, + "mengsan/assets/cards/mengsan_ic_inferno.png": { + "size": 3389310, + "sha256": "b1108d5cb2cefa40f9a4a5ea13a924a2624a57b4ca7c2ae8b7e7086ba09b090d" + }, + "mengsan/assets/cards/mengsan_ic_inflame.png": { + "size": 3236167, + "sha256": "f9ee9fd58e4d5e74669665e0cbbc9050a0b61aa3ec2a798f97da97c1a6d44e1c" + }, + "mengsan/assets/cards/mengsan_ic_juggernaut.png": { + "size": 3433571, + "sha256": "4faca2d750999826ca934c45ea648850233b4038207b6cf1d5f05eb6ffd34f49" + }, + "mengsan/assets/cards/mengsan_ic_juggle.png": { + "size": 3320003, + "sha256": "d3f8d1394131b5f7d6cbeef9b8eb89ac25b0c108d79267210d5b6f9f7adcf513" + }, + "mengsan/assets/cards/mengsan_ic_midnight.png": { + "size": 3163175, + "sha256": "e0f68012e73f192a2cc169235eb0b49f27b6991c37a5e8862cdb4f32d010d39b" + }, + "mengsan/assets/cards/mengsan_ic_not_yet.png": { + "size": 3398829, + "sha256": "dddcd2e90d84883c33653169df1591633b944932aaaf6f34f43be10724007515" + }, + "mengsan/assets/cards/mengsan_ic_offering.png": { + "size": 3358902, + "sha256": "a8ac083487185734223c31e1e428f9ee78c0dad7e6e37baa201bb33a44e974b5" + }, + "mengsan/assets/cards/mengsan_ic_pacts_end.png": { + "size": 3444230, + "sha256": "c23a5eba95bf6d44519eb890580ac576ea5477f96345cd9c58b03183d74179bc" + }, + "mengsan/assets/cards/mengsan_ic_pillage.png": { + "size": 3371280, + "sha256": "f7aba0b71d6cbd43d6de528713e7b2ca015c35a99826dcfbd8cb04f9d8e4c684" + }, + "mengsan/assets/cards/mengsan_ic_primal_force.png": { + "size": 3284334, + "sha256": "c3b0d479166fae86e39f60739118fb993baae2d9c2e0ff2bc0b606d95b436b84" + }, + "mengsan/assets/cards/mengsan_ic_pyre.png": { + "size": 3272678, + "sha256": "e29710c3846fbe3571460232bbe337f502b1a5cb3928774b0796b16339030e3f" + }, + "mengsan/assets/cards/mengsan_ic_rage.png": { + "size": 3254815, + "sha256": "178f291ccb5760f122b335b63963432e0e547f54b6d2a94b95641c382e60b708" + }, + "mengsan/assets/cards/mengsan_ic_rally.png": { + "size": 3208925, + "sha256": "5ca01c9b5ab51118da8afe5f5019ac9c8394d21c3153260d307d3c1e83de9159" + }, + "mengsan/assets/cards/mengsan_ic_relentless_assault.png": { + "size": 3157513, + "sha256": "d5093dcfc00a559844f160f6a07a32c5633047312f04e2c84cf385850f4565c1" + }, + "mengsan/assets/cards/mengsan_ic_rupture.png": { + "size": 3242389, + "sha256": "a7d85597a367d43006abb2bc54fc1adade5f1fe396d80aa64b1d1fc0790c5517" + }, + "mengsan/assets/cards/mengsan_ic_second_wind.png": { + "size": 3326737, + "sha256": "c00a2886359ff2cc6e067de86ef0126040934bd079e1d65b12a58e749bbc48f8" + }, + "mengsan/assets/cards/mengsan_ic_stampede.png": { + "size": 3340733, + "sha256": "3e7ecc806b9c528cdd2e2de45eaf04926e9fd0a49a71ae06602e1fb9938e6804" + }, + "mengsan/assets/cards/mengsan_ic_stoke.png": { + "size": 3224630, + "sha256": "2f2947c935a95aa22e7aea17e5b684e8077d0ccfd56348d404a3dea40e428850" + }, + "mengsan/assets/cards/mengsan_ic_stomp.png": { + "size": 3230547, + "sha256": "ed2a8f438c40c486e4efbc128c45bce568137d3d87a4d97ad2a3b69317e43342" + }, + "mengsan/assets/cards/mengsan_ic_stone_armor.png": { + "size": 3409260, + "sha256": "f72619cfaed0d7b433dc58a6204751e292a4e69cde3089417ce346894ebe683f" + }, + "mengsan/assets/cards/mengsan_ic_taunt.png": { + "size": 3260651, + "sha256": "122103d43fb1053d12d172c2a488bae9f7a6765c6d36ea2d3b13412a2de6e33d" + }, + "mengsan/assets/cards/mengsan_ic_tear_asunder.png": { + "size": 3318965, + "sha256": "df905976f4888b4eccc1418591a38c725f9c45cd579097adbf255c5f3ba2aeae" + }, + "mengsan/assets/cards/mengsan_ic_thrash.png": { + "size": 3408839, + "sha256": "8277ecb7faf2df11ad9e05de620b47ea44f20a866ff02fb9ff042ec8479bfad4" + }, + "mengsan/assets/cards/mengsan_ic_twin_strike.png": { + "size": 3465328, + "sha256": "a2e60f67dfff10789fd453f1525941238389786f3dc5e71788a30d312b751914" + }, + "mengsan/assets/cards/mengsan_ic_unleash.png": { + "size": 3412724, + "sha256": "3dd9906996d07deaa06e1b56839609175a1d9d88bccdd9237465844c3e5882bc" + }, + "mengsan/assets/cards/mengsan_ic_unshakable.png": { + "size": 3286732, + "sha256": "a7540f94fe98fce1434928b6a204f96f2f036a3db81579e499b2a31a7d9c6184" + }, + "mengsan/assets/cards/mengsan_ic_uppercut.png": { + "size": 3369967, + "sha256": "0935723010a4fd761821e827e6d456a07c63ac937beb0ca8421415d2bd67bc9d" + }, + "mengsan/assets/cards/mengsan_infection_shuying.png": { + "size": 3154682, + "sha256": "7a47a79522cf9c684efce578b43197936a37feca7e229097d86903118e5a5862" + }, + "mengsan/assets/cards/mengsan_jianbingdaji.png": { + "size": 295064, + "sha256": "379b700ebba9843c94f63b82ebd68536e232a0f5b4f4e093b055ae26023a83eb" + }, + "mengsan/assets/cards/mengsan_jianyi.png": { + "size": 308533, + "sha256": "8440f6dc36bbc3fec6bd7406f2a0623aae37f63e7cbc1a7dc7b912805f4c9f38" + }, + "mengsan/assets/cards/mengsan_pomie.png": { + "size": 299414, + "sha256": "67ab0a3a024f0a4095f232d16390f0431afa05cc2c9d292752dfa23a4185ee28" + }, + "mengsan/assets/cards/mengsan_qiling.png": { + "size": 284417, + "sha256": "f2f38bbcf852e7f6fc7d6e7c971d622e2d437bde8eafccb1226dab66a4b51164" + }, + "mengsan/assets/cards/mengsan_quanshenzhuangji.png": { + "size": 298500, + "sha256": "19cfcaf0607a17db14f82e458e8aa77d02c73b234aeea6af3bd715ea7fdf0673" + }, + "mengsan/assets/cards/mengsan_rongrongzhiquan.png": { + "size": 286444, + "sha256": "c43ff6366ac35a6b7a2cc99b2bcbb8052c86efeb3a807c66696e363597ded83d" + }, + "mengsan/assets/cards/mengsan_shandianpili.png": { + "size": 289884, + "sha256": "54e18bfb541c08a78922bc38f26323c20a7f160e54218016f0375c8c4cabedb3" + }, + "mengsan/assets/cards/mengsan_shuangchongdaji.png": { + "size": 290611, + "sha256": "2dde2cabca61df441a98daf41f82a66c6343aba7337b3b650d63e01dbedf56c2" + }, + "mengsan/assets/cards/mengsan_slimed_shuying.png": { + "size": 3173387, + "sha256": "6185d04b40409525e0eb8268eb55ff0a047ad68833169230936dca937c37b05e" + }, + "mengsan/assets/cards/mengsan_songjianwushi.png": { + "size": 299499, + "sha256": "fb9db91a0ceafd18e71b15807bc994a93797a0d438bb7d8fe4db459adfe11534" + }, + "mengsan/assets/cards/mengsan_status_beckon.png": { + "size": 3089872, + "sha256": "ec9aa4385fcffa00030390c2bf02ed833cf7b4200d268955831949e3deb4c22d" + }, + "mengsan/assets/cards/mengsan_status_burn.png": { + "size": 3202565, + "sha256": "fee1bddc02b755972e99be68fd354ec69cb9b6286a15c9c8203ed2708bb29904" + }, + "mengsan/assets/cards/mengsan_status_debris.png": { + "size": 3212683, + "sha256": "8f251585e1487a3d8c04111eaac50ca7860745a8386107c0497f777fdc2ac3ec" + }, + "mengsan/assets/cards/mengsan_status_deprecated_card.png": { + "size": 3211849, + "sha256": "9dc7828e6e5559479d48a2f26144ca40ee365b5587183496d41085bc01f7464e" + }, + "mengsan/assets/cards/mengsan_status_frantic_escape.png": { + "size": 3331510, + "sha256": "e5b328f4f8efae481d25ef7fc2739c4f614ff05ee65799363a4194e229406526" + }, + "mengsan/assets/cards/mengsan_status_soot.png": { + "size": 3164859, + "sha256": "885f2f1557cb9483c0bc0538c6eb3f1abb017809883d24a2f864732df4e48bef" + }, + "mengsan/assets/cards/mengsan_status_toxic.png": { + "size": 3047189, + "sha256": "d2e6f92ba539b91a21cd3eb836f2b28c21e81548fbc1fcbf1686680533bc39c6" + }, + "mengsan/assets/cards/mengsan_status_void.png": { + "size": 2969653, + "sha256": "63ad2acb439b474b93a250398cb153063c01c74ddd4ce7c82a8717f743495ef1" + }, + "mengsan/assets/cards/mengsan_status_wither.png": { + "size": 3329606, + "sha256": "265095ea049e38be62725ece19e91c15ff6abb232a4276779b93555ce0dccad1" + }, + "mengsan/assets/cards/mengsan_tiezhanbo.png": { + "size": 286763, + "sha256": "9e1a8525003f822c93bc8ed84d3548a6848ad29d849434fceb64e9561b7df61b" + }, + "mengsan/assets/cards/mengsan_touchui.png": { + "size": 296991, + "sha256": "de29a754d72d6151eba7c7028b61ded9d475e80d3303f9246186cb37b8f59d3d" + }, + "mengsan/assets/cards/mengsan_tupo.png": { + "size": 297945, + "sha256": "9de2094ccdd9c4ddc743cda6a9e9e12b8decc98019d6af1a2c4290c9f1c789cf" + }, + "mengsan/assets/cards/mengsan_wanmeidaji.png": { + "size": 298675, + "sha256": "2d786cf6078dbf1ea545886bd98298c867fbbf4c5f0fc1a74f28a2ac8f06cc1a" + }, + "mengsan/assets/cards/mengsan_wound_shuying.png": { + "size": 3102499, + "sha256": "16aa7c0485416028c1ac4990ceb6d9f850dc93d63a74bfa3514dfddd101f6c29" + }, + "mengsan/assets/cards/mengsan_wuzhuang.png": { + "size": 297615, + "sha256": "8b3d3d473d2db34a39b69e0cac53658da2b3e1e95086c982389640b490881a7d" + }, + "mengsan/assets/cards/mengsan_xuanfengzhan.png": { + "size": 283797, + "sha256": "85d06995103cf42f59790a93a89d64ede27111eb3c8e61d7e33a6b493b09240b" + }, + "mengsan/assets/cards/mengsan_xueqiang.png": { + "size": 271433, + "sha256": "da5b03fba289b63ae4e3c55b9c0b702aa3a805770887e5d323c0f3dd88bc01e6" + }, + "mengsan/assets/cards/mengsan_yuanhen.png": { + "size": 282703, + "sha256": "c802721d642360389b8ab9437f8f2dc27d804e8da537ac17ae38e9c744e77903" + }, + "mengsan/assets/cards/mengsan_yubeidaji.png": { + "size": 291900, + "sha256": "9ea0dad98b3e76906629905557976a6d95c66d53257fde0ccc33c46f7f20ce84" + }, + "mengsan/assets/cards/mengsan_yujin.png": { + "size": 290863, + "sha256": "62f01ae1869fa481be843eae236c2046e5388dfa537ae6c02291085b5248caba" + }, + "mengsan/assets/cards/mengsan_zhanli.png": { + "size": 283172, + "sha256": "5297fdf3163730e5cb9e318c905287cae52d61aa53e6e9dd7586193706b35daa" + }, + "mengsan/assets/cards/mengsan_zhongsha.png": { + "size": 277449, + "sha256": "430256856588341cc0ad3fe15aee2e9b63711187d9ebe77118df0b60003b956f" + }, + "mengsan/assets/cards/wound-original.png": { + "size": 323855, + "sha256": "c588716825f10b5d5fc504221184337b167542d1a7e6cf169c773cbaf8e2de48" + }, + "mengsan/assets/intent/Intent_attack_1.png": { + "size": 9558, + "sha256": "f2c470238c20a762cdaf4dda607b7ede3b46cf8ebde99fbecb689bc5a4aafef7" + }, + "mengsan/assets/intent/Intent_attack_2.png": { + "size": 11308, + "sha256": "d7db260070139367b9a1cd9729e701ea0a738e9314ed8d69362e4a6db0765f53" + }, + "mengsan/assets/intent/Intent_attack_3.png": { + "size": 12005, + "sha256": "f312ddeebc78634a4d943f53ac9430aebd2e83fe0f17fcaff4f21d02af957983" + }, + "mengsan/assets/intent/Intent_attack_4.png": { + "size": 12570, + "sha256": "54e3c06ac574817d75525f84792acd59a977f083b6ae54ce871b1587864449a3" + }, + "mengsan/assets/intent/Intent_attack_5.png": { + "size": 14137, + "sha256": "afbd87907f01aa2b2dccbd6c24e571d7632052b250a4c9d1ab10b1cf08c843e9" + }, + "mengsan/assets/intent/Intent_buff.png": { + "size": 5505, + "sha256": "15dbb076206d24c230dbb77ef7992b17d08a78a3add78777f82e7e307de1f9b9" + }, + "mengsan/assets/intent/Intent_card_debuff.png": { + "size": 8179, + "sha256": "1a9b999c5c9f861b05d2beab84e847a1c9313eecec438ad86ca361f3b5a545f9" + }, + "mengsan/assets/intent/Intent_death_blow.png": { + "size": 7712, + "sha256": "bd7f528ba8c54cd49ca08b54bd63355247f16fd8fc73fee84f0c3d32c5e0c3a0" + }, + "mengsan/assets/intent/Intent_debuff.png": { + "size": 15067, + "sha256": "3b0f30e7f9ac9fb38968094be967d6c9447e7c80411f7c35a77883b0f8c8b885" + }, + "mengsan/assets/intent/Intent_defend.png": { + "size": 9518, + "sha256": "801ac36df2596bc40cc34211596865b7c5011b84d1b2b026c41e907faf824085" + }, + "mengsan/assets/intent/Intent_escape.png": { + "size": 7440, + "sha256": "bd1520b97af0a7e6ae64d227e9100e439b1397339be437fae5e8e3ccbf9a3d07" + }, + "mengsan/assets/intent/Intent_heal.png": { + "size": 5046, + "sha256": "26fe175442542e594d801f14223381e1e8b8fbf10b0bdb4e3b4618d4601bc6d3" + }, + "mengsan/assets/intent/Intent_sleep.png": { + "size": 8969, + "sha256": "72b5fa19e2e9c22f3508b59943bc7befa5a05e8791ab8d12530c228242f5cb9b" + }, + "mengsan/assets/intent/Intent_status_card.png": { + "size": 10048, + "sha256": "15bd3635046063126d039612911df2870c3fadf8f1104ddde12a7087aa5f0a5e" + }, + "mengsan/assets/intent/Intent_stun.png": { + "size": 13334, + "sha256": "35ef85fb8d9c5ef789742e8467b3049f40fddfcbdf042747f28345e7ebdc1f4e" + }, + "mengsan/assets/intent/Intent_summon.png": { + "size": 10668, + "sha256": "0338e8ee861802ec7160519b2945b05089c4f9672f3f1d8009f2484bff34e261" + }, + "mengsan/assets/intent/Intent_unknown.png": { + "size": 6401, + "sha256": "505e0339329dd5374fddce7be5e677f1a9d50f0db7a476ffd7e6349691ea408d" + }, + "mengsan/assets/portraits/bygone-effigy-forest-portrait.png": { + "size": 2884630, + "sha256": "002ed3ec353c114667478afef3aec4e35a59da1e18d24cc82b6376785cd4bfdb" + }, + "mengsan/assets/portraits/bygone-effigy-original.png": { + "size": 164866, + "sha256": "201df632cc52fbe572642c1fef10b9bb33dd4a74cec6e068a595056ac5c9e403" + }, + "mengsan/assets/portraits/byrdonis-forest-portrait.png": { + "size": 2699460, + "sha256": "80feac5bc9f6a09eb4882eaef520e42955404d443458abf7f46db27b20226b18" + }, + "mengsan/assets/portraits/byrdonis-original.png": { + "size": 119279, + "sha256": "b5d17a9ce0c741fe2031b979aee3f9285aaeb4f0c85d5de4a1440700e3a921f2" + }, + "mengsan/assets/portraits/ceremonial-beast-forest-portrait.png": { + "size": 2682259, + "sha256": "d3e7844a420473cf3c16922ea485fc560baefcb65f71e1593a12b8a683e864d5" + }, + "mengsan/assets/portraits/ceremonial-beast-original.png": { + "size": 221253, + "sha256": "0b8a913d8e78e45eee6eec7d9081380b37788bcabc2d5e0137fedef0608ea09c" + }, + "mengsan/assets/portraits/cubex-construct-forest-portrait.png": { + "size": 2628127, + "sha256": "446a72d5a6226d81f1ebb2bff450464f6be1cac76c56405121f89879419180f8" + }, + "mengsan/assets/portraits/cubex-construct-original.webp": { + "size": 7980, + "sha256": "37ed7258667485ff17c2739b0c725efa06b5f3c114e834d9ae6f25800c5934bc" + }, + "mengsan/assets/portraits/eye-with-teeth-forest-portrait.png": { + "size": 2303458, + "sha256": "b9ec3cd9d4e7e2272fed6cdae5dd871efa1f36382ca3b251c35514433c70ab03" + }, + "mengsan/assets/portraits/eye-with-teeth.webp": { + "size": 16056, + "sha256": "fcb437dcd559e6d7a8d4be5fba6f90d9ad44ab214452f7fb548583261100682b" + }, + "mengsan/assets/portraits/flyconid-portrait.png": { + "size": 1990312, + "sha256": "83f9724758fa09ade94731e061da584ec986b8cbd058d4ba966db237f6dc58ed" + }, + "mengsan/assets/portraits/fogmog-and-eye-forest-portrait.png": { + "size": 2314490, + "sha256": "c7e869cf1fdc18cce6eb499d39809c30a97faa55b3d19b7bb95932c1b1f91672" + }, + "mengsan/assets/portraits/fogmog-forest-portrait.png": { + "size": 2310155, + "sha256": "a76c62c937c00ddc4dff51c7e14fce20ab57ad69fe5dffe39aee8a4f9f9d21b7" + }, + "mengsan/assets/portraits/fogmog.webp": { + "size": 12420, + "sha256": "9af334ab2cec331d2ce2bc17df772b91a3a436e1d6e47c4179e4107b1d5c9d80" + }, + "mengsan/assets/portraits/fuzzy-wurm-crawler-forest-portrait.png": { + "size": 2447058, + "sha256": "d4794bd0807b08012d06ac2734ffff3ba430fae966af3c4c94b375610848e607" + }, + "mengsan/assets/portraits/fuzzy-wurm-crawler-original.webp": { + "size": 9968, + "sha256": "bbd3e1eb4858c299f8b328b4974ee2fc958cc530c4c51d3f9203e89d151dfe56" + }, + "mengsan/assets/portraits/guanyu-portrait.jpg": { + "size": 980085, + "sha256": "729d194ccf4507b5d025e7c697811b7236c6286f5feb7e2acd68964a9dd90602" + }, + "mengsan/assets/portraits/inklet-forest-portrait.png": { + "size": 2202786, + "sha256": "cb22e222c95aeca9730d53536a066b69b1d77e3b9e32acb1a3f670e28e7b162d" + }, + "mengsan/assets/portraits/inklet-original.png": { + "size": 18975, + "sha256": "aed225fc3df345e48deb9cf4d314699006cd9b69f005358390e3a4039cbd0649" + }, + "mengsan/assets/portraits/kin-follower-forest-portrait.png": { + "size": 2349442, + "sha256": "cc0a249e832f6df0068eedc20a0ba82640ff377e8f2ada1b80edb00e89e75ea2" + }, + "mengsan/assets/portraits/kin-follower-original.webp": { + "size": 10758, + "sha256": "ea19927f34cd0af0023be79d09cb6ae1d7ddd17ed1fd24b62ca39622ab9cb89d" + }, + "mengsan/assets/portraits/kin-priest-forest-portrait.png": { + "size": 2508934, + "sha256": "b379e7fffdb06e3f7734109a509f379c909deb4c54fa11700fc5eca015cc73d5" + }, + "mengsan/assets/portraits/kin-priest-original.png": { + "size": 105120, + "sha256": "616bd85d422cc8b022ceecaa184a0a6e6e83e54d069671145023bb3819b7d37e" + }, + "mengsan/assets/portraits/leaf-slime-m-forest-portrait.png": { + "size": 2290313, + "sha256": "bbf7d43a1e5f9d464dff54935ccede4c8334b209fb67f20c3fe9e871925a2c53" + }, + "mengsan/assets/portraits/leaf-slime-m-original.png": { + "size": 22536, + "sha256": "a06b5fddbbc2df03bf2c971dea9a9b36f5f6fed606a1f9ef47f2097fe1a68220" + }, + "mengsan/assets/portraits/leaf-slime-s-forest-portrait.png": { + "size": 2048824, + "sha256": "aca265306d482c6bb89d3d716ff091b44eb03781f8a33aa68b25f97b6a61ce9d" + }, + "mengsan/assets/portraits/leaf-slime-s-original.png": { + "size": 9099, + "sha256": "8c6f876ac0037d7b971bbb7e56256341fb270f274442bc11162787e9e0877cd6" + }, + "mengsan/assets/portraits/liubei-portrait.jpg": { + "size": 945285, + "sha256": "2a26577e496535a6f578d7bd77c13a1d5fd8b25da9d9a29947195c6c03589eac" + }, + "mengsan/assets/portraits/mawler-forest-portrait.png": { + "size": 2557684, + "sha256": "656452b9250c98d643773031cc8f2b1b62b6d9bfa90f95dcbc2e969e0bcc3571" + }, + "mengsan/assets/portraits/mawler-original.webp": { + "size": 9340, + "sha256": "2b79b8ea9052c6f8881b543dc87f49f15c2f466e88e8ee928110b97460dcaf35" + }, + "mengsan/assets/portraits/nibbit-forest-portrait.png": { + "size": 2465408, + "sha256": "1a0913dc85f7d02824ced4806a0fb64286aaccd704fa0accd173d46d6758238e" + }, + "mengsan/assets/portraits/nibbit-original.webp": { + "size": 8956, + "sha256": "123124eba6b62191aa1216f6b8fa541b4fc7cd05d6d57b386df1098c6e3e7ab5" + }, + "mengsan/assets/portraits/phrog-parasite-forest-portrait.png": { + "size": 2779629, + "sha256": "9c8b255b9d6877c79054267f3648d11cc6bb26dbf7be13a74d0b7015f9bce23e" + }, + "mengsan/assets/portraits/phrog-parasite-original.png": { + "size": 194971, + "sha256": "933ffcf205a3aef6d7ccc64423b2ecf6cdf4c82ee143e5106222f2e095e603cf" + }, + "mengsan/assets/portraits/raider-assassin-portrait.png": { + "size": 2495247, + "sha256": "5e390c7536915a868975bcdc4d63458e31cf4ecc4ae6d45726692ec92fcc1b01" + }, + "mengsan/assets/portraits/raider-axe-portrait.png": { + "size": 2707142, + "sha256": "35621425ef3bfdfdcfd72fec4ed15ecaa47bc632b8f4687607f02bf2684187a0" + }, + "mengsan/assets/portraits/raider-brute-portrait.png": { + "size": 2523079, + "sha256": "99cc94bb1e57d4c256ec5c78e964d877ec9b269ced93adadd226b7774f9853da" + }, + "mengsan/assets/portraits/raider-crossbow-portrait.png": { + "size": 2574258, + "sha256": "72c303e5a421e5cc790bf1bec9d13ac174a08e46d0d8cd3113e53cf57065acdb" + }, + "mengsan/assets/portraits/raider-tracker-portrait.png": { + "size": 2469608, + "sha256": "dd9d4c269161145ffc3de6b446e4f0a9bb7f69e3c9a0243f8418fda2413c4ad6" + }, + "mengsan/assets/portraits/shrinker-beetle-forest-portrait.png": { + "size": 2212880, + "sha256": "0b196f76754bf1946102d3d4889c856ebda9415ed54c7b2864052d7492e5cbbb" + }, + "mengsan/assets/portraits/shrinker-beetle-original.png": { + "size": 29818, + "sha256": "84002e8534f1c51abc790f7fe5c8d761e3861eb81d4799d322ef07d31ba689d4" + }, + "mengsan/assets/portraits/slithering-strangler-forest-portrait.png": { + "size": 2464156, + "sha256": "cda3af95f3b5c042859d14929614c4def0ee24bbc74dbea22ffe37c727c5166e" + }, + "mengsan/assets/portraits/slithering-strangler-original.png": { + "size": 34870, + "sha256": "aa70738cbb412fb50aab01e7435f43e1aea08b28b6b262326f61607b08092319" + }, + "mengsan/assets/portraits/snapping-jaxfruit-forest-portrait.png": { + "size": 2331283, + "sha256": "90ac4774e51d8d32da09269c68f85494513e0540bd0f9c22c6e5239aebf4d33f" + }, + "mengsan/assets/portraits/snapping-jaxfruit-original.png": { + "size": 57669, + "sha256": "a2b236b6d47deeb99233ea78666422440611aff95f53f0667b62442ae8224594" + }, + "mengsan/assets/portraits/sushuang-zhangshiping.png": { + "size": 2961815, + "sha256": "a57aff9ea3666bb4a5dc8fd66adc8dba88b50391fb5dc29e20ff6caae14b51c7" + }, + "mengsan/assets/portraits/twig-slime-m-forest-portrait.png": { + "size": 2378273, + "sha256": "b73751425e4c73f19eda40b7acc12171f4e1a53b14091c27be34c38635193859" + }, + "mengsan/assets/portraits/twig-slime-m-original.png": { + "size": 36491, + "sha256": "f2d7632f0d0612cc48756cb16ed9e5b2e8e9afb13d776f959b1f66b25417b097" + }, + "mengsan/assets/portraits/twig-slime-s-forest-portrait.png": { + "size": 2145175, + "sha256": "42616385167126cc4a183a8c6d6c651900e23f29d70edbd86fdc0c4fcd284e96" + }, + "mengsan/assets/portraits/twig-slime-s-original.png": { + "size": 12691, + "sha256": "77cb7172d0d209566a94c5c0022ac1ef10ce0d5912c1e7ccb8fc0f0186e3b8b6" + }, + "mengsan/assets/portraits/vantom-forest-portrait.png": { + "size": 2356886, + "sha256": "46649e3ce6e833d4c743e88d963831885ab4c56c23622620d9f7464169a3ec59" + }, + "mengsan/assets/portraits/vantom-original.png": { + "size": 96170, + "sha256": "87c14412b3f857f4404fe5d2b3c5f04a64fff6ea9357e0d9f3550bbd1336d97f" + }, + "mengsan/assets/portraits/vine-shambler-forest-portrait.png": { + "size": 2473037, + "sha256": "35f004dee2721cc8d6ecf981bc9bb24d3e2fad397cbe4205bd8d6330845b7c42" + }, + "mengsan/assets/portraits/vine-shambler-original-thumbnail.webp": { + "size": 24254, + "sha256": "882d887821a4dd5310f9c90f62c7501d3ad37d46a1a60bf613f77dc8f2569508" + }, + "mengsan/assets/portraits/wriggler-forest-portrait.png": { + "size": 2799836, + "sha256": "1b93f4e0e3bbd1b6d70851b160be50e315c8ab00ead3b4d7dcd0eee724ea8dbb" + }, + "mengsan/assets/portraits/wriggler-original.webp": { + "size": 9008, + "sha256": "b48ee157b976123f515ec7cd5dd6dfc0909c2eaa3735f1acc04ab8a388fd8ecb" + }, + "mengsan/assets/portraits/zhangfei-portrait.jpg": { + "size": 3004186, + "sha256": "f4d6e551a3098ecc869c6b53e8d24be50e8ca97f399ab987b96f32a53c75a02d" + }, + "mengsan/battle/battle-director.js": { + "size": 13125, + "sha256": "a82a031a5d92f4e734625a40793db883ef96bd4c9b56bfef5bc56f2b982e4aea" + }, + "mengsan/battle/battle-flow.js": { + "size": 5900, + "sha256": "1ae2db34176a7fc5d19e5001316e1f2aac4b1a883662bafc43af8cb95775970b" + }, + "mengsan/battle/battle-loop.js": { + "size": 3652, + "sha256": "8ff23c71320c91cee2955cabfd8384190a07b80cbddd44bba50fcf9fef4daeb0" + }, + "mengsan/battle/battle-resources.js": { + "size": 3095, + "sha256": "0da540600249f9addb23eecfc218930c2a376a0f357eb3a16b628d4c20817020" + }, + "mengsan/battle/battle-session.js": { + "size": 5561, + "sha256": "ab9880fc1934a87d1ca6640778605ec33e9812c94930567f868a84fd29e56ad7" + }, + "mengsan/battle/battle-settlement.js": { + "size": 12024, + "sha256": "74e6647cf5ff050962c3e171c1f48747e7a373fdcd5fc35b3ad9c170cbe8931f" + }, + "mengsan/battle/combat-rules.js": { + "size": 5966, + "sha256": "e540dc2e2eb83ac20868252cef73373d6c807455d57f220949aafd1f762e3828" + }, + "mengsan/battle/death-blow.js": { + "size": 1151, + "sha256": "aa3ba0da315ae81ae8430c50a57203889838adac8d533aa5400a4bb9976becf5" + }, + "mengsan/battle/engine-session.js": { + "size": 3763, + "sha256": "1addfa8f6a6e1e56d2dd32b6d1249ef84aa0aaa1ca049a7e0ccb688be51261bb" + }, + "mengsan/battle/intent-damage.js": { + "size": 1298, + "sha256": "616a18728b31677b70cc14b88af027c4cc912ae90f11de41355b900ed2c29ea1" + }, + "mengsan/battle/intent-effects.js": { + "size": 2411, + "sha256": "0f3dc871427bf718b64ce057a08d6b98b100e66fad9f803662818b03cda1e3c1" + }, + "mengsan/battle/skill-teardown.js": { + "size": 6849, + "sha256": "5f92d02d245aa3021ea47b245b221c58987b1e5770fe4329eef6ae733b0810ff" + }, + "mengsan/battle/stun-intent.js": { + "size": 1340, + "sha256": "898f9e64b80c5a963229eed340455f810eec2c1777bb98ba599d56101ba11bf7" + }, + "mengsan/bonds/battle.js": { + "size": 1771, + "sha256": "bdb4d429b14b98d7d5434afb2d11039938b7eaea37cf5f87a3fba86fc958b254" + }, + "mengsan/bonds/definitions.js": { + "size": 2505, + "sha256": "f2c02c24fa7704ce4800969c5e4183840c556386bb4fe29a51ef25070636c3df" + }, + "mengsan/bonds/intents.js": { + "size": 2952, + "sha256": "935f5f1ec90ba7e0cd0539db9c51ad5ba11e9063bbbd060a56f4ef6c34bfe00d" + }, + "mengsan/bonds/state.js": { + "size": 4436, + "sha256": "ea5e5fd29a695a6d65fd1ad4aeb8ed97f5dfbdb3b34f8748f099140cd27341e3" + }, + "mengsan/cards/affixes.js": { + "size": 1124, + "sha256": "f65993cabf6fe9b01e9a41de4ff0d47aba56eb67cd9e00d4a67cd87d9ed9bce8" + }, + "mengsan/cards/battle-card-copy.js": { + "size": 1737, + "sha256": "acffc9c0ecc7d22985f894522376e13d39067e71cf1532f2a2cd8d9466671fdf" + }, + "mengsan/cards/battle-card-reclaim.js": { + "size": 1551, + "sha256": "397a0c05591122d91bed20499cdd0b55f3fddcdaa6a2d6cf8d7f4434c1306feb" + }, + "mengsan/cards/battle-energy.js": { + "size": 1502, + "sha256": "cee19be46c41d274bf706c2add12f64834980ddb32526862e78de37bda28caef" + }, + "mengsan/cards/block-draw.js": { + "size": 1197, + "sha256": "4770aa0a753b80d103e3c087bbe2046609e99a05dcbc5c388526ba87c554b7ce" + }, + "mengsan/cards/card-data.js": { + "size": 3457, + "sha256": "0bafffc9e6e9c6bd8bae74948bc9b1742450adbda6a9dcb80206054c27bfbbe3" + }, + "mengsan/cards/card-definitions.js": { + "size": 3006, + "sha256": "b824439eb6808642fa55a81779f5b990e52382c85c9f256a6346f3e21331b9d0" + }, + "mengsan/cards/card-pools.js": { + "size": 628, + "sha256": "1830028ecefe5dedf09f27358d9235a91d6282dbdea46d410a48315afbc3b118" + }, + "mengsan/cards/classification-types.js": { + "size": 515, + "sha256": "10813deaaf218c68512e44da8eac9a83717b8b942a48df498cfae570c9f939f4" + }, + "mengsan/cards/description.js": { + "size": 524, + "sha256": "a37daf0e29fb05f8248ff3b9c9777e68e2dab6e7cf86a3c53510e099a1e10d1d" + }, + "mengsan/cards/hand-exhaust.js": { + "size": 2673, + "sha256": "904bd21885750bcb5cabee5478d74a9f5e61651d2b9aeaff1f7b41559f1665e6" + }, + "mengsan/cards/hand-upgrade.js": { + "size": 4359, + "sha256": "599859fc9a5283655778816847d2edff52c787b47b41f1e5e6fc6ac2160caed7" + }, + "mengsan/cards/havoc-play.js": { + "size": 1921, + "sha256": "dad0d45cbbd0a8f13a9aa0c238d14b3c6e7553eb3795e3291e2c53d5b69bd7d0" + }, + "mengsan/cards/infection-card.js": { + "size": 3056, + "sha256": "f39708a57dffe6ec86e176e4d3572fed9bad60350a83fadaf21d75e8d7f817f1" + }, + "mengsan/cards/intrinsic-affixes.js": { + "size": 819, + "sha256": "67630d860ce74ceb9ba9fb40e8f7a4b13dcf55c83277653392d23b5f0ea9c0e1" + }, + "mengsan/cards/ironclad-advanced-data.js": { + "size": 126, + "sha256": "e18fd3b159e910a02d26d4d0a9ea3d630b51fd71f734fb183b5542f6f777713b" + }, + "mengsan/cards/ironclad-data.js": { + "size": 117, + "sha256": "8a90e36e9375661d1e9273da6fbfea3c6ef1be8e0772c2d5899b518d1d127390" + }, + "mengsan/cards/ironclad-hooks.js": { + "size": 2240, + "sha256": "32eb74642d081bebd6e38b8e43c9fd8448163a4c93c1322c83e26798b3e556b7" + }, + "mengsan/cards/ironclad.js": { + "size": 120, + "sha256": "5317a2bf599d8a7fabcf19dfa4f76284cbe6b7f43ec5a3f8c607ab9e69d9f61d" + }, + "mengsan/cards/life-loss-block.js": { + "size": 1177, + "sha256": "718254e938a08be9e4fa036698fe5c33ac98c842722e1edebf7bb9906922f64b" + }, + "mengsan/cards/mode-cards.js": { + "size": 7770, + "sha256": "4a8f9d61ba438ea3ba2e857f0b81bd85bee2ffc1c9bd0bcbd18dbdeac36aec8f" + }, + "mengsan/cards/packs/colorless/data.js": { + "size": 32174, + "sha256": "4ba74b97668b97b99099efd19fdba2e81da02e2ab3764b332fc71cbd135b87cd" + }, + "mengsan/cards/packs/colorless/index.js": { + "size": 382, + "sha256": "b106052a24fc47b10472b907c69036e341a90a5945d32e140d67dc2c030a5f92" + }, + "mengsan/cards/packs/colorless/rewards.js": { + "size": 613, + "sha256": "af212d7c4cd8f63c5837e8f29519cd341b775135fa332f8a0f4f82eae4ac349d" + }, + "mengsan/cards/packs/curse/data.js": { + "size": 6704, + "sha256": "35d43aa2ada598257fe0d0c18f5ea746d67429c0d9a3d73b145dedb363fe50df" + }, + "mengsan/cards/packs/data.js": { + "size": 902, + "sha256": "4f04d425ba55670fe5707ef189dd3b74fa9934117fc5929c0f1b9b67c874863b" + }, + "mengsan/cards/packs/event/data.js": { + "size": 14820, + "sha256": "0badede4d570d625e371f3e06750d708c381f2eff3f14e54f1a803690f28c1c9" + }, + "mengsan/cards/packs/index.js": { + "size": 1109, + "sha256": "0711819c90af702cc98fc189c710bd278a9aa66022f04a3566ab18509b92af53" + }, + "mengsan/cards/packs/liubei/advanced-data.js": { + "size": 2842, + "sha256": "af05e8f5ffe996f11f1383023557f00d60eaccadc534a8fa8df3f9b57fc6b036" + }, + "mengsan/cards/packs/liubei/advanced.js": { + "size": 31216, + "sha256": "736ea6b7c45650fa4219c4dcafdcc61886c743f065469b22ad972728e7552595" + }, + "mengsan/cards/packs/liubei/data.js": { + "size": 4464, + "sha256": "8e8f9ce7f3a5867c848e54133a726007af2a534106ba9749ade0f8a86ad73bee" + }, + "mengsan/cards/packs/liubei/index.js": { + "size": 610, + "sha256": "49cd27d3341c93774bcaa05f1233d358962cdc3482c6dcfccb6d480cc5240dbb" + }, + "mengsan/cards/packs/liubei/legacy.js": { + "size": 55175, + "sha256": "32776f897128bfa7ab2e6dad1cbab4c09901fd0c093da599da7161fe5dca16ff" + }, + "mengsan/cards/packs/liubei/metadata.js": { + "size": 5618, + "sha256": "9b5dbdd73c6f901f1f005baecd1f70cf81997ca9cf0897e7309f5c5a73eb77cc" + }, + "mengsan/cards/packs/liubei/names.js": { + "size": 816, + "sha256": "d40ce599c1a9c21903ff6889ffb3f9d682d48eb269797ee960db9084d93905f9" + }, + "mengsan/cards/packs/liubei/rewards.js": { + "size": 12445, + "sha256": "5deddf3784b4e7ae8a757d4ff06481d0f32f1f31fed86b939cc1f9184e462396" + }, + "mengsan/cards/packs/liubei/runtime.js": { + "size": 21392, + "sha256": "ca6d9887fb9f8978c34ade455f564b3a0a52eceb5b844447e863390acdb3bed9" + }, + "mengsan/cards/packs/liubei/traits.js": { + "size": 8277, + "sha256": "38e1b081a3789f3760c4a0889b34803667006b326dce1f8e2aa2c862d997a549" + }, + "mengsan/cards/packs/rewards.js": { + "size": 327, + "sha256": "e7f15d41aa526c6b6ba93b137dd2e31f63fbace91d1bf8c3f8dbdd0fa6ac3176" + }, + "mengsan/cards/packs/shared/data.js": { + "size": 405, + "sha256": "93244ab064a100fa9a93f0cb19acc1d22b6ce17036b2f5cc71fd4f1db42f8497" + }, + "mengsan/cards/packs/shared/runtime.js": { + "size": 41670, + "sha256": "f1f6541bae4da3d38911e62f51fc930270c16b5a1b2c3a4348c0712925396a7d" + }, + "mengsan/cards/packs/status/data.js": { + "size": 4513, + "sha256": "16de512e265f4062ed9fa077d655a416147308b72d0893348c0ad8c27f5b89b3" + }, + "mengsan/cards/personal-piles.js": { + "size": 24202, + "sha256": "47c23c71b36d6643953a783b3a74c85494cd2f04880d9cbfa44db69e47cfb32f" + }, + "mengsan/cards/rampage.js": { + "size": 5265, + "sha256": "7563d2316cece8ab1d9b04de28b47e4562b82ad8a6dd493e1c7f64e726a6a94c" + }, + "mengsan/cards/random-hand-exhaust.js": { + "size": 1293, + "sha256": "486758ef934e381bd825a46b07e140f5f28e08eceadba25dd9f107ffefe80372" + }, + "mengsan/cards/rarity-data.js": { + "size": 440, + "sha256": "ab3858d78537599e2b108e321e9e92dff7b414d8fb94ae472f55a7b0a54d3ec7" + }, + "mengsan/cards/rarity.js": { + "size": 3011, + "sha256": "2e2df62319ba5a95d76abc2a1864febe162809f07478103da26925d593c55224" + }, + "mengsan/cards/retired-cards.js": { + "size": 1575, + "sha256": "ce2f18876f10dbc685a712c990a289731fcff3f2e02256363f9476c68b7bfb2d" + }, + "mengsan/cards/shared-battle-end.js": { + "size": 946, + "sha256": "424b1e24bd19ebaee6a890521bdc4e30ecb26e7145581311208b1d9375b972f9" + }, + "mengsan/cards/shared-hooks.js": { + "size": 823, + "sha256": "bcd757ce6c1ea9c4f56ccfcfcec7adc7b8184c9e4567f0f4b4519a8a867cd7a0" + }, + "mengsan/cards/slimed-card.js": { + "size": 1037, + "sha256": "e3cb9102bc28bbbd561f546dfe9cd6832a69e2336e9d989b1ad8d3636bf5e64e" + }, + "mengsan/cards/spite.js": { + "size": 1832, + "sha256": "6cef9cd617cfce569238dbc90db242140724a4da3e4963581649cd1848e15d90" + }, + "mengsan/cards/starting-deck.js": { + "size": 1668, + "sha256": "89367f707791d9f275b5fd5b6cf54b4d47e80fe9133b4a314a9a0bf4b9cdea3f" + }, + "mengsan/cards/status-cards.js": { + "size": 746, + "sha256": "a2c623938bd4d9264c605899bb94cb6599bfcd962ebb5c06a59cb084236ab325" + }, + "mengsan/cards/strike-counter.js": { + "size": 1534, + "sha256": "a3de2b465d19a2f14de20ad51e915b4e3459def64fb355627ed2791865a83af6" + }, + "mengsan/cards/temporary-strength.js": { + "size": 3764, + "sha256": "5b843d1f9367108e190a4170644c644f83e113d728ada4fb7d2a997f12ea2fc7" + }, + "mengsan/cards/upgrades.js": { + "size": 1892, + "sha256": "78f37bd5da682b3a1a3b679df25c026f256adeb5ccc48f6caa0be5d58bdb7529" + }, + "mengsan/cards/whirlwind.js": { + "size": 1581, + "sha256": "a62955b5d5291bc887e7c36b808cecbd189d1eb1506cd67873e7298f9369bf9d" + }, + "mengsan/cards/wither-card.js": { + "size": 869, + "sha256": "8e4cac00e3196c1a4315c36c63e6a0ce36621e7fc2f73e44fa811a424254d17c" + }, + "mengsan/cards/wound-card.js": { + "size": 1891, + "sha256": "2ac15f4252546c183b3de546bd6134a75f8b42ea67632f0d57b8fbd35a8796ea" + }, + "mengsan/config.js": { + "size": 875, + "sha256": "7fa71a17b12d31b1e8ce279b396c0969ddcf8986a6d6ce9e9583aa476b4685c9" + }, + "mengsan/content/acts/act1/encounters.js": { + "size": 3800, + "sha256": "62d9a8fd585cfbcc7cc5ad2916d1e5b30d42834e1f89167b7f670b36dcde54db" + }, + "mengsan/content/acts/act1/index.js": { + "size": 464, + "sha256": "b9d3989f85ad8893c755d7364a35488a4c170dba9c09aac2e7e3c7c70553d580" + }, + "mengsan/content/acts/act1/inklet-group.js": { + "size": 516, + "sha256": "bbc6ef9d321b8d04431acd03b2648fc71bb3cb4d892e695b6ac41d667d2ae22a" + }, + "mengsan/content/acts/act1/kin-group.js": { + "size": 776, + "sha256": "82e43fbd9e9ad997cf07cf5c633ae63f1fe7564156e0ab9429e6c2c713689e23" + }, + "mengsan/content/acts/act1/map.js": { + "size": 1793, + "sha256": "a43b1516a93509fa4035ae6c23d4b106566248161a61eb15983b36e0c9495747" + }, + "mengsan/content/acts/act1/nibbit-pair.js": { + "size": 475, + "sha256": "e339e7d91ea24a86b409b89d05a9d6c6846023de343f951f840f68c3baab4a30" + }, + "mengsan/content/acts/act1/raider-trio.js": { + "size": 1595, + "sha256": "a18f6bc3b1074bdfda23a8ea7887e4fd581a88698045f49e2ea94a6b989ea412" + }, + "mengsan/content/acts/act1/rewards.js": { + "size": 797, + "sha256": "dc9c450a77abe5cd35c4ab46dc8833e49287a9d63fa773bb0da1fd64525eb77a" + }, + "mengsan/content/acts/act1/stories/forest-ambush.js": { + "size": 5225, + "sha256": "de94b30bafc2e4251a6a70ecaf4b1ceaf300cc2750f71d30014971e1c389b240" + }, + "mengsan/content/acts/act1/stories/liubei-opening.js": { + "size": 5731, + "sha256": "1d61fbea5194a1960d9e6bc95d0f5b733687afae7d769a1ac9e0846be97341b4" + }, + "mengsan/content/acts/act2/index.js": { + "size": 129, + "sha256": "d91675bca78c4064b2876c6e9601395aa776f679fe40e63d2385b484d1767c37" + }, + "mengsan/content/acts/act2/map.js": { + "size": 402, + "sha256": "9a8a4a282402a8c25759408a57afba8d73a638d1f64d24407638b1f9156ce6a5" + }, + "mengsan/content/acts/act3/index.js": { + "size": 129, + "sha256": "d91675bca78c4064b2876c6e9601395aa776f679fe40e63d2385b484d1767c37" + }, + "mengsan/content/acts/act3/map.js": { + "size": 411, + "sha256": "274a4263af5db5e4d57e71702293c3a53fcc3ae9117e9c0ed5e86b6fa33ba6d1" + }, + "mengsan/content/encounters.js": { + "size": 870, + "sha256": "5f1cf6053693df8992cdece2a818ea56cbd6ddf023a60e42e2d0e54e27426fd2" + }, + "mengsan/content/registry.js": { + "size": 7581, + "sha256": "71a6aa7e0048992fb38e0a3084f793f4874deefd614bbba3a84d22dea45e0a7a" + }, + "mengsan/content/scenario-characters.js": { + "size": 9835, + "sha256": "608a47959212ece33171e7d12c2116701e09dac98634f97279e294192e03dfc0" + }, + "mengsan/content/shared/index.js": { + "size": 126, + "sha256": "25625b636c2b82e58c0792c40b7f32562abca99e0fd7861b73a74cdf40c708af" + }, + "mengsan/content/shared/rewards.js": { + "size": 9046, + "sha256": "ed2b83da3e322cd6e1d2bf15c5011757faf380e1b8343efbebdb06c83c311736" + }, + "mengsan/mode.js": { + "size": 113891, + "sha256": "a4f7498ef506f894890fbcbd3058fab0c13876657d3d207b3e99be9829702577" + }, + "mengsan/monsters/actions.js": { + "size": 34417, + "sha256": "97461914dd3a40e0bba6cb5e1ff41fe34208ab996dafcc64ed5a33d0d3cdebc5" + }, + "mengsan/monsters/artifact-status.js": { + "size": 2385, + "sha256": "b85161c9acc1d92715f4cb7be7603bfea8b5e6d42f4caa9d30835f38575ca03a" + }, + "mengsan/monsters/beast-intent.js": { + "size": 6805, + "sha256": "3962c0ab114907f692ee2d484311119d0c621dd460f00087875f6a9140886964" + }, + "mengsan/monsters/byrdonis-intent.js": { + "size": 3062, + "sha256": "59847f053122a0600d6b4c8b0a5573427da14717177fb1461640b8def9141443" + }, + "mengsan/monsters/crawler-intent.js": { + "size": 1587, + "sha256": "994561eea23a9983931d545ef76281e70dba575563a97059b8b5fc4501a4d40b" + }, + "mengsan/monsters/cubex-intent.js": { + "size": 2151, + "sha256": "bf4af01cbeb8a45ba47dd7f200fa96d808e18322b2908ed4254465644ed87903" + }, + "mengsan/monsters/effigy-intent.js": { + "size": 1462, + "sha256": "e244757471a7142e3b796be5f24f5a4765564d49fd6b7ddb1484883a46838a43" + }, + "mengsan/monsters/effigy-slow.js": { + "size": 3367, + "sha256": "d2c2cf715e24090df2be4e68ff15d55dfdebf862ad87fdfa592bbf7bf20f2f94" + }, + "mengsan/monsters/flyconid-intent.js": { + "size": 1743, + "sha256": "6ff2be31555c591a872cc0ffcc7aea99a7bc9b6bd5156993f8847156eef8637b" + }, + "mengsan/monsters/fogmog-battle.js": { + "size": 3856, + "sha256": "c5cba9d10c25f7ece2290110f787e4fc3dfa50f266a9faac4bfdb0ac957868e4" + }, + "mengsan/monsters/fogmog-intent.js": { + "size": 2160, + "sha256": "367bf74fef71ff55d398902fd14512af5bfecc82e756237f73102fe116381c4e" + }, + "mengsan/monsters/inklet-intent.js": { + "size": 2925, + "sha256": "db404520db50e2d052beb236578973ae4b61e61c6a57a366c8b63452b01e19a3" + }, + "mengsan/monsters/jaxfruit-intent.js": { + "size": 1256, + "sha256": "46abbda7bf21af236f5e7a4c08a5739649bcf46ea70c59dcb18b5226b2568bb8" + }, + "mengsan/monsters/kin-intent.js": { + "size": 4393, + "sha256": "3321a96df24d651c2bfea7a7ccfd920be20ae75fa2cb8543abb5985cf7501835" + }, + "mengsan/monsters/leafmedium-intent.js": { + "size": 1462, + "sha256": "ae373c29e39891992b857afaf11befac1354f163026a797e8bf13ad8d81b71ad" + }, + "mengsan/monsters/leafslime-goop.js": { + "size": 1653, + "sha256": "133da2f627601e79aa51b6eb87acff777a3ca60c74d142b0d205c40ccbfef3f0" + }, + "mengsan/monsters/leafslime-intent.js": { + "size": 1403, + "sha256": "3c8f841fb8a02683134a18d1197320b1bb03101f8765e069684654f9ff9ee3c8" + }, + "mengsan/monsters/mawler-intent.js": { + "size": 1899, + "sha256": "23bfd30298779d9a41c28a3100fb026e17ecd2d1e066f0bb842c57a9ee56b833" + }, + "mengsan/monsters/monster.js": { + "size": 11658, + "sha256": "1464b42fce32b9266724c8f0eaa06923c1171cff539c337c96af1cc32bd7bc8a" + }, + "mengsan/monsters/nibbit-intent.js": { + "size": 2292, + "sha256": "b3e34455e8cd88528069345aec2f7f55938b52e05fef4af9b8cd803bbe1579b3" + }, + "mengsan/monsters/phrog-battle.js": { + "size": 2197, + "sha256": "0c4621e858cc4a4b0e6d1fa86f7d5e06784dfc4ed4e9aba1342c65f6b80f9d02" + }, + "mengsan/monsters/phrog-intent.js": { + "size": 4060, + "sha256": "e058edf8b6febadcd685dc7c16c05d90e8ba56c1d9b26c132e4ef2fc0e95d9df" + }, + "mengsan/monsters/raider-intent.js": { + "size": 2060, + "sha256": "b15b89d53914c472c3ef4f2e54e3551bb6a80cd783c21d340676189c2e22c948" + }, + "mengsan/monsters/shrinker-intent.js": { + "size": 1491, + "sha256": "758ea3ef47d0cf529f09b9733c9fda02c029f7eb30f7ca12b1ccd506267ef90b" + }, + "mengsan/monsters/shrinker-status.js": { + "size": 2356, + "sha256": "08a44974fb5064ea0f83b3b2d3d6d6030508ef8bef5ed4d8a91f274a8e6b95bf" + }, + "mengsan/monsters/strangler-constrict.js": { + "size": 3054, + "sha256": "87b012405f7840e5a064e994e9413e8671e4aebe954ee9fd03c9798ed78efe61" + }, + "mengsan/monsters/strangler-intent.js": { + "size": 1578, + "sha256": "b5ea12fd74db365592ace7c55671bad8b18aa34da9a829852fc1a0e3cf1f229a" + }, + "mengsan/monsters/twigmedium-intent.js": { + "size": 2576, + "sha256": "c27f58e02f08b87ed62865fc11bb3ae43b5d2098ac199c5714946d24e0afaa87" + }, + "mengsan/monsters/twigslime-intent.js": { + "size": 1299, + "sha256": "dd772c38e732ebcdda6e5b0ecd4725f73b36cb880ff156f3aaa64470a53633bc" + }, + "mengsan/monsters/vantom-intent.js": { + "size": 2266, + "sha256": "29c0f36f5b060866e5c28c842883d951d55ceb4f0df7a7e204905fd6d803045b" + }, + "mengsan/monsters/vine-intent.js": { + "size": 1229, + "sha256": "e2bc8348d9ef5d8ff38b3d65a0b05b741fef31cf12ca6b6abe7b81ce58515b00" + }, + "mengsan/monsters/vine-tangled.js": { + "size": 2214, + "sha256": "e8eb1e501af16d5da2157ff357bef3193233b4fbcd62cd823a4a6114124abd7e" + }, + "mengsan/progression/dialogue-choice.js": { + "size": 837, + "sha256": "e00707810480b281bf3e8c25923cf09997d06cf2dc3b7eea35c5a1a47c5cd3dd" + }, + "mengsan/progression/encounter-progress.js": { + "size": 1555, + "sha256": "e674c332baef9b3baffbfed3c86c41cd38a5c95bdd4fb3b1009a9819e90a06ba" + }, + "mengsan/progression/map-checkpoint.js": { + "size": 314, + "sha256": "c0b5fa06fda99cf4d06570cb7e95fafc141b355055d20b0bd8f7ff91f1c1afa7" + }, + "mengsan/progression/mode-storage.js": { + "size": 3833, + "sha256": "83ce9bb6191644f547a0dd79bd46ba86be149047b3e24844a4757270f6a09fcf" + }, + "mengsan/progression/reward-package.js": { + "size": 1561, + "sha256": "2cee489f48e5f9d4bfbb9fd1e50dc6d41cf9f778b21432e5aea1bb6730b21364" + }, + "mengsan/progression/reward.js": { + "size": 3332, + "sha256": "77e29120ef5bc20b27865c342e5abda0357063b591e226908f8c4bc0f5320c21" + }, + "mengsan/progression/state.js": { + "size": 14841, + "sha256": "82a9b2aef527046c9ffb4bbfc470ec2230e6ec42881467a643655a5723a79269" + }, + "mengsan/progression/story.js": { + "size": 737, + "sha256": "4db3cf98d5a102cf5bf6d3022b0bdb4c7d70430537e2c4a6a9e541822951f016" + }, + "mengsan/register.js": { + "size": 791, + "sha256": "95290f693f1d5bbc592e35a3cd063201f46d5deffbd0e281682f371b02ae342e" + }, + "mengsan/relics/battle.js": { + "size": 5015, + "sha256": "976e13531625ba00d2941d482d36d9dfd5cf22d1b8de66b519e8b3b928f8a6c9" + }, + "mengsan/relics/combat.js": { + "size": 4101, + "sha256": "1fd264d6d07be511cde006ab18f14e5ac8170ad539083a20a3259b1b70548202" + }, + "mengsan/relics/definitions.js": { + "size": 4707, + "sha256": "894f66d2a98761926655124f0cbd05acd20aaa8335d61d838fd3cd6bf550d224" + }, + "mengsan/relics/rooms.js": { + "size": 1032, + "sha256": "88af2a408d062f78f98dded9e7de4a51c98468eefe217e827d43aa34e3a7410f" + }, + "mengsan/ui/bonds.js": { + "size": 4499, + "sha256": "2580f60f3b9aac29206a8e1198322505a2283bf81ea379d3a6ca31251f42d5e3" + }, + "mengsan/ui/card-library.js": { + "size": 20776, + "sha256": "7d5d1be851490b9d1ccda7b3acf621bf2dbb608f47e0284a200ce788889d7f90" + }, + "mengsan/ui/dialogue.js": { + "size": 7842, + "sha256": "e61b22cf27f810a45404e0d05969b03e0a6603d581f8cf7a052a3c30348325f7" + }, + "mengsan/ui/energy-display.js": { + "size": 511, + "sha256": "37bc74971d52429c7e1c3d9d0c2dcccb29a61e968ac1bf4eb0a2330801d8fd69" + }, + "mengsan/ui/flow-ui.js": { + "size": 3106, + "sha256": "f19d26b3ccd5579885bd9fe9a3444f4d06125898020b165acf91ad2d1f6a37e0" + }, + "mengsan/ui/gm-manager.js": { + "size": 4727, + "sha256": "1ffcf493ffcc4d9b16911e81ff62b15693213355c3226270e5224b03042e8868" + }, + "mengsan/ui/hand/adapter.js": { + "size": 955, + "sha256": "dbec5a22b19ba7790c2118328701331b9065cdd7877ad237fa6ba635aad0ab6b" + }, + "mengsan/ui/hand/diagnostics.js": { + "size": 740, + "sha256": "9ea7729febf7cba177dcf76ff8a65ef3c0f44ff3e8cc020bbb39a4654837293b" + }, + "mengsan/ui/hand/fan.js": { + "size": 4355, + "sha256": "55e1e01dda7b4231c8e1a2fadf6ccbee39b5e98c543bc55500c6493cf6cd51b1" + }, + "mengsan/ui/hand/index.js": { + "size": 9105, + "sha256": "a236f0ed58ad3f7953cc79b04d688efa3c7a394d2fbeb3d5ac2eef15f7242fa7" + }, + "mengsan/ui/hand/input.js": { + "size": 12278, + "sha256": "b04df574792e6e4be7cce4e0d8bd682efeb091b9b752b40a0b110575442adfbb" + }, + "mengsan/ui/hand/overlays.js": { + "size": 3135, + "sha256": "d89e62b5c73b64dfbd2f449e95b6362d2273ef57bc8db72f552af200d3dc2ad8" + }, + "mengsan/ui/hand/style.css": { + "size": 35634, + "sha256": "c31ed6dd5b7b2e9d48125f8503512d3af2c0050cb9144dcfdc9d571c351450d6" + }, + "mengsan/ui/intent-display.js": { + "size": 10744, + "sha256": "a920a929183f2815c6ef4226af0585be0f876196b6a061e287b02654068f7083" + }, + "mengsan/ui/map/index.js": { + "size": 62, + "sha256": "6fb7e6e2547eec9d421727b9d6794e3c96d8e3a77984d7688544a67a00c4999d" + }, + "mengsan/ui/map/renderer.js": { + "size": 25409, + "sha256": "a9282e2e6ce30973be064a73ce01b5e13cd580179bde24f9482661e9a2b94571" + }, + "mengsan/ui/navigation.js": { + "size": 5274, + "sha256": "22bf6b5ec02c4917311540aac56c1c20b43df5b761acdb867937399db2c2c7c6" + }, + "mengsan/ui/relics.js": { + "size": 3452, + "sha256": "98049bd5a21a314475211fdb681b4989075b8eca4d33d58e88f4fd4cec19455e" + }, + "mengsan/ui/reward-package.js": { + "size": 4316, + "sha256": "1c584d2c5bd7252eeaeeaa1da5dbfb11c3df7c364dab0c82f557ca1981afb70a" + }, + "mengsan/ui/reward-ui.js": { + "size": 15399, + "sha256": "ad9e727b90aef2cfcea2a7f757b5c12e43655ea055d0729fd955173076a80ef9" + }, + "mengsan/ui/style.css": { + "size": 78961, + "sha256": "beaa52e86c3f30144c55333c110e973944180c5d2f17f275821ce75cfbdf2a37" + }, + "mengsan/ui/virtual-card-grid.js": { + "size": 4509, + "sha256": "0d8aa28041246a20895a289ab360e982044b2028f6c03090a0039ce85f7e2977" + } + }, + "tree": { + "type": "directory", + "name": "术樱包", + "children": { + "mengsan": { + "type": "directory", + "children": { + "assets": { + "type": "directory", + "children": { + "card-overlays": { + "type": "directory", + "children": { + "infection-sprite.webp": { + "type": "file", + "size": 677174, + "sha256": "96ec222b2cb1a76d524ba5a947a21fb140944409ef20acb453c645cee520fea4" + } + } + }, + "cards": { + "type": "directory", + "children": { + "mengsan_baozou.png": { + "type": "file", + "size": 288919, + "sha256": "ccfea8a1595a41528d93de7b90d78302e035dc42a058bd73a3d314e39c777306" + }, + "mengsan_colorless_alchemize.png": { + "type": "file", + "size": 3127713, + "sha256": "030695f8c986e22f1ec74496da907f050c3439f9dd58db362af08da92af20164" + }, + "mengsan_colorless_anointed.png": { + "type": "file", + "size": 3086399, + "sha256": "3a68cb906e29c366bfb8beab3958276050e24f9dc77d854ae71debe095446838" + }, + "mengsan_colorless_automation.png": { + "type": "file", + "size": 3162166, + "sha256": "00b0f61f95fbd9118c3ecce4c68981ad59c970805f3c5fd4e0637ccaa704f0f5" + }, + "mengsan_colorless_beacon_of_hope.png": { + "type": "file", + "size": 3014849, + "sha256": "038bee1de6075fdcd9a2b1269ba3cd040e76727a6a3b2d36e41e1644bb631adc" + }, + "mengsan_colorless_beat_down.png": { + "type": "file", + "size": 3261861, + "sha256": "476af9e96f7c3531ea0251ddb4f3ff87801054960ded214aba5f8656473a0473" + }, + "mengsan_colorless_believe_in_you.png": { + "type": "file", + "size": 3193359, + "sha256": "43519b09e365e4e91f6771fa09943969ea96ffc05300ea17a0019bdc7ab32943" + }, + "mengsan_colorless_bolas.png": { + "type": "file", + "size": 3203088, + "sha256": "404d32ecb50eee1570ce743c43457c589839c98c7a63b746225bd0d2fd866c8e" + }, + "mengsan_colorless_calamity.png": { + "type": "file", + "size": 3043506, + "sha256": "2832971475e0cd42f28ce19009b4dc72ac13fdfd21c84bd94ce3ffe0ca801c25" + }, + "mengsan_colorless_catastrophe.png": { + "type": "file", + "size": 3311872, + "sha256": "ea81434a87f2277cbff5403a46399f7e73d1c7007cf3a60a6521a277c6e9edf6" + }, + "mengsan_colorless_coordinate.png": { + "type": "file", + "size": 3277184, + "sha256": "f05c1f215783e1fe97f920931afd20a8a4686036088da6f34212231a472db2a2" + }, + "mengsan_colorless_dark_shackles.png": { + "type": "file", + "size": 3036771, + "sha256": "b43aafcbe4882f3a4ff4881615f409ac193ea35156956ec9f4d2150de7f8c2bb" + }, + "mengsan_colorless_discovery.png": { + "type": "file", + "size": 3231144, + "sha256": "1d6c186eccdb7d1850c064ad25eb309cba3d8fe32238248813e92c47424945b9" + }, + "mengsan_colorless_dramatic_entrance.png": { + "type": "file", + "size": 3354223, + "sha256": "8497a4026aac2aa838251157cf53ee3cd5d7cbd420ab436927985c94332033e9" + }, + "mengsan_colorless_entropy.png": { + "type": "file", + "size": 3347868, + "sha256": "9672138ec7f8f5360efe7198378e2aa36898299c199c41727e25db0efb7a6632" + }, + "mengsan_colorless_equilibrium.png": { + "type": "file", + "size": 3135158, + "sha256": "109a7369ec0f3a473d8ffec5e527dc1b7c22d929c90547bbc3cc4db333bc8aa6" + }, + "mengsan_colorless_eternal_armor.png": { + "type": "file", + "size": 3167981, + "sha256": "3b532054c25452f093015ff92cb0bf4e15085baf367f42e56533992e6a6339cf" + }, + "mengsan_colorless_fasten.png": { + "type": "file", + "size": 3134484, + "sha256": "62c627389524fd5aa90c30e2e37e70365946b61fb162f5f1084cb0a677b7ad33" + }, + "mengsan_colorless_finesse.png": { + "type": "file", + "size": 3200227, + "sha256": "f0c3ce167fc36bdade027f9c89cfda0aebe2857e0e442ab8da83dac0062cb88b" + }, + "mengsan_colorless_fisticuffs.png": { + "type": "file", + "size": 2986131, + "sha256": "3424c53ede2dd31def9f4b36290b403e976b00e6d4acf9d1090a1ad3bff50355" + }, + "mengsan_colorless_flash_of_steel.png": { + "type": "file", + "size": 3254260, + "sha256": "9638fbd12bdae0d856c0d7c514b88668d1925d53ff8e0b18f46cdf6363c6e41d" + }, + "mengsan_colorless_gang_up.png": { + "type": "file", + "size": 3454683, + "sha256": "151638d8f635a72652e5ed9653647a5c4f24b4285032c17d9d85441f4dd1c424" + }, + "mengsan_colorless_gold_axe.png": { + "type": "file", + "size": 3241447, + "sha256": "5a83405d841ff7b5c40d0aaf37507b3274ae2e98e75070bec1f742da11bb10e4" + }, + "mengsan_colorless_hand_of_greed.png": { + "type": "file", + "size": 3204239, + "sha256": "e3f35625547fde12c1646385b1def672d18b6b03373898bb44c3ff535c307811" + }, + "mengsan_colorless_hidden_gem.png": { + "type": "file", + "size": 3250322, + "sha256": "600be77b409fef66f260c8bdc0c25c46148064977f3cb5c8cb8e023b4da679ed" + }, + "mengsan_colorless_huddle_up.png": { + "type": "file", + "size": 3133981, + "sha256": "2783d26a011a442f1ee7ba2b8f50bb889886eab91685e5fff9d6d38cb0656da0" + }, + "mengsan_colorless_impatience.png": { + "type": "file", + "size": 3249719, + "sha256": "00f00214e278a79fe138941153a59843760b2b0f3280d192628619f75f541d79" + }, + "mengsan_colorless_intercept.png": { + "type": "file", + "size": 3205359, + "sha256": "d2dbb2979f3944cdd18cba6147e989ee3f78f8d6ed91665dfa1b3f7fb97e6352" + }, + "mengsan_colorless_jack_of_all_trades.png": { + "type": "file", + "size": 3402938, + "sha256": "a9b06fe1f21f3a86d5cb16a0b312c3d6788c62b5bbabf75a844de431828ecdd6" + }, + "mengsan_colorless_jackpot.png": { + "type": "file", + "size": 3222698, + "sha256": "c9f9dbc67abcefd6576ff936ece05c274b4e682cc4b9e252b10feba78c4ba9f8" + }, + "mengsan_colorless_knockdown.png": { + "type": "file", + "size": 3328270, + "sha256": "402647d62ae54ea742243185bf70e4ad95f87469cffcebb3e35d1826985cfdcb" + }, + "mengsan_colorless_lift.png": { + "type": "file", + "size": 3150681, + "sha256": "7a57e2e134a0e7579d8ea3d1b65a551b41f5814ad2b57c73a6afd1cf3751d71f" + }, + "mengsan_colorless_master_of_strategy.png": { + "type": "file", + "size": 3113651, + "sha256": "441e69484c575409c7d8201cc39c21956111b9462aec4aa10405e5f6f3815d46" + }, + "mengsan_colorless_mayhem.png": { + "type": "file", + "size": 3221855, + "sha256": "a30d6f318aa0969c7867a31669df515f87296c4ec65e7d03bd9aeac1f65ca3f6" + }, + "mengsan_colorless_mimic.png": { + "type": "file", + "size": 3092367, + "sha256": "23e33b4d6005c9667d54d821114bad372cde5885a3dcd0b36f0373947f518aaa" + }, + "mengsan_colorless_mind_blast.png": { + "type": "file", + "size": 3146553, + "sha256": "d7596195551d4c555dfd0e0841eea31666256f1a25f0e1126a242c9fb85e3999" + }, + "mengsan_colorless_nostalgia.png": { + "type": "file", + "size": 3191912, + "sha256": "99dbe6ade64d2f09fabae292dd8b3fa3a603ad55012c4ac7ca5cf61dff4e5ee1" + }, + "mengsan_colorless_omnislice.png": { + "type": "file", + "size": 3221281, + "sha256": "d64936d9fcdbea09456eca5af79866b679532a77a03ba6f7d179d6c1225963d5" + }, + "mengsan_colorless_panache.png": { + "type": "file", + "size": 3393936, + "sha256": "154f52ded899d099c10b42a2551526c992c5b8bb013d29d76edac935780e73da" + }, + "mengsan_colorless_panic_button.png": { + "type": "file", + "size": 3161638, + "sha256": "c88fdd70d1ca36b09e1038b5bb17757621c7cedde849c52e50890c63fb7e6c13" + }, + "mengsan_colorless_prep_time.png": { + "type": "file", + "size": 3210939, + "sha256": "235350559f4b50b207907ea6217fe5a19381d297425c97862e9cb5efec556dc8" + }, + "mengsan_colorless_production.png": { + "type": "file", + "size": 3192726, + "sha256": "1290f77a24dc3a7287e61ff7ef70f2c94033915d6996c3e22e039e9669cb8651" + }, + "mengsan_colorless_prolong.png": { + "type": "file", + "size": 3059311, + "sha256": "316111895375c230029e76f1ef674e7cb08e9e58472a90b4267b182c28e9221e" + }, + "mengsan_colorless_prowess.png": { + "type": "file", + "size": 3228141, + "sha256": "bb7a67bd845a376174dbb28627bac4771454cb50bbadc24cbc2a26391d9badc8" + }, + "mengsan_colorless_purity.png": { + "type": "file", + "size": 3190337, + "sha256": "00aad33dbf174fa6e5fe3f0231ee1e1ddcfb0e13011132e2ba34179400ee33c8" + }, + "mengsan_colorless_rally.png": { + "type": "file", + "size": 3267674, + "sha256": "315913dd9c6106d1a5833eabc5f0caa88ebda9dbed72440aab2fbd46bab14bb4" + }, + "mengsan_colorless_rend.png": { + "type": "file", + "size": 3293457, + "sha256": "36a641178ee9c13eaf78cc96656e445e5147d09b07ca2b05c82e0a30c440ad26" + }, + "mengsan_colorless_restlessness.png": { + "type": "file", + "size": 2907076, + "sha256": "e1d87fa3e36dd61fd5b84779f6ef4b0f0341c3216cca763c83e6d9a1dc1fe737" + }, + "mengsan_colorless_rolling_boulder.png": { + "type": "file", + "size": 3318250, + "sha256": "d9e7faabcde18d42c6dcc4f66ab0e822a930a8fba0e2060f42d1655991cbd253" + }, + "mengsan_colorless_salvo.png": { + "type": "file", + "size": 3228759, + "sha256": "00da2f4a4bc69909cd89b187b96776d104a667ddf728d330ffd12a0aec4c222e" + }, + "mengsan_colorless_scrawl.png": { + "type": "file", + "size": 3092946, + "sha256": "72a71d758a258a37437f26106cab486e55c26a451fc19bb1abf885f0da7b8f44" + }, + "mengsan_colorless_secret_technique.png": { + "type": "file", + "size": 3031198, + "sha256": "20a686c1a98bc730af4ced690487ec6f0d3d140be2b5c474696325a648958f2d" + }, + "mengsan_colorless_secret_weapon.png": { + "type": "file", + "size": 3100884, + "sha256": "05858a388df685c1dbc2b60e6a3bb6e441d8991d01a3293b0613d8eb71a22bfb" + }, + "mengsan_colorless_seeker_strike.png": { + "type": "file", + "size": 3226533, + "sha256": "6dac80c8ff7afe7659ad69cb4d2db02c366c0c02e6088e9bd395418e9b74148f" + }, + "mengsan_colorless_shockwave.png": { + "type": "file", + "size": 3123692, + "sha256": "c4b10848314eeb75e3e4ca588f4a34f886cfbcd8f585d5b68391ff8d3affb16f" + }, + "mengsan_colorless_splash.png": { + "type": "file", + "size": 3008746, + "sha256": "0a8a4393dc4462de1f33bd03a1173792a3b29d4f92a235dd1b6dd0ecbc41dd6d" + }, + "mengsan_colorless_stratagem.png": { + "type": "file", + "size": 3124646, + "sha256": "55375fcebdaa50dea41f55ef29d349880beca781d3e583584d9d8e377a498247" + }, + "mengsan_colorless_tag_team.png": { + "type": "file", + "size": 3222820, + "sha256": "62f3a67a893d485c0a376ed96e7d0a97c69429d7648cd4075f72a8b074878907" + }, + "mengsan_colorless_the_ball.png": { + "type": "file", + "size": 3304542, + "sha256": "4e3673066a4b80a7c902896fea95f42e9fb9b71a058bb4309ee085df7f0d8a2a" + }, + "mengsan_colorless_the_bomb.png": { + "type": "file", + "size": 3150065, + "sha256": "b2d0d8799316933ae0bed6cd3b925bf729b448de3daf1d769918b5a41d79de78" + }, + "mengsan_colorless_the_gambit.png": { + "type": "file", + "size": 3194137, + "sha256": "cd37df03c949469af917d0150f73416cf05cc087dfb67fc9879d69238fdbff74" + }, + "mengsan_colorless_thinking_ahead.png": { + "type": "file", + "size": 3141303, + "sha256": "7bd6b4edb3e8bb92d8b90773df68570831c1be948a103a426433191dd5135b60" + }, + "mengsan_colorless_thrumming_hatchet.png": { + "type": "file", + "size": 3114116, + "sha256": "e7f9630a4d218eba20870c15d93a96eb2d5bea7f8697c4792832dd99e91e705b" + }, + "mengsan_colorless_ultimate_defend.png": { + "type": "file", + "size": 3310196, + "sha256": "45d4d4418254f13366deea71137d5b891177b49c981ac9c83293a000f7e8462c" + }, + "mengsan_colorless_ultimate_strike.png": { + "type": "file", + "size": 3222462, + "sha256": "25652e3b55614c84b25c27ae63605bae88bb0e284d4c6181474d0e2cca9e074a" + }, + "mengsan_colorless_volley.png": { + "type": "file", + "size": 3086676, + "sha256": "fdd8ca3936a72134d3085b34d69c5bd73b1e80d6f443908aa2bd48fe13b52892" + }, + "mengsan_curse_ascenders_bane.png": { + "type": "file", + "size": 3156093, + "sha256": "f12a36ffcd6f0dba728fcc7eddfef3323c04a33115bd4c3e53e3a4931b25587d" + }, + "mengsan_curse_bad_luck.png": { + "type": "file", + "size": 3075399, + "sha256": "3aa36640e3a47477931176fadd7423ab0016b2d0b53e1d293b933f421aa723f4" + }, + "mengsan_curse_clumsy.png": { + "type": "file", + "size": 3320908, + "sha256": "d4fd3da9b1dc49625f7e2d5d612234cb5f57ab75616ce1c2c5eb609e893dd628" + }, + "mengsan_curse_curse_of_the_bell.png": { + "type": "file", + "size": 3190971, + "sha256": "d52d05e8ee1c3355ec34440694538049f6c3a9fac591d0a2d44947a9dde2ff7c" + }, + "mengsan_curse_debt.png": { + "type": "file", + "size": 3206708, + "sha256": "073aa84c0268bffae9cc017222bef8a1a8e8ba364abd6b64c3dee61a382f8c6e" + }, + "mengsan_curse_decay.png": { + "type": "file", + "size": 3159488, + "sha256": "4e4bb998c9d410bac843201e1161bf85da03797d3e7f46ebb22943e3bf2c45bc" + }, + "mengsan_curse_doubt.png": { + "type": "file", + "size": 3272171, + "sha256": "aa0e651e89cb3199f9568e5c00d72b842be96104a96698b8b484b8755862cce1" + }, + "mengsan_curse_enthralled.png": { + "type": "file", + "size": 3093077, + "sha256": "08df7fda97faf5bacbd9402eaccad5f03e16a69364464a6a97ada181e3fa3ac1" + }, + "mengsan_curse_folly.png": { + "type": "file", + "size": 3100483, + "sha256": "02707297707414f7eca6da31881b57f70d9d9805a06498f2363ec5793a91251b" + }, + "mengsan_curse_greed.png": { + "type": "file", + "size": 3283765, + "sha256": "0ba7674f5e8a2d07360389f45330666fc793e55c5f77abfd500f513db2bf9fed" + }, + "mengsan_curse_guilty.png": { + "type": "file", + "size": 3026762, + "sha256": "69bbe906990f27813ac542428549e33c29285cb44bf521515000c1c1d0702e83" + }, + "mengsan_curse_injury.png": { + "type": "file", + "size": 3232271, + "sha256": "ae043cfe8f0e5fb50ed75287b82494b7b38a918a331b6bf6a017e3283f4c4226" + }, + "mengsan_curse_normality.png": { + "type": "file", + "size": 3015776, + "sha256": "c2dd8e2fe83165c8129a2ff05ae1b86d5f4cfa258420280ccf49cd0d033f2c01" + }, + "mengsan_curse_poor_sleep.png": { + "type": "file", + "size": 3195332, + "sha256": "b3934757b27c91e96322227b64bae769addc87338cd9d2b18c0bdf8169165b7f" + }, + "mengsan_curse_regret.png": { + "type": "file", + "size": 3197963, + "sha256": "72f758eaa98a1fe254f025cf4096807498a8b9536a4cd085a6f8ac6cec9af2c9" + }, + "mengsan_curse_shame.png": { + "type": "file", + "size": 3159251, + "sha256": "e7090eb518f5ff5c022c77ced36866e3477c62319388feb99045ac0a24fcb289" + }, + "mengsan_curse_spore_mind.png": { + "type": "file", + "size": 3210425, + "sha256": "00dca10bcf03085d3d343c14fcc3703c5f3b3170ab992e74069a2cd730117177" + }, + "mengsan_curse_writhe.png": { + "type": "file", + "size": 3277925, + "sha256": "2dbaee5d8d3070f01d6bb0503c3235b6601fe4af7b5217fc4e17271161f7ed10" + }, + "mengsan_dazed_shuying.png": { + "type": "file", + "size": 3232164, + "sha256": "422244600455c716960bcc5f1aaf028733061b7f5b01e174125b9a46cab200ad" + }, + "mengsan_event_abundance.png": { + "type": "file", + "size": 3175446, + "sha256": "829339ea6c9f232b2004c179782393036629709e8eec6949cd6aa0d8c1b37950" + }, + "mengsan_event_apotheosis.png": { + "type": "file", + "size": 3202650, + "sha256": "3e213766a173b3c01004a5faf3b53481233f19375f4281929196d75dff37573e" + }, + "mengsan_event_apparition.png": { + "type": "file", + "size": 3276116, + "sha256": "07100920c178b5bfd383e5ced6fb9d6353c8d1995c80e9fedf409c7184cb0e0d" + }, + "mengsan_event_brightest_flame.png": { + "type": "file", + "size": 3088445, + "sha256": "e3abc30102a32ca9f91fb9af30bf3cd2790ff550388090d117e7978e31dede15" + }, + "mengsan_event_byrd_swoop.png": { + "type": "file", + "size": 3303389, + "sha256": "3769b6f25018f510ad489bfcdc5cb327c02c5aafd0a1b9ca780792b3c0fe13a8" + }, + "mengsan_event_enlightenment.png": { + "type": "file", + "size": 3306552, + "sha256": "5eb347d0984e569e43b792b044abd478d53d9cb9c9eecc5ba4e97773747a2699" + }, + "mengsan_event_exterminate.png": { + "type": "file", + "size": 3481990, + "sha256": "a0e53996ab3943760329f1aab87e1bc414964f5b88a19778415beecd61cb98be" + }, + "mengsan_event_feeding_frenzy.png": { + "type": "file", + "size": 2991504, + "sha256": "e7261af1c355fc358b15a8131d9b60909b6ba30db94b4d6f948802c3e3158c92" + }, + "mengsan_event_mad_science_chaos.png": { + "type": "file", + "size": 3225466, + "sha256": "2aa820d8c2f380eba5108df919295c4a509c33360bc934e678c72a60fa18d902" + }, + "mengsan_event_mad_science_choking.png": { + "type": "file", + "size": 3144566, + "sha256": "a8e5960f8d10e1a8ad9e390d309d50525f8831b82d139b3e65fe6caa3653e31d" + }, + "mengsan_event_mad_science_curious.png": { + "type": "file", + "size": 3128414, + "sha256": "2f42323201b656cfaba157c9f45ba8795518bb5cd5a4a65af9d6d8c5474d7a57" + }, + "mengsan_event_mad_science_energized.png": { + "type": "file", + "size": 3208057, + "sha256": "3de4101bff5223dd7b115294b67835fc7a849ca667ed729e6b6b8576125ffbcf" + }, + "mengsan_event_mad_science_expertise.png": { + "type": "file", + "size": 3158770, + "sha256": "a28fbf79892ab046388381a20e435f69bf53a6ba21148a3741c284989742a4b7" + }, + "mengsan_event_mad_science_improvement.png": { + "type": "file", + "size": 3101461, + "sha256": "f85a01280f85571848917e9375a122e17e7dbe0d6750766cbeeba18ffc85927f" + }, + "mengsan_event_mad_science_sapping.png": { + "type": "file", + "size": 3167779, + "sha256": "0cff4b294bbbc3c0097fc6036fd1febf49a82cb777b85b1ef4e2b7cf1aa0d344" + }, + "mengsan_event_mad_science_violence.png": { + "type": "file", + "size": 3217299, + "sha256": "07eb2165f6bda2bfce8c8267c83ff7caefc617105a33b0e99dd8e3c99582a97d" + }, + "mengsan_event_mad_science_wisdom.png": { + "type": "file", + "size": 3108692, + "sha256": "ff24a151489315ad9cef2bdec1d3adaa0dfd3bc5590870ac333b8af9d019d787" + }, + "mengsan_event_maul.png": { + "type": "file", + "size": 3146092, + "sha256": "7d66cab5f938b31c6191be72693251fb28b447a6a357f4888af712296d8b6090" + }, + "mengsan_event_metamorphosis.png": { + "type": "file", + "size": 3255357, + "sha256": "5af1bb8458b9533114d88eef94c91730094fa7d23190406fa86910f5006a97bf" + }, + "mengsan_event_neows_fury.png": { + "type": "file", + "size": 3270494, + "sha256": "e77228e317921968bc584f015c5cb5a018093eb8e8f8e14f68655d088fbd7175" + }, + "mengsan_event_peck.png": { + "type": "file", + "size": 3245605, + "sha256": "d7fa9557a82e46219917686a77fe1341cc57f843c166f65c63e8895885f29f88" + }, + "mengsan_event_relax.png": { + "type": "file", + "size": 3151165, + "sha256": "6cb2956664d30064d741fb76bd1f8444e470acdb3b294b006f64bb4d58ef4e68" + }, + "mengsan_event_squash.png": { + "type": "file", + "size": 3209656, + "sha256": "a280aa3721788e4fa71c718de80a6ae30658b6557b43a46dd06edf70b2df72a8" + }, + "mengsan_event_toric_toughness.png": { + "type": "file", + "size": 3097893, + "sha256": "735a500a48f344a755aeb94a2d6f40e2eb1b3692a109c60595d83ebc5635e7b2" + }, + "mengsan_event_whistle.png": { + "type": "file", + "size": 3288482, + "sha256": "585c5c4a5885c930a1c9f72dbdb5c1d4617d672481bae40f91f44256335474f2" + }, + "mengsan_event_wish.png": { + "type": "file", + "size": 3052956, + "sha256": "ec05fe126b5b533aba8eb31294ec446a72d7cbe61808e906075ad80271996dbb" + }, + "mengsan_fangxue.png": { + "type": "file", + "size": 300961, + "sha256": "c8f5ba0b966ba7ddbcac18edb80a5a407ee366f7e65c37ec620e927fabb2fcfd" + }, + "mengsan_fangyu.png": { + "type": "file", + "size": 284168, + "sha256": "63b6661a86350a635ade93322dfb76569dfc8aa652ff22150c538780bab7becb" + }, + "mengsan_feijianhuixuanbiao.png": { + "type": "file", + "size": 299751, + "sha256": "c7b5b3c56851459998933d05d939f46c39323feb13c26efbbcb5738d8a910a5d" + }, + "mengsan_fennu.png": { + "type": "file", + "size": 296860, + "sha256": "81b5c13db2980a7877506eaf9a7b0bbb028a77dda7c18cf1b4c4a402ac0db29e" + }, + "mengsan_ic_ashen_strike.png": { + "type": "file", + "size": 3380843, + "sha256": "62bb1d4a3cce45797e6ce257b690f127596acee491dd0b3091bf52018b839480" + }, + "mengsan_ic_barricade.png": { + "type": "file", + "size": 3184834, + "sha256": "39ee5e80832d97b79c82400b6f42c03a8701288447b37b698f7f5db8d80dcba6" + }, + "mengsan_ic_battle_ready.png": { + "type": "file", + "size": 3106395, + "sha256": "5392a0ace902bdd1487e5ca97ea469ba73efbbabab26f237ce76ef2ae7f4526f" + }, + "mengsan_ic_battle_trance.png": { + "type": "file", + "size": 3151914, + "sha256": "c327d3554861c1d2dc13c81d8142197bfa07359abac54c8611968168c12256d8" + }, + "mengsan_ic_blaze.png": { + "type": "file", + "size": 3375895, + "sha256": "75804bfb929e75b4ced9f332807fd74003127b3f83f72ccfe6d4d9f35a26bb13" + }, + "mengsan_ic_bludgeon.png": { + "type": "file", + "size": 3389225, + "sha256": "79c71b0419b979f7f06ffa12be1761ab41244d33c721c1a679d0448d00b29337" + }, + "mengsan_ic_bodyguard.png": { + "type": "file", + "size": 3343236, + "sha256": "50da52b4a1ee4537d6328d311bbb4842f7de74796613784f9d6d53f5f3d5f32e" + }, + "mengsan_ic_boulder.png": { + "type": "file", + "size": 3285153, + "sha256": "bff97d7a6f172b16cb3bb0434face4173bda907355516975efd0b177abdcb727" + }, + "mengsan_ic_brand.png": { + "type": "file", + "size": 3294776, + "sha256": "8f0da53ec9a4ba94742f7b08aab5c8c065ad7d7f06d0456c7b8beea9edc0156c" + }, + "mengsan_ic_break.png": { + "type": "file", + "size": 3312419, + "sha256": "59e6a3748d903a508f5a2fb1fe4160848ab301b7f82b43b15c90daa88134c992" + }, + "mengsan_ic_bully.png": { + "type": "file", + "size": 3295672, + "sha256": "11e917a6a33573ea00f187afb8b18c313887e901f30d8ea09dfb206c6dcd5ff3" + }, + "mengsan_ic_burning_pact.png": { + "type": "file", + "size": 3261391, + "sha256": "e3163733cefc1f8c99f0d6549329e0fff3fb3a6c15dbb917d455173d39fa6a85" + }, + "mengsan_ic_clash.png": { + "type": "file", + "size": 3375675, + "sha256": "c61999fb8ee8d70150348630fc773d7391f2239d01289acb1cb98906ad6b29b6" + }, + "mengsan_ic_colossus.png": { + "type": "file", + "size": 3446064, + "sha256": "95758ea8bc56299aa7f29f48eed911ece7fa11fbbfd77d6ee865e3142002a607" + }, + "mengsan_ic_conflagration.png": { + "type": "file", + "size": 3383784, + "sha256": "24340736d1292a3e199148b02c6a395d91e94ee541833a7d9acf472285046967" + }, + "mengsan_ic_corruption.png": { + "type": "file", + "size": 3321262, + "sha256": "040e1f40a9205862b4893fefbbfd0fbde2530983694bafd1509347830b60bf26" + }, + "mengsan_ic_crimson_mantle.png": { + "type": "file", + "size": 3315436, + "sha256": "7a0b60005554b2ecff4eada7096b5be930146e51769cc6f3a5784fc8f6f84459" + }, + "mengsan_ic_cruelty.png": { + "type": "file", + "size": 3318267, + "sha256": "d538eac00eeb64bda764b32555207e79de8eb860d4b9e80377f0566534f8dc4a" + }, + "mengsan_ic_dark_embrace.png": { + "type": "file", + "size": 2966840, + "sha256": "ffdd9cc3fbd16ff58c647d4e05c6e8dd78f348140de2a0651dffe40e4ec49cdd" + }, + "mengsan_ic_demon_form.png": { + "type": "file", + "size": 3414331, + "sha256": "33b54e87e86d88c50e9995ea6849eed9e2f65dc0a2aa50a8a72b8ddc3b209165" + }, + "mengsan_ic_demonic_shield.png": { + "type": "file", + "size": 3363214, + "sha256": "1f2ccf30e3534948282ac9cec36aeb1eb8a76161f4970188610f4ebd564a6cf1" + }, + "mengsan_ic_dismantle.png": { + "type": "file", + "size": 3290705, + "sha256": "1524381ee0fd906e7c287017ac2c07df01a2128837635c958456d90f8a116526" + }, + "mengsan_ic_dominate.png": { + "type": "file", + "size": 3314562, + "sha256": "1b054572525b494046efc2df72e87446a121bee8084a6ab022f526d8e9f0076f" + }, + "mengsan_ic_drum_of_battle.png": { + "type": "file", + "size": 3155334, + "sha256": "498552963fda156c5f179fd7a3b4ceb1adaa6e8f9e4e02d864508a95a17c21ce" + }, + "mengsan_ic_dual_wield.png": { + "type": "file", + "size": 3219368, + "sha256": "a22ebfe2790fe287f16e91f3bf767b1bdd2770c86559ee39060dfac2708dee2e" + }, + "mengsan_ic_eager.png": { + "type": "file", + "size": 3155388, + "sha256": "30fc98f684f1f4d1e428245b89917b7bb68c037b5d7582a8f7eeab99685d2b5d" + }, + "mengsan_ic_entrench.png": { + "type": "file", + "size": 3500692, + "sha256": "38f8a2cb023b54a49d9c85bb523c4c7f8c6ea60d6ed30d2e02825358fba5a900" + }, + "mengsan_ic_evil_eye.png": { + "type": "file", + "size": 3403431, + "sha256": "cbf7f72b018401d6cc208b8f03440417d0943d70e1f91fc3f6ce9f3279f997f0" + }, + "mengsan_ic_feed.png": { + "type": "file", + "size": 3195433, + "sha256": "e68e07bae5c148ada1a33964265fc9eb9a0d4388fee3e35f6f9b0fef0f53afc8" + }, + "mengsan_ic_feel_no_pain.png": { + "type": "file", + "size": 3148743, + "sha256": "216044b33d3dcab884597a5382d932b34be68b36700de401952107e96bcdb83c" + }, + "mengsan_ic_ferocity.png": { + "type": "file", + "size": 3156966, + "sha256": "7b71c93f5fe9a48c63f52c4072c021ff8bd3da26e89a478cdec9ca675ed150c2" + }, + "mengsan_ic_fiend_fire.png": { + "type": "file", + "size": 3445601, + "sha256": "8e10353d6717f4f649acf821b3e3b4af70a058d40aef7e86ceef3b70fb413d5f" + }, + "mengsan_ic_fight_me.png": { + "type": "file", + "size": 3312407, + "sha256": "3bd8d2bf23c8fa667cb40297716218bb55f3cceb12e77b841651aebf9c7f5545" + }, + "mengsan_ic_flame_barrier.png": { + "type": "file", + "size": 3459567, + "sha256": "c73225d6181f96b6d5f35ef306d858c73b60d3af986cf0232e704374413a134a" + }, + "mengsan_ic_forgotten_ritual.png": { + "type": "file", + "size": 3345779, + "sha256": "8a2b56f31440771c5c853d1dfe9860e507675b9d330ff0a48f7d11762e51f537" + }, + "mengsan_ic_grapple.png": { + "type": "file", + "size": 3215001, + "sha256": "512ddb86f8ea7489b5b292cdcd4f0d7d892ccd7c4a2bba0e218c06325f23646c" + }, + "mengsan_ic_hellraiser.png": { + "type": "file", + "size": 3258334, + "sha256": "b0a15fd06317803c7f29537a03527d41ae430f53595a7ff92ba491cdb068b76e" + }, + "mengsan_ic_hemokinesis.png": { + "type": "file", + "size": 3582165, + "sha256": "e7392585c268ccca314b9be969951fc803e011beaf2cc3c72d578c0561b674d8" + }, + "mengsan_ic_howl_from_beyond.png": { + "type": "file", + "size": 3196502, + "sha256": "801203f8103404b55c861450d104866975eb2dd379233c8d19de370a83c4ea9d" + }, + "mengsan_ic_impervious.png": { + "type": "file", + "size": 3398556, + "sha256": "45366fc69d3c6b7b91fe15611c76f693fe2f445406e53ef582b3379447759f18" + }, + "mengsan_ic_infernal_blade.png": { + "type": "file", + "size": 3081535, + "sha256": "10cfbfdd41d76c924f4c6fa74f57dc6546c181f26602c091e602ad0234b4ee07" + }, + "mengsan_ic_inferno.png": { + "type": "file", + "size": 3389310, + "sha256": "b1108d5cb2cefa40f9a4a5ea13a924a2624a57b4ca7c2ae8b7e7086ba09b090d" + }, + "mengsan_ic_inflame.png": { + "type": "file", + "size": 3236167, + "sha256": "f9ee9fd58e4d5e74669665e0cbbc9050a0b61aa3ec2a798f97da97c1a6d44e1c" + }, + "mengsan_ic_juggernaut.png": { + "type": "file", + "size": 3433571, + "sha256": "4faca2d750999826ca934c45ea648850233b4038207b6cf1d5f05eb6ffd34f49" + }, + "mengsan_ic_juggle.png": { + "type": "file", + "size": 3320003, + "sha256": "d3f8d1394131b5f7d6cbeef9b8eb89ac25b0c108d79267210d5b6f9f7adcf513" + }, + "mengsan_ic_midnight.png": { + "type": "file", + "size": 3163175, + "sha256": "e0f68012e73f192a2cc169235eb0b49f27b6991c37a5e8862cdb4f32d010d39b" + }, + "mengsan_ic_not_yet.png": { + "type": "file", + "size": 3398829, + "sha256": "dddcd2e90d84883c33653169df1591633b944932aaaf6f34f43be10724007515" + }, + "mengsan_ic_offering.png": { + "type": "file", + "size": 3358902, + "sha256": "a8ac083487185734223c31e1e428f9ee78c0dad7e6e37baa201bb33a44e974b5" + }, + "mengsan_ic_pacts_end.png": { + "type": "file", + "size": 3444230, + "sha256": "c23a5eba95bf6d44519eb890580ac576ea5477f96345cd9c58b03183d74179bc" + }, + "mengsan_ic_pillage.png": { + "type": "file", + "size": 3371280, + "sha256": "f7aba0b71d6cbd43d6de528713e7b2ca015c35a99826dcfbd8cb04f9d8e4c684" + }, + "mengsan_ic_primal_force.png": { + "type": "file", + "size": 3284334, + "sha256": "c3b0d479166fae86e39f60739118fb993baae2d9c2e0ff2bc0b606d95b436b84" + }, + "mengsan_ic_pyre.png": { + "type": "file", + "size": 3272678, + "sha256": "e29710c3846fbe3571460232bbe337f502b1a5cb3928774b0796b16339030e3f" + }, + "mengsan_ic_rage.png": { + "type": "file", + "size": 3254815, + "sha256": "178f291ccb5760f122b335b63963432e0e547f54b6d2a94b95641c382e60b708" + }, + "mengsan_ic_rally.png": { + "type": "file", + "size": 3208925, + "sha256": "5ca01c9b5ab51118da8afe5f5019ac9c8394d21c3153260d307d3c1e83de9159" + }, + "mengsan_ic_relentless_assault.png": { + "type": "file", + "size": 3157513, + "sha256": "d5093dcfc00a559844f160f6a07a32c5633047312f04e2c84cf385850f4565c1" + }, + "mengsan_ic_rupture.png": { + "type": "file", + "size": 3242389, + "sha256": "a7d85597a367d43006abb2bc54fc1adade5f1fe396d80aa64b1d1fc0790c5517" + }, + "mengsan_ic_second_wind.png": { + "type": "file", + "size": 3326737, + "sha256": "c00a2886359ff2cc6e067de86ef0126040934bd079e1d65b12a58e749bbc48f8" + }, + "mengsan_ic_stampede.png": { + "type": "file", + "size": 3340733, + "sha256": "3e7ecc806b9c528cdd2e2de45eaf04926e9fd0a49a71ae06602e1fb9938e6804" + }, + "mengsan_ic_stoke.png": { + "type": "file", + "size": 3224630, + "sha256": "2f2947c935a95aa22e7aea17e5b684e8077d0ccfd56348d404a3dea40e428850" + }, + "mengsan_ic_stomp.png": { + "type": "file", + "size": 3230547, + "sha256": "ed2a8f438c40c486e4efbc128c45bce568137d3d87a4d97ad2a3b69317e43342" + }, + "mengsan_ic_stone_armor.png": { + "type": "file", + "size": 3409260, + "sha256": "f72619cfaed0d7b433dc58a6204751e292a4e69cde3089417ce346894ebe683f" + }, + "mengsan_ic_taunt.png": { + "type": "file", + "size": 3260651, + "sha256": "122103d43fb1053d12d172c2a488bae9f7a6765c6d36ea2d3b13412a2de6e33d" + }, + "mengsan_ic_tear_asunder.png": { + "type": "file", + "size": 3318965, + "sha256": "df905976f4888b4eccc1418591a38c725f9c45cd579097adbf255c5f3ba2aeae" + }, + "mengsan_ic_thrash.png": { + "type": "file", + "size": 3408839, + "sha256": "8277ecb7faf2df11ad9e05de620b47ea44f20a866ff02fb9ff042ec8479bfad4" + }, + "mengsan_ic_twin_strike.png": { + "type": "file", + "size": 3465328, + "sha256": "a2e60f67dfff10789fd453f1525941238389786f3dc5e71788a30d312b751914" + }, + "mengsan_ic_unleash.png": { + "type": "file", + "size": 3412724, + "sha256": "3dd9906996d07deaa06e1b56839609175a1d9d88bccdd9237465844c3e5882bc" + }, + "mengsan_ic_unshakable.png": { + "type": "file", + "size": 3286732, + "sha256": "a7540f94fe98fce1434928b6a204f96f2f036a3db81579e499b2a31a7d9c6184" + }, + "mengsan_ic_uppercut.png": { + "type": "file", + "size": 3369967, + "sha256": "0935723010a4fd761821e827e6d456a07c63ac937beb0ca8421415d2bd67bc9d" + }, + "mengsan_infection_shuying.png": { + "type": "file", + "size": 3154682, + "sha256": "7a47a79522cf9c684efce578b43197936a37feca7e229097d86903118e5a5862" + }, + "mengsan_jianbingdaji.png": { + "type": "file", + "size": 295064, + "sha256": "379b700ebba9843c94f63b82ebd68536e232a0f5b4f4e093b055ae26023a83eb" + }, + "mengsan_jianyi.png": { + "type": "file", + "size": 308533, + "sha256": "8440f6dc36bbc3fec6bd7406f2a0623aae37f63e7cbc1a7dc7b912805f4c9f38" + }, + "mengsan_pomie.png": { + "type": "file", + "size": 299414, + "sha256": "67ab0a3a024f0a4095f232d16390f0431afa05cc2c9d292752dfa23a4185ee28" + }, + "mengsan_qiling.png": { + "type": "file", + "size": 284417, + "sha256": "f2f38bbcf852e7f6fc7d6e7c971d622e2d437bde8eafccb1226dab66a4b51164" + }, + "mengsan_quanshenzhuangji.png": { + "type": "file", + "size": 298500, + "sha256": "19cfcaf0607a17db14f82e458e8aa77d02c73b234aeea6af3bd715ea7fdf0673" + }, + "mengsan_rongrongzhiquan.png": { + "type": "file", + "size": 286444, + "sha256": "c43ff6366ac35a6b7a2cc99b2bcbb8052c86efeb3a807c66696e363597ded83d" + }, + "mengsan_shandianpili.png": { + "type": "file", + "size": 289884, + "sha256": "54e18bfb541c08a78922bc38f26323c20a7f160e54218016f0375c8c4cabedb3" + }, + "mengsan_shuangchongdaji.png": { + "type": "file", + "size": 290611, + "sha256": "2dde2cabca61df441a98daf41f82a66c6343aba7337b3b650d63e01dbedf56c2" + }, + "mengsan_slimed_shuying.png": { + "type": "file", + "size": 3173387, + "sha256": "6185d04b40409525e0eb8268eb55ff0a047ad68833169230936dca937c37b05e" + }, + "mengsan_songjianwushi.png": { + "type": "file", + "size": 299499, + "sha256": "fb9db91a0ceafd18e71b15807bc994a93797a0d438bb7d8fe4db459adfe11534" + }, + "mengsan_status_beckon.png": { + "type": "file", + "size": 3089872, + "sha256": "ec9aa4385fcffa00030390c2bf02ed833cf7b4200d268955831949e3deb4c22d" + }, + "mengsan_status_burn.png": { + "type": "file", + "size": 3202565, + "sha256": "fee1bddc02b755972e99be68fd354ec69cb9b6286a15c9c8203ed2708bb29904" + }, + "mengsan_status_debris.png": { + "type": "file", + "size": 3212683, + "sha256": "8f251585e1487a3d8c04111eaac50ca7860745a8386107c0497f777fdc2ac3ec" + }, + "mengsan_status_deprecated_card.png": { + "type": "file", + "size": 3211849, + "sha256": "9dc7828e6e5559479d48a2f26144ca40ee365b5587183496d41085bc01f7464e" + }, + "mengsan_status_frantic_escape.png": { + "type": "file", + "size": 3331510, + "sha256": "e5b328f4f8efae481d25ef7fc2739c4f614ff05ee65799363a4194e229406526" + }, + "mengsan_status_soot.png": { + "type": "file", + "size": 3164859, + "sha256": "885f2f1557cb9483c0bc0538c6eb3f1abb017809883d24a2f864732df4e48bef" + }, + "mengsan_status_toxic.png": { + "type": "file", + "size": 3047189, + "sha256": "d2e6f92ba539b91a21cd3eb836f2b28c21e81548fbc1fcbf1686680533bc39c6" + }, + "mengsan_status_void.png": { + "type": "file", + "size": 2969653, + "sha256": "63ad2acb439b474b93a250398cb153063c01c74ddd4ce7c82a8717f743495ef1" + }, + "mengsan_status_wither.png": { + "type": "file", + "size": 3329606, + "sha256": "265095ea049e38be62725ece19e91c15ff6abb232a4276779b93555ce0dccad1" + }, + "mengsan_tiezhanbo.png": { + "type": "file", + "size": 286763, + "sha256": "9e1a8525003f822c93bc8ed84d3548a6848ad29d849434fceb64e9561b7df61b" + }, + "mengsan_touchui.png": { + "type": "file", + "size": 296991, + "sha256": "de29a754d72d6151eba7c7028b61ded9d475e80d3303f9246186cb37b8f59d3d" + }, + "mengsan_tupo.png": { + "type": "file", + "size": 297945, + "sha256": "9de2094ccdd9c4ddc743cda6a9e9e12b8decc98019d6af1a2c4290c9f1c789cf" + }, + "mengsan_wanmeidaji.png": { + "type": "file", + "size": 298675, + "sha256": "2d786cf6078dbf1ea545886bd98298c867fbbf4c5f0fc1a74f28a2ac8f06cc1a" + }, + "mengsan_wound_shuying.png": { + "type": "file", + "size": 3102499, + "sha256": "16aa7c0485416028c1ac4990ceb6d9f850dc93d63a74bfa3514dfddd101f6c29" + }, + "mengsan_wuzhuang.png": { + "type": "file", + "size": 297615, + "sha256": "8b3d3d473d2db34a39b69e0cac53658da2b3e1e95086c982389640b490881a7d" + }, + "mengsan_xuanfengzhan.png": { + "type": "file", + "size": 283797, + "sha256": "85d06995103cf42f59790a93a89d64ede27111eb3c8e61d7e33a6b493b09240b" + }, + "mengsan_xueqiang.png": { + "type": "file", + "size": 271433, + "sha256": "da5b03fba289b63ae4e3c55b9c0b702aa3a805770887e5d323c0f3dd88bc01e6" + }, + "mengsan_yuanhen.png": { + "type": "file", + "size": 282703, + "sha256": "c802721d642360389b8ab9437f8f2dc27d804e8da537ac17ae38e9c744e77903" + }, + "mengsan_yubeidaji.png": { + "type": "file", + "size": 291900, + "sha256": "9ea0dad98b3e76906629905557976a6d95c66d53257fde0ccc33c46f7f20ce84" + }, + "mengsan_yujin.png": { + "type": "file", + "size": 290863, + "sha256": "62f01ae1869fa481be843eae236c2046e5388dfa537ae6c02291085b5248caba" + }, + "mengsan_zhanli.png": { + "type": "file", + "size": 283172, + "sha256": "5297fdf3163730e5cb9e318c905287cae52d61aa53e6e9dd7586193706b35daa" + }, + "mengsan_zhongsha.png": { + "type": "file", + "size": 277449, + "sha256": "430256856588341cc0ad3fe15aee2e9b63711187d9ebe77118df0b60003b956f" + }, + "wound-original.png": { + "type": "file", + "size": 323855, + "sha256": "c588716825f10b5d5fc504221184337b167542d1a7e6cf169c773cbaf8e2de48" + } + } + }, + "intent": { + "type": "directory", + "children": { + "Intent_attack_1.png": { + "type": "file", + "size": 9558, + "sha256": "f2c470238c20a762cdaf4dda607b7ede3b46cf8ebde99fbecb689bc5a4aafef7" + }, + "Intent_attack_2.png": { + "type": "file", + "size": 11308, + "sha256": "d7db260070139367b9a1cd9729e701ea0a738e9314ed8d69362e4a6db0765f53" + }, + "Intent_attack_3.png": { + "type": "file", + "size": 12005, + "sha256": "f312ddeebc78634a4d943f53ac9430aebd2e83fe0f17fcaff4f21d02af957983" + }, + "Intent_attack_4.png": { + "type": "file", + "size": 12570, + "sha256": "54e3c06ac574817d75525f84792acd59a977f083b6ae54ce871b1587864449a3" + }, + "Intent_attack_5.png": { + "type": "file", + "size": 14137, + "sha256": "afbd87907f01aa2b2dccbd6c24e571d7632052b250a4c9d1ab10b1cf08c843e9" + }, + "Intent_buff.png": { + "type": "file", + "size": 5505, + "sha256": "15dbb076206d24c230dbb77ef7992b17d08a78a3add78777f82e7e307de1f9b9" + }, + "Intent_card_debuff.png": { + "type": "file", + "size": 8179, + "sha256": "1a9b999c5c9f861b05d2beab84e847a1c9313eecec438ad86ca361f3b5a545f9" + }, + "Intent_death_blow.png": { + "type": "file", + "size": 7712, + "sha256": "bd7f528ba8c54cd49ca08b54bd63355247f16fd8fc73fee84f0c3d32c5e0c3a0" + }, + "Intent_debuff.png": { + "type": "file", + "size": 15067, + "sha256": "3b0f30e7f9ac9fb38968094be967d6c9447e7c80411f7c35a77883b0f8c8b885" + }, + "Intent_defend.png": { + "type": "file", + "size": 9518, + "sha256": "801ac36df2596bc40cc34211596865b7c5011b84d1b2b026c41e907faf824085" + }, + "Intent_escape.png": { + "type": "file", + "size": 7440, + "sha256": "bd1520b97af0a7e6ae64d227e9100e439b1397339be437fae5e8e3ccbf9a3d07" + }, + "Intent_heal.png": { + "type": "file", + "size": 5046, + "sha256": "26fe175442542e594d801f14223381e1e8b8fbf10b0bdb4e3b4618d4601bc6d3" + }, + "Intent_sleep.png": { + "type": "file", + "size": 8969, + "sha256": "72b5fa19e2e9c22f3508b59943bc7befa5a05e8791ab8d12530c228242f5cb9b" + }, + "Intent_status_card.png": { + "type": "file", + "size": 10048, + "sha256": "15bd3635046063126d039612911df2870c3fadf8f1104ddde12a7087aa5f0a5e" + }, + "Intent_stun.png": { + "type": "file", + "size": 13334, + "sha256": "35ef85fb8d9c5ef789742e8467b3049f40fddfcbdf042747f28345e7ebdc1f4e" + }, + "Intent_summon.png": { + "type": "file", + "size": 10668, + "sha256": "0338e8ee861802ec7160519b2945b05089c4f9672f3f1d8009f2484bff34e261" + }, + "Intent_unknown.png": { + "type": "file", + "size": 6401, + "sha256": "505e0339329dd5374fddce7be5e677f1a9d50f0db7a476ffd7e6349691ea408d" + } + } + }, + "portraits": { + "type": "directory", + "children": { + "bygone-effigy-forest-portrait.png": { + "type": "file", + "size": 2884630, + "sha256": "002ed3ec353c114667478afef3aec4e35a59da1e18d24cc82b6376785cd4bfdb" + }, + "bygone-effigy-original.png": { + "type": "file", + "size": 164866, + "sha256": "201df632cc52fbe572642c1fef10b9bb33dd4a74cec6e068a595056ac5c9e403" + }, + "byrdonis-forest-portrait.png": { + "type": "file", + "size": 2699460, + "sha256": "80feac5bc9f6a09eb4882eaef520e42955404d443458abf7f46db27b20226b18" + }, + "byrdonis-original.png": { + "type": "file", + "size": 119279, + "sha256": "b5d17a9ce0c741fe2031b979aee3f9285aaeb4f0c85d5de4a1440700e3a921f2" + }, + "ceremonial-beast-forest-portrait.png": { + "type": "file", + "size": 2682259, + "sha256": "d3e7844a420473cf3c16922ea485fc560baefcb65f71e1593a12b8a683e864d5" + }, + "ceremonial-beast-original.png": { + "type": "file", + "size": 221253, + "sha256": "0b8a913d8e78e45eee6eec7d9081380b37788bcabc2d5e0137fedef0608ea09c" + }, + "cubex-construct-forest-portrait.png": { + "type": "file", + "size": 2628127, + "sha256": "446a72d5a6226d81f1ebb2bff450464f6be1cac76c56405121f89879419180f8" + }, + "cubex-construct-original.webp": { + "type": "file", + "size": 7980, + "sha256": "37ed7258667485ff17c2739b0c725efa06b5f3c114e834d9ae6f25800c5934bc" + }, + "eye-with-teeth-forest-portrait.png": { + "type": "file", + "size": 2303458, + "sha256": "b9ec3cd9d4e7e2272fed6cdae5dd871efa1f36382ca3b251c35514433c70ab03" + }, + "eye-with-teeth.webp": { + "type": "file", + "size": 16056, + "sha256": "fcb437dcd559e6d7a8d4be5fba6f90d9ad44ab214452f7fb548583261100682b" + }, + "flyconid-portrait.png": { + "type": "file", + "size": 1990312, + "sha256": "83f9724758fa09ade94731e061da584ec986b8cbd058d4ba966db237f6dc58ed" + }, + "fogmog-and-eye-forest-portrait.png": { + "type": "file", + "size": 2314490, + "sha256": "c7e869cf1fdc18cce6eb499d39809c30a97faa55b3d19b7bb95932c1b1f91672" + }, + "fogmog-forest-portrait.png": { + "type": "file", + "size": 2310155, + "sha256": "a76c62c937c00ddc4dff51c7e14fce20ab57ad69fe5dffe39aee8a4f9f9d21b7" + }, + "fogmog.webp": { + "type": "file", + "size": 12420, + "sha256": "9af334ab2cec331d2ce2bc17df772b91a3a436e1d6e47c4179e4107b1d5c9d80" + }, + "fuzzy-wurm-crawler-forest-portrait.png": { + "type": "file", + "size": 2447058, + "sha256": "d4794bd0807b08012d06ac2734ffff3ba430fae966af3c4c94b375610848e607" + }, + "fuzzy-wurm-crawler-original.webp": { + "type": "file", + "size": 9968, + "sha256": "bbd3e1eb4858c299f8b328b4974ee2fc958cc530c4c51d3f9203e89d151dfe56" + }, + "guanyu-portrait.jpg": { + "type": "file", + "size": 980085, + "sha256": "729d194ccf4507b5d025e7c697811b7236c6286f5feb7e2acd68964a9dd90602" + }, + "inklet-forest-portrait.png": { + "type": "file", + "size": 2202786, + "sha256": "cb22e222c95aeca9730d53536a066b69b1d77e3b9e32acb1a3f670e28e7b162d" + }, + "inklet-original.png": { + "type": "file", + "size": 18975, + "sha256": "aed225fc3df345e48deb9cf4d314699006cd9b69f005358390e3a4039cbd0649" + }, + "kin-follower-forest-portrait.png": { + "type": "file", + "size": 2349442, + "sha256": "cc0a249e832f6df0068eedc20a0ba82640ff377e8f2ada1b80edb00e89e75ea2" + }, + "kin-follower-original.webp": { + "type": "file", + "size": 10758, + "sha256": "ea19927f34cd0af0023be79d09cb6ae1d7ddd17ed1fd24b62ca39622ab9cb89d" + }, + "kin-priest-forest-portrait.png": { + "type": "file", + "size": 2508934, + "sha256": "b379e7fffdb06e3f7734109a509f379c909deb4c54fa11700fc5eca015cc73d5" + }, + "kin-priest-original.png": { + "type": "file", + "size": 105120, + "sha256": "616bd85d422cc8b022ceecaa184a0a6e6e83e54d069671145023bb3819b7d37e" + }, + "leaf-slime-m-forest-portrait.png": { + "type": "file", + "size": 2290313, + "sha256": "bbf7d43a1e5f9d464dff54935ccede4c8334b209fb67f20c3fe9e871925a2c53" + }, + "leaf-slime-m-original.png": { + "type": "file", + "size": 22536, + "sha256": "a06b5fddbbc2df03bf2c971dea9a9b36f5f6fed606a1f9ef47f2097fe1a68220" + }, + "leaf-slime-s-forest-portrait.png": { + "type": "file", + "size": 2048824, + "sha256": "aca265306d482c6bb89d3d716ff091b44eb03781f8a33aa68b25f97b6a61ce9d" + }, + "leaf-slime-s-original.png": { + "type": "file", + "size": 9099, + "sha256": "8c6f876ac0037d7b971bbb7e56256341fb270f274442bc11162787e9e0877cd6" + }, + "liubei-portrait.jpg": { + "type": "file", + "size": 945285, + "sha256": "2a26577e496535a6f578d7bd77c13a1d5fd8b25da9d9a29947195c6c03589eac" + }, + "mawler-forest-portrait.png": { + "type": "file", + "size": 2557684, + "sha256": "656452b9250c98d643773031cc8f2b1b62b6d9bfa90f95dcbc2e969e0bcc3571" + }, + "mawler-original.webp": { + "type": "file", + "size": 9340, + "sha256": "2b79b8ea9052c6f8881b543dc87f49f15c2f466e88e8ee928110b97460dcaf35" + }, + "nibbit-forest-portrait.png": { + "type": "file", + "size": 2465408, + "sha256": "1a0913dc85f7d02824ced4806a0fb64286aaccd704fa0accd173d46d6758238e" + }, + "nibbit-original.webp": { + "type": "file", + "size": 8956, + "sha256": "123124eba6b62191aa1216f6b8fa541b4fc7cd05d6d57b386df1098c6e3e7ab5" + }, + "phrog-parasite-forest-portrait.png": { + "type": "file", + "size": 2779629, + "sha256": "9c8b255b9d6877c79054267f3648d11cc6bb26dbf7be13a74d0b7015f9bce23e" + }, + "phrog-parasite-original.png": { + "type": "file", + "size": 194971, + "sha256": "933ffcf205a3aef6d7ccc64423b2ecf6cdf4c82ee143e5106222f2e095e603cf" + }, + "raider-assassin-portrait.png": { + "type": "file", + "size": 2495247, + "sha256": "5e390c7536915a868975bcdc4d63458e31cf4ecc4ae6d45726692ec92fcc1b01" + }, + "raider-axe-portrait.png": { + "type": "file", + "size": 2707142, + "sha256": "35621425ef3bfdfdcfd72fec4ed15ecaa47bc632b8f4687607f02bf2684187a0" + }, + "raider-brute-portrait.png": { + "type": "file", + "size": 2523079, + "sha256": "99cc94bb1e57d4c256ec5c78e964d877ec9b269ced93adadd226b7774f9853da" + }, + "raider-crossbow-portrait.png": { + "type": "file", + "size": 2574258, + "sha256": "72c303e5a421e5cc790bf1bec9d13ac174a08e46d0d8cd3113e53cf57065acdb" + }, + "raider-tracker-portrait.png": { + "type": "file", + "size": 2469608, + "sha256": "dd9d4c269161145ffc3de6b446e4f0a9bb7f69e3c9a0243f8418fda2413c4ad6" + }, + "shrinker-beetle-forest-portrait.png": { + "type": "file", + "size": 2212880, + "sha256": "0b196f76754bf1946102d3d4889c856ebda9415ed54c7b2864052d7492e5cbbb" + }, + "shrinker-beetle-original.png": { + "type": "file", + "size": 29818, + "sha256": "84002e8534f1c51abc790f7fe5c8d761e3861eb81d4799d322ef07d31ba689d4" + }, + "slithering-strangler-forest-portrait.png": { + "type": "file", + "size": 2464156, + "sha256": "cda3af95f3b5c042859d14929614c4def0ee24bbc74dbea22ffe37c727c5166e" + }, + "slithering-strangler-original.png": { + "type": "file", + "size": 34870, + "sha256": "aa70738cbb412fb50aab01e7435f43e1aea08b28b6b262326f61607b08092319" + }, + "snapping-jaxfruit-forest-portrait.png": { + "type": "file", + "size": 2331283, + "sha256": "90ac4774e51d8d32da09269c68f85494513e0540bd0f9c22c6e5239aebf4d33f" + }, + "snapping-jaxfruit-original.png": { + "type": "file", + "size": 57669, + "sha256": "a2b236b6d47deeb99233ea78666422440611aff95f53f0667b62442ae8224594" + }, + "sushuang-zhangshiping.png": { + "type": "file", + "size": 2961815, + "sha256": "a57aff9ea3666bb4a5dc8fd66adc8dba88b50391fb5dc29e20ff6caae14b51c7" + }, + "twig-slime-m-forest-portrait.png": { + "type": "file", + "size": 2378273, + "sha256": "b73751425e4c73f19eda40b7acc12171f4e1a53b14091c27be34c38635193859" + }, + "twig-slime-m-original.png": { + "type": "file", + "size": 36491, + "sha256": "f2d7632f0d0612cc48756cb16ed9e5b2e8e9afb13d776f959b1f66b25417b097" + }, + "twig-slime-s-forest-portrait.png": { + "type": "file", + "size": 2145175, + "sha256": "42616385167126cc4a183a8c6d6c651900e23f29d70edbd86fdc0c4fcd284e96" + }, + "twig-slime-s-original.png": { + "type": "file", + "size": 12691, + "sha256": "77cb7172d0d209566a94c5c0022ac1ef10ce0d5912c1e7ccb8fc0f0186e3b8b6" + }, + "vantom-forest-portrait.png": { + "type": "file", + "size": 2356886, + "sha256": "46649e3ce6e833d4c743e88d963831885ab4c56c23622620d9f7464169a3ec59" + }, + "vantom-original.png": { + "type": "file", + "size": 96170, + "sha256": "87c14412b3f857f4404fe5d2b3c5f04a64fff6ea9357e0d9f3550bbd1336d97f" + }, + "vine-shambler-forest-portrait.png": { + "type": "file", + "size": 2473037, + "sha256": "35f004dee2721cc8d6ecf981bc9bb24d3e2fad397cbe4205bd8d6330845b7c42" + }, + "vine-shambler-original-thumbnail.webp": { + "type": "file", + "size": 24254, + "sha256": "882d887821a4dd5310f9c90f62c7501d3ad37d46a1a60bf613f77dc8f2569508" + }, + "wriggler-forest-portrait.png": { + "type": "file", + "size": 2799836, + "sha256": "1b93f4e0e3bbd1b6d70851b160be50e315c8ab00ead3b4d7dcd0eee724ea8dbb" + }, + "wriggler-original.webp": { + "type": "file", + "size": 9008, + "sha256": "b48ee157b976123f515ec7cd5dd6dfc0909c2eaa3735f1acc04ab8a388fd8ecb" + }, + "zhangfei-portrait.jpg": { + "type": "file", + "size": 3004186, + "sha256": "f4d6e551a3098ecc869c6b53e8d24be50e8ca97f399ab987b96f32a53c75a02d" + } + } + } + } + }, + "battle": { + "type": "directory", + "children": { + "battle-director.js": { + "type": "file", + "size": 13125, + "sha256": "a82a031a5d92f4e734625a40793db883ef96bd4c9b56bfef5bc56f2b982e4aea" + }, + "battle-flow.js": { + "type": "file", + "size": 5900, + "sha256": "1ae2db34176a7fc5d19e5001316e1f2aac4b1a883662bafc43af8cb95775970b" + }, + "battle-loop.js": { + "type": "file", + "size": 3652, + "sha256": "8ff23c71320c91cee2955cabfd8384190a07b80cbddd44bba50fcf9fef4daeb0" + }, + "battle-resources.js": { + "type": "file", + "size": 3095, + "sha256": "0da540600249f9addb23eecfc218930c2a376a0f357eb3a16b628d4c20817020" + }, + "battle-session.js": { + "type": "file", + "size": 5561, + "sha256": "ab9880fc1934a87d1ca6640778605ec33e9812c94930567f868a84fd29e56ad7" + }, + "battle-settlement.js": { + "type": "file", + "size": 12024, + "sha256": "74e6647cf5ff050962c3e171c1f48747e7a373fdcd5fc35b3ad9c170cbe8931f" + }, + "combat-rules.js": { + "type": "file", + "size": 5966, + "sha256": "e540dc2e2eb83ac20868252cef73373d6c807455d57f220949aafd1f762e3828" + }, + "death-blow.js": { + "type": "file", + "size": 1151, + "sha256": "aa3ba0da315ae81ae8430c50a57203889838adac8d533aa5400a4bb9976becf5" + }, + "engine-session.js": { + "type": "file", + "size": 3763, + "sha256": "1addfa8f6a6e1e56d2dd32b6d1249ef84aa0aaa1ca049a7e0ccb688be51261bb" + }, + "intent-damage.js": { + "type": "file", + "size": 1298, + "sha256": "616a18728b31677b70cc14b88af027c4cc912ae90f11de41355b900ed2c29ea1" + }, + "intent-effects.js": { + "type": "file", + "size": 2411, + "sha256": "0f3dc871427bf718b64ce057a08d6b98b100e66fad9f803662818b03cda1e3c1" + }, + "skill-teardown.js": { + "type": "file", + "size": 6849, + "sha256": "5f92d02d245aa3021ea47b245b221c58987b1e5770fe4329eef6ae733b0810ff" + }, + "stun-intent.js": { + "type": "file", + "size": 1340, + "sha256": "898f9e64b80c5a963229eed340455f810eec2c1777bb98ba599d56101ba11bf7" + } + } + }, + "bonds": { + "type": "directory", + "children": { + "battle.js": { + "type": "file", + "size": 1771, + "sha256": "bdb4d429b14b98d7d5434afb2d11039938b7eaea37cf5f87a3fba86fc958b254" + }, + "definitions.js": { + "type": "file", + "size": 2505, + "sha256": "f2c02c24fa7704ce4800969c5e4183840c556386bb4fe29a51ef25070636c3df" + }, + "intents.js": { + "type": "file", + "size": 2952, + "sha256": "935f5f1ec90ba7e0cd0539db9c51ad5ba11e9063bbbd060a56f4ef6c34bfe00d" + }, + "state.js": { + "type": "file", + "size": 4436, + "sha256": "ea5e5fd29a695a6d65fd1ad4aeb8ed97f5dfbdb3b34f8748f099140cd27341e3" + } + } + }, + "cards": { + "type": "directory", + "children": { + "affixes.js": { + "type": "file", + "size": 1124, + "sha256": "f65993cabf6fe9b01e9a41de4ff0d47aba56eb67cd9e00d4a67cd87d9ed9bce8" + }, + "battle-card-copy.js": { + "type": "file", + "size": 1737, + "sha256": "acffc9c0ecc7d22985f894522376e13d39067e71cf1532f2a2cd8d9466671fdf" + }, + "battle-card-reclaim.js": { + "type": "file", + "size": 1551, + "sha256": "397a0c05591122d91bed20499cdd0b55f3fddcdaa6a2d6cf8d7f4434c1306feb" + }, + "battle-energy.js": { + "type": "file", + "size": 1502, + "sha256": "cee19be46c41d274bf706c2add12f64834980ddb32526862e78de37bda28caef" + }, + "block-draw.js": { + "type": "file", + "size": 1197, + "sha256": "4770aa0a753b80d103e3c087bbe2046609e99a05dcbc5c388526ba87c554b7ce" + }, + "card-data.js": { + "type": "file", + "size": 3457, + "sha256": "0bafffc9e6e9c6bd8bae74948bc9b1742450adbda6a9dcb80206054c27bfbbe3" + }, + "card-definitions.js": { + "type": "file", + "size": 3006, + "sha256": "b824439eb6808642fa55a81779f5b990e52382c85c9f256a6346f3e21331b9d0" + }, + "card-pools.js": { + "type": "file", + "size": 628, + "sha256": "1830028ecefe5dedf09f27358d9235a91d6282dbdea46d410a48315afbc3b118" + }, + "classification-types.js": { + "type": "file", + "size": 515, + "sha256": "10813deaaf218c68512e44da8eac9a83717b8b942a48df498cfae570c9f939f4" + }, + "description.js": { + "type": "file", + "size": 524, + "sha256": "a37daf0e29fb05f8248ff3b9c9777e68e2dab6e7cf86a3c53510e099a1e10d1d" + }, + "hand-exhaust.js": { + "type": "file", + "size": 2673, + "sha256": "904bd21885750bcb5cabee5478d74a9f5e61651d2b9aeaff1f7b41559f1665e6" + }, + "hand-upgrade.js": { + "type": "file", + "size": 4359, + "sha256": "599859fc9a5283655778816847d2edff52c787b47b41f1e5e6fc6ac2160caed7" + }, + "havoc-play.js": { + "type": "file", + "size": 1921, + "sha256": "dad0d45cbbd0a8f13a9aa0c238d14b3c6e7553eb3795e3291e2c53d5b69bd7d0" + }, + "infection-card.js": { + "type": "file", + "size": 3056, + "sha256": "f39708a57dffe6ec86e176e4d3572fed9bad60350a83fadaf21d75e8d7f817f1" + }, + "intrinsic-affixes.js": { + "type": "file", + "size": 819, + "sha256": "67630d860ce74ceb9ba9fb40e8f7a4b13dcf55c83277653392d23b5f0ea9c0e1" + }, + "ironclad-advanced-data.js": { + "type": "file", + "size": 126, + "sha256": "e18fd3b159e910a02d26d4d0a9ea3d630b51fd71f734fb183b5542f6f777713b" + }, + "ironclad-data.js": { + "type": "file", + "size": 117, + "sha256": "8a90e36e9375661d1e9273da6fbfea3c6ef1be8e0772c2d5899b518d1d127390" + }, + "ironclad-hooks.js": { + "type": "file", + "size": 2240, + "sha256": "32eb74642d081bebd6e38b8e43c9fd8448163a4c93c1322c83e26798b3e556b7" + }, + "ironclad.js": { + "type": "file", + "size": 120, + "sha256": "5317a2bf599d8a7fabcf19dfa4f76284cbe6b7f43ec5a3f8c607ab9e69d9f61d" + }, + "life-loss-block.js": { + "type": "file", + "size": 1177, + "sha256": "718254e938a08be9e4fa036698fe5c33ac98c842722e1edebf7bb9906922f64b" + }, + "mode-cards.js": { + "type": "file", + "size": 7770, + "sha256": "4a8f9d61ba438ea3ba2e857f0b81bd85bee2ffc1c9bd0bcbd18dbdeac36aec8f" + }, + "packs": { + "type": "directory", + "children": { + "colorless": { + "type": "directory", + "children": { + "data.js": { + "type": "file", + "size": 32174, + "sha256": "4ba74b97668b97b99099efd19fdba2e81da02e2ab3764b332fc71cbd135b87cd" + }, + "index.js": { + "type": "file", + "size": 382, + "sha256": "b106052a24fc47b10472b907c69036e341a90a5945d32e140d67dc2c030a5f92" + }, + "rewards.js": { + "type": "file", + "size": 613, + "sha256": "af212d7c4cd8f63c5837e8f29519cd341b775135fa332f8a0f4f82eae4ac349d" + } + } + }, + "curse": { + "type": "directory", + "children": { + "data.js": { + "type": "file", + "size": 6704, + "sha256": "35d43aa2ada598257fe0d0c18f5ea746d67429c0d9a3d73b145dedb363fe50df" + } + } + }, + "data.js": { + "type": "file", + "size": 902, + "sha256": "4f04d425ba55670fe5707ef189dd3b74fa9934117fc5929c0f1b9b67c874863b" + }, + "event": { + "type": "directory", + "children": { + "data.js": { + "type": "file", + "size": 14820, + "sha256": "0badede4d570d625e371f3e06750d708c381f2eff3f14e54f1a803690f28c1c9" + } + } + }, + "index.js": { + "type": "file", + "size": 1109, + "sha256": "0711819c90af702cc98fc189c710bd278a9aa66022f04a3566ab18509b92af53" + }, + "liubei": { + "type": "directory", + "children": { + "advanced-data.js": { + "type": "file", + "size": 2842, + "sha256": "af05e8f5ffe996f11f1383023557f00d60eaccadc534a8fa8df3f9b57fc6b036" + }, + "advanced.js": { + "type": "file", + "size": 31216, + "sha256": "736ea6b7c45650fa4219c4dcafdcc61886c743f065469b22ad972728e7552595" + }, + "data.js": { + "type": "file", + "size": 4464, + "sha256": "8e8f9ce7f3a5867c848e54133a726007af2a534106ba9749ade0f8a86ad73bee" + }, + "index.js": { + "type": "file", + "size": 610, + "sha256": "49cd27d3341c93774bcaa05f1233d358962cdc3482c6dcfccb6d480cc5240dbb" + }, + "legacy.js": { + "type": "file", + "size": 55175, + "sha256": "32776f897128bfa7ab2e6dad1cbab4c09901fd0c093da599da7161fe5dca16ff" + }, + "metadata.js": { + "type": "file", + "size": 5618, + "sha256": "9b5dbdd73c6f901f1f005baecd1f70cf81997ca9cf0897e7309f5c5a73eb77cc" + }, + "names.js": { + "type": "file", + "size": 816, + "sha256": "d40ce599c1a9c21903ff6889ffb3f9d682d48eb269797ee960db9084d93905f9" + }, + "rewards.js": { + "type": "file", + "size": 12445, + "sha256": "5deddf3784b4e7ae8a757d4ff06481d0f32f1f31fed86b939cc1f9184e462396" + }, + "runtime.js": { + "type": "file", + "size": 21392, + "sha256": "ca6d9887fb9f8978c34ade455f564b3a0a52eceb5b844447e863390acdb3bed9" + }, + "traits.js": { + "type": "file", + "size": 8277, + "sha256": "38e1b081a3789f3760c4a0889b34803667006b326dce1f8e2aa2c862d997a549" + } + } + }, + "rewards.js": { + "type": "file", + "size": 327, + "sha256": "e7f15d41aa526c6b6ba93b137dd2e31f63fbace91d1bf8c3f8dbdd0fa6ac3176" + }, + "shared": { + "type": "directory", + "children": { + "data.js": { + "type": "file", + "size": 405, + "sha256": "93244ab064a100fa9a93f0cb19acc1d22b6ce17036b2f5cc71fd4f1db42f8497" + }, + "runtime.js": { + "type": "file", + "size": 41670, + "sha256": "f1f6541bae4da3d38911e62f51fc930270c16b5a1b2c3a4348c0712925396a7d" + } + } + }, + "status": { + "type": "directory", + "children": { + "data.js": { + "type": "file", + "size": 4513, + "sha256": "16de512e265f4062ed9fa077d655a416147308b72d0893348c0ad8c27f5b89b3" + } + } + } + } + }, + "personal-piles.js": { + "type": "file", + "size": 24202, + "sha256": "47c23c71b36d6643953a783b3a74c85494cd2f04880d9cbfa44db69e47cfb32f" + }, + "rampage.js": { + "type": "file", + "size": 5265, + "sha256": "7563d2316cece8ab1d9b04de28b47e4562b82ad8a6dd493e1c7f64e726a6a94c" + }, + "random-hand-exhaust.js": { + "type": "file", + "size": 1293, + "sha256": "486758ef934e381bd825a46b07e140f5f28e08eceadba25dd9f107ffefe80372" + }, + "rarity-data.js": { + "type": "file", + "size": 440, + "sha256": "ab3858d78537599e2b108e321e9e92dff7b414d8fb94ae472f55a7b0a54d3ec7" + }, + "rarity.js": { + "type": "file", + "size": 3011, + "sha256": "2e2df62319ba5a95d76abc2a1864febe162809f07478103da26925d593c55224" + }, + "retired-cards.js": { + "type": "file", + "size": 1575, + "sha256": "ce2f18876f10dbc685a712c990a289731fcff3f2e02256363f9476c68b7bfb2d" + }, + "shared-battle-end.js": { + "type": "file", + "size": 946, + "sha256": "424b1e24bd19ebaee6a890521bdc4e30ecb26e7145581311208b1d9375b972f9" + }, + "shared-hooks.js": { + "type": "file", + "size": 823, + "sha256": "bcd757ce6c1ea9c4f56ccfcfcec7adc7b8184c9e4567f0f4b4519a8a867cd7a0" + }, + "slimed-card.js": { + "type": "file", + "size": 1037, + "sha256": "e3cb9102bc28bbbd561f546dfe9cd6832a69e2336e9d989b1ad8d3636bf5e64e" + }, + "spite.js": { + "type": "file", + "size": 1832, + "sha256": "6cef9cd617cfce569238dbc90db242140724a4da3e4963581649cd1848e15d90" + }, + "starting-deck.js": { + "type": "file", + "size": 1668, + "sha256": "89367f707791d9f275b5fd5b6cf54b4d47e80fe9133b4a314a9a0bf4b9cdea3f" + }, + "status-cards.js": { + "type": "file", + "size": 746, + "sha256": "a2c623938bd4d9264c605899bb94cb6599bfcd962ebb5c06a59cb084236ab325" + }, + "strike-counter.js": { + "type": "file", + "size": 1534, + "sha256": "a3de2b465d19a2f14de20ad51e915b4e3459def64fb355627ed2791865a83af6" + }, + "temporary-strength.js": { + "type": "file", + "size": 3764, + "sha256": "5b843d1f9367108e190a4170644c644f83e113d728ada4fb7d2a997f12ea2fc7" + }, + "upgrades.js": { + "type": "file", + "size": 1892, + "sha256": "78f37bd5da682b3a1a3b679df25c026f256adeb5ccc48f6caa0be5d58bdb7529" + }, + "whirlwind.js": { + "type": "file", + "size": 1581, + "sha256": "a62955b5d5291bc887e7c36b808cecbd189d1eb1506cd67873e7298f9369bf9d" + }, + "wither-card.js": { + "type": "file", + "size": 869, + "sha256": "8e4cac00e3196c1a4315c36c63e6a0ce36621e7fc2f73e44fa811a424254d17c" + }, + "wound-card.js": { + "type": "file", + "size": 1891, + "sha256": "2ac15f4252546c183b3de546bd6134a75f8b42ea67632f0d57b8fbd35a8796ea" + } + } + }, + "config.js": { + "type": "file", + "size": 875, + "sha256": "7fa71a17b12d31b1e8ce279b396c0969ddcf8986a6d6ce9e9583aa476b4685c9" + }, + "content": { + "type": "directory", + "children": { + "acts": { + "type": "directory", + "children": { + "act1": { + "type": "directory", + "children": { + "encounters.js": { + "type": "file", + "size": 3800, + "sha256": "62d9a8fd585cfbcc7cc5ad2916d1e5b30d42834e1f89167b7f670b36dcde54db" + }, + "index.js": { + "type": "file", + "size": 464, + "sha256": "b9d3989f85ad8893c755d7364a35488a4c170dba9c09aac2e7e3c7c70553d580" + }, + "inklet-group.js": { + "type": "file", + "size": 516, + "sha256": "bbc6ef9d321b8d04431acd03b2648fc71bb3cb4d892e695b6ac41d667d2ae22a" + }, + "kin-group.js": { + "type": "file", + "size": 776, + "sha256": "82e43fbd9e9ad997cf07cf5c633ae63f1fe7564156e0ab9429e6c2c713689e23" + }, + "map.js": { + "type": "file", + "size": 1793, + "sha256": "a43b1516a93509fa4035ae6c23d4b106566248161a61eb15983b36e0c9495747" + }, + "nibbit-pair.js": { + "type": "file", + "size": 475, + "sha256": "e339e7d91ea24a86b409b89d05a9d6c6846023de343f951f840f68c3baab4a30" + }, + "raider-trio.js": { + "type": "file", + "size": 1595, + "sha256": "a18f6bc3b1074bdfda23a8ea7887e4fd581a88698045f49e2ea94a6b989ea412" + }, + "rewards.js": { + "type": "file", + "size": 797, + "sha256": "dc9c450a77abe5cd35c4ab46dc8833e49287a9d63fa773bb0da1fd64525eb77a" + }, + "stories": { + "type": "directory", + "children": { + "forest-ambush.js": { + "type": "file", + "size": 5225, + "sha256": "de94b30bafc2e4251a6a70ecaf4b1ceaf300cc2750f71d30014971e1c389b240" + }, + "liubei-opening.js": { + "type": "file", + "size": 5731, + "sha256": "1d61fbea5194a1960d9e6bc95d0f5b733687afae7d769a1ac9e0846be97341b4" + } + } + } + } + }, + "act2": { + "type": "directory", + "children": { + "index.js": { + "type": "file", + "size": 129, + "sha256": "d91675bca78c4064b2876c6e9601395aa776f679fe40e63d2385b484d1767c37" + }, + "map.js": { + "type": "file", + "size": 402, + "sha256": "9a8a4a282402a8c25759408a57afba8d73a638d1f64d24407638b1f9156ce6a5" + } + } + }, + "act3": { + "type": "directory", + "children": { + "index.js": { + "type": "file", + "size": 129, + "sha256": "d91675bca78c4064b2876c6e9601395aa776f679fe40e63d2385b484d1767c37" + }, + "map.js": { + "type": "file", + "size": 411, + "sha256": "274a4263af5db5e4d57e71702293c3a53fcc3ae9117e9c0ed5e86b6fa33ba6d1" + } + } + } + } + }, + "encounters.js": { + "type": "file", + "size": 870, + "sha256": "5f1cf6053693df8992cdece2a818ea56cbd6ddf023a60e42e2d0e54e27426fd2" + }, + "registry.js": { + "type": "file", + "size": 7581, + "sha256": "71a6aa7e0048992fb38e0a3084f793f4874deefd614bbba3a84d22dea45e0a7a" + }, + "scenario-characters.js": { + "type": "file", + "size": 9835, + "sha256": "608a47959212ece33171e7d12c2116701e09dac98634f97279e294192e03dfc0" + }, + "shared": { + "type": "directory", + "children": { + "index.js": { + "type": "file", + "size": 126, + "sha256": "25625b636c2b82e58c0792c40b7f32562abca99e0fd7861b73a74cdf40c708af" + }, + "rewards.js": { + "type": "file", + "size": 9046, + "sha256": "ed2b83da3e322cd6e1d2bf15c5011757faf380e1b8343efbebdb06c83c311736" + } + } + } + } + }, + "mode.js": { + "type": "file", + "size": 113891, + "sha256": "a4f7498ef506f894890fbcbd3058fab0c13876657d3d207b3e99be9829702577" + }, + "monsters": { + "type": "directory", + "children": { + "actions.js": { + "type": "file", + "size": 34417, + "sha256": "97461914dd3a40e0bba6cb5e1ff41fe34208ab996dafcc64ed5a33d0d3cdebc5" + }, + "artifact-status.js": { + "type": "file", + "size": 2385, + "sha256": "b85161c9acc1d92715f4cb7be7603bfea8b5e6d42f4caa9d30835f38575ca03a" + }, + "beast-intent.js": { + "type": "file", + "size": 6805, + "sha256": "3962c0ab114907f692ee2d484311119d0c621dd460f00087875f6a9140886964" + }, + "byrdonis-intent.js": { + "type": "file", + "size": 3062, + "sha256": "59847f053122a0600d6b4c8b0a5573427da14717177fb1461640b8def9141443" + }, + "crawler-intent.js": { + "type": "file", + "size": 1587, + "sha256": "994561eea23a9983931d545ef76281e70dba575563a97059b8b5fc4501a4d40b" + }, + "cubex-intent.js": { + "type": "file", + "size": 2151, + "sha256": "bf4af01cbeb8a45ba47dd7f200fa96d808e18322b2908ed4254465644ed87903" + }, + "effigy-intent.js": { + "type": "file", + "size": 1462, + "sha256": "e244757471a7142e3b796be5f24f5a4765564d49fd6b7ddb1484883a46838a43" + }, + "effigy-slow.js": { + "type": "file", + "size": 3367, + "sha256": "d2c2cf715e24090df2be4e68ff15d55dfdebf862ad87fdfa592bbf7bf20f2f94" + }, + "flyconid-intent.js": { + "type": "file", + "size": 1743, + "sha256": "6ff2be31555c591a872cc0ffcc7aea99a7bc9b6bd5156993f8847156eef8637b" + }, + "fogmog-battle.js": { + "type": "file", + "size": 3856, + "sha256": "c5cba9d10c25f7ece2290110f787e4fc3dfa50f266a9faac4bfdb0ac957868e4" + }, + "fogmog-intent.js": { + "type": "file", + "size": 2160, + "sha256": "367bf74fef71ff55d398902fd14512af5bfecc82e756237f73102fe116381c4e" + }, + "inklet-intent.js": { + "type": "file", + "size": 2925, + "sha256": "db404520db50e2d052beb236578973ae4b61e61c6a57a366c8b63452b01e19a3" + }, + "jaxfruit-intent.js": { + "type": "file", + "size": 1256, + "sha256": "46abbda7bf21af236f5e7a4c08a5739649bcf46ea70c59dcb18b5226b2568bb8" + }, + "kin-intent.js": { + "type": "file", + "size": 4393, + "sha256": "3321a96df24d651c2bfea7a7ccfd920be20ae75fa2cb8543abb5985cf7501835" + }, + "leafmedium-intent.js": { + "type": "file", + "size": 1462, + "sha256": "ae373c29e39891992b857afaf11befac1354f163026a797e8bf13ad8d81b71ad" + }, + "leafslime-goop.js": { + "type": "file", + "size": 1653, + "sha256": "133da2f627601e79aa51b6eb87acff777a3ca60c74d142b0d205c40ccbfef3f0" + }, + "leafslime-intent.js": { + "type": "file", + "size": 1403, + "sha256": "3c8f841fb8a02683134a18d1197320b1bb03101f8765e069684654f9ff9ee3c8" + }, + "mawler-intent.js": { + "type": "file", + "size": 1899, + "sha256": "23bfd30298779d9a41c28a3100fb026e17ecd2d1e066f0bb842c57a9ee56b833" + }, + "monster.js": { + "type": "file", + "size": 11658, + "sha256": "1464b42fce32b9266724c8f0eaa06923c1171cff539c337c96af1cc32bd7bc8a" + }, + "nibbit-intent.js": { + "type": "file", + "size": 2292, + "sha256": "b3e34455e8cd88528069345aec2f7f55938b52e05fef4af9b8cd803bbe1579b3" + }, + "phrog-battle.js": { + "type": "file", + "size": 2197, + "sha256": "0c4621e858cc4a4b0e6d1fa86f7d5e06784dfc4ed4e9aba1342c65f6b80f9d02" + }, + "phrog-intent.js": { + "type": "file", + "size": 4060, + "sha256": "e058edf8b6febadcd685dc7c16c05d90e8ba56c1d9b26c132e4ef2fc0e95d9df" + }, + "raider-intent.js": { + "type": "file", + "size": 2060, + "sha256": "b15b89d53914c472c3ef4f2e54e3551bb6a80cd783c21d340676189c2e22c948" + }, + "shrinker-intent.js": { + "type": "file", + "size": 1491, + "sha256": "758ea3ef47d0cf529f09b9733c9fda02c029f7eb30f7ca12b1ccd506267ef90b" + }, + "shrinker-status.js": { + "type": "file", + "size": 2356, + "sha256": "08a44974fb5064ea0f83b3b2d3d6d6030508ef8bef5ed4d8a91f274a8e6b95bf" + }, + "strangler-constrict.js": { + "type": "file", + "size": 3054, + "sha256": "87b012405f7840e5a064e994e9413e8671e4aebe954ee9fd03c9798ed78efe61" + }, + "strangler-intent.js": { + "type": "file", + "size": 1578, + "sha256": "b5ea12fd74db365592ace7c55671bad8b18aa34da9a829852fc1a0e3cf1f229a" + }, + "twigmedium-intent.js": { + "type": "file", + "size": 2576, + "sha256": "c27f58e02f08b87ed62865fc11bb3ae43b5d2098ac199c5714946d24e0afaa87" + }, + "twigslime-intent.js": { + "type": "file", + "size": 1299, + "sha256": "dd772c38e732ebcdda6e5b0ecd4725f73b36cb880ff156f3aaa64470a53633bc" + }, + "vantom-intent.js": { + "type": "file", + "size": 2266, + "sha256": "29c0f36f5b060866e5c28c842883d951d55ceb4f0df7a7e204905fd6d803045b" + }, + "vine-intent.js": { + "type": "file", + "size": 1229, + "sha256": "e2bc8348d9ef5d8ff38b3d65a0b05b741fef31cf12ca6b6abe7b81ce58515b00" + }, + "vine-tangled.js": { + "type": "file", + "size": 2214, + "sha256": "e8eb1e501af16d5da2157ff357bef3193233b4fbcd62cd823a4a6114124abd7e" + } + } + }, + "progression": { + "type": "directory", + "children": { + "dialogue-choice.js": { + "type": "file", + "size": 837, + "sha256": "e00707810480b281bf3e8c25923cf09997d06cf2dc3b7eea35c5a1a47c5cd3dd" + }, + "encounter-progress.js": { + "type": "file", + "size": 1555, + "sha256": "e674c332baef9b3baffbfed3c86c41cd38a5c95bdd4fb3b1009a9819e90a06ba" + }, + "map-checkpoint.js": { + "type": "file", + "size": 314, + "sha256": "c0b5fa06fda99cf4d06570cb7e95fafc141b355055d20b0bd8f7ff91f1c1afa7" + }, + "mode-storage.js": { + "type": "file", + "size": 3833, + "sha256": "83ce9bb6191644f547a0dd79bd46ba86be149047b3e24844a4757270f6a09fcf" + }, + "reward-package.js": { + "type": "file", + "size": 1561, + "sha256": "2cee489f48e5f9d4bfbb9fd1e50dc6d41cf9f778b21432e5aea1bb6730b21364" + }, + "reward.js": { + "type": "file", + "size": 3332, + "sha256": "77e29120ef5bc20b27865c342e5abda0357063b591e226908f8c4bc0f5320c21" + }, + "state.js": { + "type": "file", + "size": 14841, + "sha256": "82a9b2aef527046c9ffb4bbfc470ec2230e6ec42881467a643655a5723a79269" + }, + "story.js": { + "type": "file", + "size": 737, + "sha256": "4db3cf98d5a102cf5bf6d3022b0bdb4c7d70430537e2c4a6a9e541822951f016" + } + } + }, + "register.js": { + "type": "file", + "size": 791, + "sha256": "95290f693f1d5bbc592e35a3cd063201f46d5deffbd0e281682f371b02ae342e" + }, + "relics": { + "type": "directory", + "children": { + "battle.js": { + "type": "file", + "size": 5015, + "sha256": "976e13531625ba00d2941d482d36d9dfd5cf22d1b8de66b519e8b3b928f8a6c9" + }, + "combat.js": { + "type": "file", + "size": 4101, + "sha256": "1fd264d6d07be511cde006ab18f14e5ac8170ad539083a20a3259b1b70548202" + }, + "definitions.js": { + "type": "file", + "size": 4707, + "sha256": "894f66d2a98761926655124f0cbd05acd20aaa8335d61d838fd3cd6bf550d224" + }, + "rooms.js": { + "type": "file", + "size": 1032, + "sha256": "88af2a408d062f78f98dded9e7de4a51c98468eefe217e827d43aa34e3a7410f" + } + } + }, + "ui": { + "type": "directory", + "children": { + "bonds.js": { + "type": "file", + "size": 4499, + "sha256": "2580f60f3b9aac29206a8e1198322505a2283bf81ea379d3a6ca31251f42d5e3" + }, + "card-library.js": { + "type": "file", + "size": 20776, + "sha256": "7d5d1be851490b9d1ccda7b3acf621bf2dbb608f47e0284a200ce788889d7f90" + }, + "dialogue.js": { + "type": "file", + "size": 7842, + "sha256": "e61b22cf27f810a45404e0d05969b03e0a6603d581f8cf7a052a3c30348325f7" + }, + "energy-display.js": { + "type": "file", + "size": 511, + "sha256": "37bc74971d52429c7e1c3d9d0c2dcccb29a61e968ac1bf4eb0a2330801d8fd69" + }, + "flow-ui.js": { + "type": "file", + "size": 3106, + "sha256": "f19d26b3ccd5579885bd9fe9a3444f4d06125898020b165acf91ad2d1f6a37e0" + }, + "gm-manager.js": { + "type": "file", + "size": 4727, + "sha256": "1ffcf493ffcc4d9b16911e81ff62b15693213355c3226270e5224b03042e8868" + }, + "hand": { + "type": "directory", + "children": { + "adapter.js": { + "type": "file", + "size": 955, + "sha256": "dbec5a22b19ba7790c2118328701331b9065cdd7877ad237fa6ba635aad0ab6b" + }, + "diagnostics.js": { + "type": "file", + "size": 740, + "sha256": "9ea7729febf7cba177dcf76ff8a65ef3c0f44ff3e8cc020bbb39a4654837293b" + }, + "fan.js": { + "type": "file", + "size": 4355, + "sha256": "55e1e01dda7b4231c8e1a2fadf6ccbee39b5e98c543bc55500c6493cf6cd51b1" + }, + "index.js": { + "type": "file", + "size": 9105, + "sha256": "a236f0ed58ad3f7953cc79b04d688efa3c7a394d2fbeb3d5ac2eef15f7242fa7" + }, + "input.js": { + "type": "file", + "size": 12278, + "sha256": "b04df574792e6e4be7cce4e0d8bd682efeb091b9b752b40a0b110575442adfbb" + }, + "overlays.js": { + "type": "file", + "size": 3135, + "sha256": "d89e62b5c73b64dfbd2f449e95b6362d2273ef57bc8db72f552af200d3dc2ad8" + }, + "style.css": { + "type": "file", + "size": 35634, + "sha256": "c31ed6dd5b7b2e9d48125f8503512d3af2c0050cb9144dcfdc9d571c351450d6" + } + } + }, + "intent-display.js": { + "type": "file", + "size": 10744, + "sha256": "a920a929183f2815c6ef4226af0585be0f876196b6a061e287b02654068f7083" + }, + "map": { + "type": "directory", + "children": { + "index.js": { + "type": "file", + "size": 62, + "sha256": "6fb7e6e2547eec9d421727b9d6794e3c96d8e3a77984d7688544a67a00c4999d" + }, + "renderer.js": { + "type": "file", + "size": 25409, + "sha256": "a9282e2e6ce30973be064a73ce01b5e13cd580179bde24f9482661e9a2b94571" + } + } + }, + "navigation.js": { + "type": "file", + "size": 5274, + "sha256": "22bf6b5ec02c4917311540aac56c1c20b43df5b761acdb867937399db2c2c7c6" + }, + "relics.js": { + "type": "file", + "size": 3452, + "sha256": "98049bd5a21a314475211fdb681b4989075b8eca4d33d58e88f4fd4cec19455e" + }, + "reward-package.js": { + "type": "file", + "size": 4316, + "sha256": "1c584d2c5bd7252eeaeeaa1da5dbfb11c3df7c364dab0c82f557ca1981afb70a" + }, + "reward-ui.js": { + "type": "file", + "size": 15399, + "sha256": "ad9e727b90aef2cfcea2a7f757b5c12e43655ea055d0729fd955173076a80ef9" + }, + "style.css": { + "type": "file", + "size": 78961, + "sha256": "beaa52e86c3f30144c55333c110e973944180c5d2f17f275821ce75cfbdf2a37" + }, + "virtual-card-grid.js": { + "type": "file", + "size": 4509, + "sha256": "0d8aa28041246a20895a289ab360e982044b2028f6c03090a0039ce85f7e2977" + } + } + } + } + } + } + }, + "module_id": "mengsan" +} \ No newline at end of file diff --git a/dist/modules/mengsan.legacy.json b/dist/modules/mengsan.legacy.json new file mode 100644 index 0000000..4890a77 --- /dev/null +++ b/dist/modules/mengsan.legacy.json @@ -0,0 +1,507 @@ +{ + "version": "2.0.1.1-rc.1", + "module_id": "mengsan", + "snapshot": "legacy", + "files": [ + "mengsan/assets/card-overlays/infection-sprite.webp", + "mengsan/assets/cards/mengsan_baozou.png", + "mengsan/assets/cards/mengsan_colorless_alchemize.png", + "mengsan/assets/cards/mengsan_colorless_anointed.png", + "mengsan/assets/cards/mengsan_colorless_automation.png", + "mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png", + "mengsan/assets/cards/mengsan_colorless_beat_down.png", + "mengsan/assets/cards/mengsan_colorless_believe_in_you.png", + "mengsan/assets/cards/mengsan_colorless_bolas.png", + "mengsan/assets/cards/mengsan_colorless_calamity.png", + "mengsan/assets/cards/mengsan_colorless_catastrophe.png", + "mengsan/assets/cards/mengsan_colorless_coordinate.png", + "mengsan/assets/cards/mengsan_colorless_dark_shackles.png", + "mengsan/assets/cards/mengsan_colorless_discovery.png", + "mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png", + "mengsan/assets/cards/mengsan_colorless_entropy.png", + "mengsan/assets/cards/mengsan_colorless_equilibrium.png", + "mengsan/assets/cards/mengsan_colorless_eternal_armor.png", + "mengsan/assets/cards/mengsan_colorless_fasten.png", + "mengsan/assets/cards/mengsan_colorless_finesse.png", + "mengsan/assets/cards/mengsan_colorless_fisticuffs.png", + "mengsan/assets/cards/mengsan_colorless_flash_of_steel.png", + "mengsan/assets/cards/mengsan_colorless_gang_up.png", + "mengsan/assets/cards/mengsan_colorless_gold_axe.png", + "mengsan/assets/cards/mengsan_colorless_hand_of_greed.png", + "mengsan/assets/cards/mengsan_colorless_hidden_gem.png", + "mengsan/assets/cards/mengsan_colorless_huddle_up.png", + "mengsan/assets/cards/mengsan_colorless_impatience.png", + "mengsan/assets/cards/mengsan_colorless_intercept.png", + "mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png", + "mengsan/assets/cards/mengsan_colorless_jackpot.png", + "mengsan/assets/cards/mengsan_colorless_knockdown.png", + "mengsan/assets/cards/mengsan_colorless_lift.png", + "mengsan/assets/cards/mengsan_colorless_master_of_strategy.png", + "mengsan/assets/cards/mengsan_colorless_mayhem.png", + "mengsan/assets/cards/mengsan_colorless_mimic.png", + "mengsan/assets/cards/mengsan_colorless_mind_blast.png", + "mengsan/assets/cards/mengsan_colorless_nostalgia.png", + "mengsan/assets/cards/mengsan_colorless_omnislice.png", + "mengsan/assets/cards/mengsan_colorless_panache.png", + "mengsan/assets/cards/mengsan_colorless_panic_button.png", + "mengsan/assets/cards/mengsan_colorless_prep_time.png", + "mengsan/assets/cards/mengsan_colorless_production.png", + "mengsan/assets/cards/mengsan_colorless_prolong.png", + "mengsan/assets/cards/mengsan_colorless_prowess.png", + "mengsan/assets/cards/mengsan_colorless_purity.png", + "mengsan/assets/cards/mengsan_colorless_rally.png", + "mengsan/assets/cards/mengsan_colorless_rend.png", + "mengsan/assets/cards/mengsan_colorless_restlessness.png", + "mengsan/assets/cards/mengsan_colorless_rolling_boulder.png", + "mengsan/assets/cards/mengsan_colorless_salvo.png", + "mengsan/assets/cards/mengsan_colorless_scrawl.png", + "mengsan/assets/cards/mengsan_colorless_secret_technique.png", + "mengsan/assets/cards/mengsan_colorless_secret_weapon.png", + "mengsan/assets/cards/mengsan_colorless_seeker_strike.png", + "mengsan/assets/cards/mengsan_colorless_shockwave.png", + "mengsan/assets/cards/mengsan_colorless_splash.png", + "mengsan/assets/cards/mengsan_colorless_stratagem.png", + "mengsan/assets/cards/mengsan_colorless_tag_team.png", + "mengsan/assets/cards/mengsan_colorless_the_ball.png", + "mengsan/assets/cards/mengsan_colorless_the_bomb.png", + "mengsan/assets/cards/mengsan_colorless_the_gambit.png", + "mengsan/assets/cards/mengsan_colorless_thinking_ahead.png", + "mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png", + "mengsan/assets/cards/mengsan_colorless_ultimate_defend.png", + "mengsan/assets/cards/mengsan_colorless_ultimate_strike.png", + "mengsan/assets/cards/mengsan_colorless_volley.png", + "mengsan/assets/cards/mengsan_curse_ascenders_bane.png", + "mengsan/assets/cards/mengsan_curse_bad_luck.png", + "mengsan/assets/cards/mengsan_curse_clumsy.png", + "mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png", + "mengsan/assets/cards/mengsan_curse_debt.png", + "mengsan/assets/cards/mengsan_curse_decay.png", + "mengsan/assets/cards/mengsan_curse_doubt.png", + "mengsan/assets/cards/mengsan_curse_enthralled.png", + "mengsan/assets/cards/mengsan_curse_folly.png", + "mengsan/assets/cards/mengsan_curse_greed.png", + "mengsan/assets/cards/mengsan_curse_guilty.png", + "mengsan/assets/cards/mengsan_curse_injury.png", + "mengsan/assets/cards/mengsan_curse_normality.png", + "mengsan/assets/cards/mengsan_curse_poor_sleep.png", + "mengsan/assets/cards/mengsan_curse_regret.png", + "mengsan/assets/cards/mengsan_curse_shame.png", + "mengsan/assets/cards/mengsan_curse_spore_mind.png", + "mengsan/assets/cards/mengsan_curse_writhe.png", + "mengsan/assets/cards/mengsan_dazed_shuying.png", + "mengsan/assets/cards/mengsan_event_abundance.png", + "mengsan/assets/cards/mengsan_event_apotheosis.png", + "mengsan/assets/cards/mengsan_event_apparition.png", + "mengsan/assets/cards/mengsan_event_brightest_flame.png", + "mengsan/assets/cards/mengsan_event_byrd_swoop.png", + "mengsan/assets/cards/mengsan_event_enlightenment.png", + "mengsan/assets/cards/mengsan_event_exterminate.png", + "mengsan/assets/cards/mengsan_event_feeding_frenzy.png", + "mengsan/assets/cards/mengsan_event_mad_science_chaos.png", + "mengsan/assets/cards/mengsan_event_mad_science_choking.png", + "mengsan/assets/cards/mengsan_event_mad_science_curious.png", + "mengsan/assets/cards/mengsan_event_mad_science_energized.png", + "mengsan/assets/cards/mengsan_event_mad_science_expertise.png", + "mengsan/assets/cards/mengsan_event_mad_science_improvement.png", + "mengsan/assets/cards/mengsan_event_mad_science_sapping.png", + "mengsan/assets/cards/mengsan_event_mad_science_violence.png", + "mengsan/assets/cards/mengsan_event_mad_science_wisdom.png", + "mengsan/assets/cards/mengsan_event_maul.png", + "mengsan/assets/cards/mengsan_event_metamorphosis.png", + "mengsan/assets/cards/mengsan_event_neows_fury.png", + "mengsan/assets/cards/mengsan_event_peck.png", + "mengsan/assets/cards/mengsan_event_relax.png", + "mengsan/assets/cards/mengsan_event_squash.png", + "mengsan/assets/cards/mengsan_event_toric_toughness.png", + "mengsan/assets/cards/mengsan_event_whistle.png", + "mengsan/assets/cards/mengsan_event_wish.png", + "mengsan/assets/cards/mengsan_fangxue.png", + "mengsan/assets/cards/mengsan_fangyu.png", + "mengsan/assets/cards/mengsan_feijianhuixuanbiao.png", + "mengsan/assets/cards/mengsan_fennu.png", + "mengsan/assets/cards/mengsan_ic_ashen_strike.png", + "mengsan/assets/cards/mengsan_ic_barricade.png", + "mengsan/assets/cards/mengsan_ic_battle_ready.png", + "mengsan/assets/cards/mengsan_ic_battle_trance.png", + "mengsan/assets/cards/mengsan_ic_blaze.png", + "mengsan/assets/cards/mengsan_ic_bludgeon.png", + "mengsan/assets/cards/mengsan_ic_bodyguard.png", + "mengsan/assets/cards/mengsan_ic_boulder.png", + "mengsan/assets/cards/mengsan_ic_brand.png", + "mengsan/assets/cards/mengsan_ic_break.png", + "mengsan/assets/cards/mengsan_ic_bully.png", + "mengsan/assets/cards/mengsan_ic_burning_pact.png", + "mengsan/assets/cards/mengsan_ic_clash.png", + "mengsan/assets/cards/mengsan_ic_colossus.png", + "mengsan/assets/cards/mengsan_ic_conflagration.png", + "mengsan/assets/cards/mengsan_ic_corruption.png", + "mengsan/assets/cards/mengsan_ic_crimson_mantle.png", + "mengsan/assets/cards/mengsan_ic_cruelty.png", + "mengsan/assets/cards/mengsan_ic_dark_embrace.png", + "mengsan/assets/cards/mengsan_ic_demon_form.png", + "mengsan/assets/cards/mengsan_ic_demonic_shield.png", + "mengsan/assets/cards/mengsan_ic_dismantle.png", + "mengsan/assets/cards/mengsan_ic_dominate.png", + "mengsan/assets/cards/mengsan_ic_drum_of_battle.png", + "mengsan/assets/cards/mengsan_ic_dual_wield.png", + "mengsan/assets/cards/mengsan_ic_eager.png", + "mengsan/assets/cards/mengsan_ic_entrench.png", + "mengsan/assets/cards/mengsan_ic_evil_eye.png", + "mengsan/assets/cards/mengsan_ic_feed.png", + "mengsan/assets/cards/mengsan_ic_feel_no_pain.png", + "mengsan/assets/cards/mengsan_ic_ferocity.png", + "mengsan/assets/cards/mengsan_ic_fiend_fire.png", + "mengsan/assets/cards/mengsan_ic_fight_me.png", + "mengsan/assets/cards/mengsan_ic_flame_barrier.png", + "mengsan/assets/cards/mengsan_ic_forgotten_ritual.png", + "mengsan/assets/cards/mengsan_ic_grapple.png", + "mengsan/assets/cards/mengsan_ic_hellraiser.png", + "mengsan/assets/cards/mengsan_ic_hemokinesis.png", + "mengsan/assets/cards/mengsan_ic_howl_from_beyond.png", + "mengsan/assets/cards/mengsan_ic_impervious.png", + "mengsan/assets/cards/mengsan_ic_infernal_blade.png", + "mengsan/assets/cards/mengsan_ic_inferno.png", + "mengsan/assets/cards/mengsan_ic_inflame.png", + "mengsan/assets/cards/mengsan_ic_juggernaut.png", + "mengsan/assets/cards/mengsan_ic_juggle.png", + "mengsan/assets/cards/mengsan_ic_midnight.png", + "mengsan/assets/cards/mengsan_ic_not_yet.png", + "mengsan/assets/cards/mengsan_ic_offering.png", + "mengsan/assets/cards/mengsan_ic_pacts_end.png", + "mengsan/assets/cards/mengsan_ic_pillage.png", + "mengsan/assets/cards/mengsan_ic_primal_force.png", + "mengsan/assets/cards/mengsan_ic_pyre.png", + "mengsan/assets/cards/mengsan_ic_rage.png", + "mengsan/assets/cards/mengsan_ic_rally.png", + "mengsan/assets/cards/mengsan_ic_relentless_assault.png", + "mengsan/assets/cards/mengsan_ic_rupture.png", + "mengsan/assets/cards/mengsan_ic_second_wind.png", + "mengsan/assets/cards/mengsan_ic_stampede.png", + "mengsan/assets/cards/mengsan_ic_stoke.png", + "mengsan/assets/cards/mengsan_ic_stomp.png", + "mengsan/assets/cards/mengsan_ic_stone_armor.png", + "mengsan/assets/cards/mengsan_ic_taunt.png", + "mengsan/assets/cards/mengsan_ic_tear_asunder.png", + "mengsan/assets/cards/mengsan_ic_thrash.png", + "mengsan/assets/cards/mengsan_ic_twin_strike.png", + "mengsan/assets/cards/mengsan_ic_unleash.png", + "mengsan/assets/cards/mengsan_ic_unshakable.png", + "mengsan/assets/cards/mengsan_ic_uppercut.png", + "mengsan/assets/cards/mengsan_infection_shuying.png", + "mengsan/assets/cards/mengsan_jianbingdaji.png", + "mengsan/assets/cards/mengsan_jianyi.png", + "mengsan/assets/cards/mengsan_pomie.png", + "mengsan/assets/cards/mengsan_qiling.png", + "mengsan/assets/cards/mengsan_quanshenzhuangji.png", + "mengsan/assets/cards/mengsan_rongrongzhiquan.png", + "mengsan/assets/cards/mengsan_shandianpili.png", + "mengsan/assets/cards/mengsan_shuangchongdaji.png", + "mengsan/assets/cards/mengsan_slimed_shuying.png", + "mengsan/assets/cards/mengsan_songjianwushi.png", + "mengsan/assets/cards/mengsan_status_beckon.png", + "mengsan/assets/cards/mengsan_status_burn.png", + "mengsan/assets/cards/mengsan_status_debris.png", + "mengsan/assets/cards/mengsan_status_deprecated_card.png", + "mengsan/assets/cards/mengsan_status_frantic_escape.png", + "mengsan/assets/cards/mengsan_status_soot.png", + "mengsan/assets/cards/mengsan_status_toxic.png", + "mengsan/assets/cards/mengsan_status_void.png", + "mengsan/assets/cards/mengsan_status_wither.png", + "mengsan/assets/cards/mengsan_tiezhanbo.png", + "mengsan/assets/cards/mengsan_touchui.png", + "mengsan/assets/cards/mengsan_tupo.png", + "mengsan/assets/cards/mengsan_wanmeidaji.png", + "mengsan/assets/cards/mengsan_wound_shuying.png", + "mengsan/assets/cards/mengsan_wuzhuang.png", + "mengsan/assets/cards/mengsan_xuanfengzhan.png", + "mengsan/assets/cards/mengsan_xueqiang.png", + "mengsan/assets/cards/mengsan_yuanhen.png", + "mengsan/assets/cards/mengsan_yubeidaji.png", + "mengsan/assets/cards/mengsan_yujin.png", + "mengsan/assets/cards/mengsan_zhanli.png", + "mengsan/assets/cards/mengsan_zhongsha.png", + "mengsan/assets/cards/wound-original.png", + "mengsan/assets/flyconid-portrait.png", + "mengsan/assets/intent/Intent_attack_1.png", + "mengsan/assets/intent/Intent_attack_1.webp", + "mengsan/assets/intent/Intent_attack_2.png", + "mengsan/assets/intent/Intent_attack_2.webp", + "mengsan/assets/intent/Intent_attack_3.png", + "mengsan/assets/intent/Intent_attack_3.webp", + "mengsan/assets/intent/Intent_attack_4.png", + "mengsan/assets/intent/Intent_attack_4.webp", + "mengsan/assets/intent/Intent_attack_5.png", + "mengsan/assets/intent/Intent_attack_5.webp", + "mengsan/assets/intent/Intent_buff.png", + "mengsan/assets/intent/Intent_card_debuff.png", + "mengsan/assets/intent/Intent_death_blow.png", + "mengsan/assets/intent/Intent_debuff.png", + "mengsan/assets/intent/Intent_debuff.webp", + "mengsan/assets/intent/Intent_defend.png", + "mengsan/assets/intent/Intent_escape.png", + "mengsan/assets/intent/Intent_heal.png", + "mengsan/assets/intent/Intent_sleep.png", + "mengsan/assets/intent/Intent_status_card.png", + "mengsan/assets/intent/Intent_stun.png", + "mengsan/assets/intent/Intent_summon.png", + "mengsan/assets/intent/Intent_unknown.png", + "mengsan/assets/portraits/bygone-effigy-forest-portrait.png", + "mengsan/assets/portraits/bygone-effigy-original.png", + "mengsan/assets/portraits/byrdonis-forest-portrait.png", + "mengsan/assets/portraits/byrdonis-original.png", + "mengsan/assets/portraits/ceremonial-beast-forest-portrait.png", + "mengsan/assets/portraits/ceremonial-beast-original.png", + "mengsan/assets/portraits/cubex-construct-forest-portrait.png", + "mengsan/assets/portraits/cubex-construct-original.webp", + "mengsan/assets/portraits/eye-with-teeth-forest-portrait.png", + "mengsan/assets/portraits/eye-with-teeth.webp", + "mengsan/assets/portraits/flyconid-portrait.png", + "mengsan/assets/portraits/fogmog-and-eye-forest-portrait.png", + "mengsan/assets/portraits/fogmog-forest-portrait.png", + "mengsan/assets/portraits/fogmog.webp", + "mengsan/assets/portraits/fuzzy-wurm-crawler-forest-portrait.png", + "mengsan/assets/portraits/fuzzy-wurm-crawler-original.webp", + "mengsan/assets/portraits/guanyu-portrait.jpg", + "mengsan/assets/portraits/inklet-forest-portrait.png", + "mengsan/assets/portraits/inklet-original.png", + "mengsan/assets/portraits/kin-follower-forest-portrait.png", + "mengsan/assets/portraits/kin-follower-original.webp", + "mengsan/assets/portraits/kin-priest-forest-portrait.png", + "mengsan/assets/portraits/kin-priest-original.png", + "mengsan/assets/portraits/leaf-slime-m-forest-portrait.png", + "mengsan/assets/portraits/leaf-slime-m-original.png", + "mengsan/assets/portraits/leaf-slime-s-forest-portrait.png", + "mengsan/assets/portraits/leaf-slime-s-original.png", + "mengsan/assets/portraits/liubei-portrait.jpg", + "mengsan/assets/portraits/mawler-forest-portrait.png", + "mengsan/assets/portraits/mawler-original.webp", + "mengsan/assets/portraits/nibbit-forest-portrait.png", + "mengsan/assets/portraits/nibbit-original.webp", + "mengsan/assets/portraits/phrog-parasite-forest-portrait.png", + "mengsan/assets/portraits/phrog-parasite-original.png", + "mengsan/assets/portraits/raider-assassin-portrait.png", + "mengsan/assets/portraits/raider-axe-portrait.png", + "mengsan/assets/portraits/raider-brute-portrait.png", + "mengsan/assets/portraits/raider-crossbow-portrait.png", + "mengsan/assets/portraits/raider-tracker-portrait.png", + "mengsan/assets/portraits/shrinker-beetle-forest-portrait.png", + "mengsan/assets/portraits/shrinker-beetle-original.png", + "mengsan/assets/portraits/slithering-strangler-forest-portrait.png", + "mengsan/assets/portraits/slithering-strangler-original.png", + "mengsan/assets/portraits/snapping-jaxfruit-forest-portrait.png", + "mengsan/assets/portraits/snapping-jaxfruit-original.png", + "mengsan/assets/portraits/sushuang-zhangshiping.png", + "mengsan/assets/portraits/twig-slime-m-forest-portrait.png", + "mengsan/assets/portraits/twig-slime-m-original.png", + "mengsan/assets/portraits/twig-slime-s-forest-portrait.png", + "mengsan/assets/portraits/twig-slime-s-original.png", + "mengsan/assets/portraits/vantom-forest-portrait.png", + "mengsan/assets/portraits/vantom-original.png", + "mengsan/assets/portraits/vine-shambler-forest-portrait.png", + "mengsan/assets/portraits/vine-shambler-original-thumbnail.webp", + "mengsan/assets/portraits/wriggler-forest-portrait.png", + "mengsan/assets/portraits/wriggler-original.webp", + "mengsan/assets/portraits/zhangfei-portrait.jpg", + "mengsan/assets/sushuang-zhangshiping.png", + "mengsan/battle/battle-director.js", + "mengsan/battle/battle-flow.js", + "mengsan/battle/battle-loop.js", + "mengsan/battle/battle-resources.js", + "mengsan/battle/battle-session.js", + "mengsan/battle/battle-settlement.js", + "mengsan/battle/combat-rules.js", + "mengsan/battle/death-blow.js", + "mengsan/battle/engine-session.js", + "mengsan/battle/intent-damage.js", + "mengsan/battle/intent-effects.js", + "mengsan/battle/skill-teardown.js", + "mengsan/battle/stun-intent.js", + "mengsan/bonds/battle.js", + "mengsan/bonds/definitions.js", + "mengsan/bonds/intents.js", + "mengsan/bonds/state.js", + "mengsan/cards/affixes.js", + "mengsan/cards/battle-card-copy.js", + "mengsan/cards/battle-card-reclaim.js", + "mengsan/cards/battle-energy.js", + "mengsan/cards/block-draw.js", + "mengsan/cards/card-data.js", + "mengsan/cards/card-definitions.js", + "mengsan/cards/card-pools.js", + "mengsan/cards/classification-types.js", + "mengsan/cards/description.js", + "mengsan/cards/hand-exhaust.js", + "mengsan/cards/hand-upgrade.js", + "mengsan/cards/havoc-play.js", + "mengsan/cards/infection-card.js", + "mengsan/cards/intrinsic-affixes.js", + "mengsan/cards/ironclad-advanced-data.js", + "mengsan/cards/ironclad-data.js", + "mengsan/cards/ironclad-hooks.js", + "mengsan/cards/ironclad.js", + "mengsan/cards/life-loss-block.js", + "mengsan/cards/mode-cards.js", + "mengsan/cards/packs/colorless/data.js", + "mengsan/cards/packs/colorless/index.js", + "mengsan/cards/packs/colorless/rewards.js", + "mengsan/cards/packs/curse/data.js", + "mengsan/cards/packs/data.js", + "mengsan/cards/packs/event/data.js", + "mengsan/cards/packs/index.js", + "mengsan/cards/packs/liubei/advanced-data.js", + "mengsan/cards/packs/liubei/advanced.js", + "mengsan/cards/packs/liubei/data.js", + "mengsan/cards/packs/liubei/index.js", + "mengsan/cards/packs/liubei/legacy.js", + "mengsan/cards/packs/liubei/metadata.js", + "mengsan/cards/packs/liubei/names.js", + "mengsan/cards/packs/liubei/rewards.js", + "mengsan/cards/packs/liubei/runtime.js", + "mengsan/cards/packs/liubei/traits.js", + "mengsan/cards/packs/rewards.js", + "mengsan/cards/packs/shared/data.js", + "mengsan/cards/packs/shared/runtime.js", + "mengsan/cards/packs/status/data.js", + "mengsan/cards/personal-piles.js", + "mengsan/cards/rampage.js", + "mengsan/cards/random-hand-exhaust.js", + "mengsan/cards/rarity-data.js", + "mengsan/cards/rarity.js", + "mengsan/cards/retired-cards.js", + "mengsan/cards/shared-battle-end.js", + "mengsan/cards/shared-hooks.js", + "mengsan/cards/slimed-card.js", + "mengsan/cards/spite.js", + "mengsan/cards/starting-deck.js", + "mengsan/cards/status-cards.js", + "mengsan/cards/strike-counter.js", + "mengsan/cards/temporary-strength.js", + "mengsan/cards/upgrades.js", + "mengsan/cards/whirlwind.js", + "mengsan/cards/wither-card.js", + "mengsan/cards/wound-card.js", + "mengsan/config.js", + "mengsan/content/acts/act1/encounters.js", + "mengsan/content/acts/act1/index.js", + "mengsan/content/acts/act1/inklet-group.js", + "mengsan/content/acts/act1/kin-group.js", + "mengsan/content/acts/act1/map.js", + "mengsan/content/acts/act1/nibbit-pair.js", + "mengsan/content/acts/act1/raider-trio.js", + "mengsan/content/acts/act1/rewards.js", + "mengsan/content/acts/act1/stories/forest-ambush.js", + "mengsan/content/acts/act1/stories/liubei-opening.js", + "mengsan/content/acts/act2/index.js", + "mengsan/content/acts/act2/map.js", + "mengsan/content/acts/act3/index.js", + "mengsan/content/acts/act3/map.js", + "mengsan/content/affixes.js", + "mengsan/content/card-definitions.js", + "mengsan/content/card-library.js", + "mengsan/content/encounters.js", + "mengsan/content/mode-cards.js", + "mengsan/content/registry.js", + "mengsan/content/scenario-characters.js", + "mengsan/content/shared/index.js", + "mengsan/content/shared/rewards.js", + "mengsan/docs/battle-config.md", + "mengsan/docs/combat-rules.md", + "mengsan/docs/liubei-opening.md", + "mengsan/map/index.js", + "mengsan/map/renderer.js", + "mengsan/mode.js", + "mengsan/monsters/actions.js", + "mengsan/monsters/artifact-status.js", + "mengsan/monsters/beast-intent.js", + "mengsan/monsters/byrdonis-intent.js", + "mengsan/monsters/crawler-intent.js", + "mengsan/monsters/cubex-intent.js", + "mengsan/monsters/effigy-intent.js", + "mengsan/monsters/effigy-slow.js", + "mengsan/monsters/flyconid-intent.js", + "mengsan/monsters/fogmog-battle.js", + "mengsan/monsters/fogmog-intent.js", + "mengsan/monsters/inklet-intent.js", + "mengsan/monsters/jaxfruit-intent.js", + "mengsan/monsters/kin-intent.js", + "mengsan/monsters/leafmedium-intent.js", + "mengsan/monsters/leafslime-goop.js", + "mengsan/monsters/leafslime-intent.js", + "mengsan/monsters/mawler-intent.js", + "mengsan/monsters/monster.js", + "mengsan/monsters/nibbit-intent.js", + "mengsan/monsters/phrog-battle.js", + "mengsan/monsters/phrog-intent.js", + "mengsan/monsters/raider-intent.js", + "mengsan/monsters/shrinker-intent.js", + "mengsan/monsters/shrinker-status.js", + "mengsan/monsters/strangler-constrict.js", + "mengsan/monsters/strangler-intent.js", + "mengsan/monsters/twigmedium-intent.js", + "mengsan/monsters/twigslime-intent.js", + "mengsan/monsters/vantom-intent.js", + "mengsan/monsters/vine-intent.js", + "mengsan/monsters/vine-tangled.js", + "mengsan/progression/dialogue-choice.js", + "mengsan/progression/encounter-progress.js", + "mengsan/progression/map-checkpoint.js", + "mengsan/progression/mode-storage.js", + "mengsan/progression/reward-package.js", + "mengsan/progression/reward.js", + "mengsan/progression/state.js", + "mengsan/progression/story.js", + "mengsan/register.js", + "mengsan/relics/battle.js", + "mengsan/relics/combat.js", + "mengsan/relics/definitions.js", + "mengsan/relics/rooms.js", + "mengsan/runtime/battle-director.js", + "mengsan/runtime/battle-flow.js", + "mengsan/runtime/battle-loop.js", + "mengsan/runtime/battle-resources.js", + "mengsan/runtime/battle-session.js", + "mengsan/runtime/battle-settlement.js", + "mengsan/runtime/card-data.js", + "mengsan/runtime/card-library.js", + "mengsan/runtime/combat-rules.js", + "mengsan/runtime/dialogue-choice.js", + "mengsan/runtime/dialogue.js", + "mengsan/runtime/engine-session.js", + "mengsan/runtime/flow-ui.js", + "mengsan/runtime/flyconid-intent.js", + "mengsan/runtime/gm-manager.js", + "mengsan/runtime/intent-display.js", + "mengsan/runtime/mode-storage.js", + "mengsan/runtime/monster.js", + "mengsan/runtime/navigation.js", + "mengsan/runtime/personal-piles.js", + "mengsan/runtime/reward-ui.js", + "mengsan/runtime/reward.js", + "mengsan/runtime/skill-teardown.js", + "mengsan/runtime/story.js", + "mengsan/state.js", + "mengsan/style.css", + "mengsan/ui/bonds.js", + "mengsan/ui/card-library.js", + "mengsan/ui/dialogue.js", + "mengsan/ui/energy-display.js", + "mengsan/ui/flow-ui.js", + "mengsan/ui/gm-manager.js", + "mengsan/ui/hand/adapter.js", + "mengsan/ui/hand/diagnostics.js", + "mengsan/ui/hand/fan.js", + "mengsan/ui/hand/index.js", + "mengsan/ui/hand/input.js", + "mengsan/ui/hand/overlays.js", + "mengsan/ui/hand/style.css", + "mengsan/ui/intent-display.js", + "mengsan/ui/map/index.js", + "mengsan/ui/map/renderer.js", + "mengsan/ui/navigation.js", + "mengsan/ui/relics.js", + "mengsan/ui/reward-package.js", + "mengsan/ui/reward-ui.js", + "mengsan/ui/style.css", + "mengsan/ui/virtual-card-grid.js" + ] +} \ No newline at end of file diff --git a/extension.js b/extension.js index 0cca5ce..3406099 100644 --- a/extension.js +++ b/extension.js @@ -251,11 +251,14 @@ shuYing.precontent = async () => { shuYing.initFunction(); shuYing.initConfig(); const mengsanState = lib.config.shuYing_mengsan_installed_version; - let hasMengsan = !!mengsanState && mengsanState != "disabled"; - if (!mengsanState && typeof game.checkFile != "function") { + const mengsanInstallation = lib.config.shuYing_mengsan_installation; + let hasMengsan = mengsanInstallation + ? mengsanInstallation.status == "installed" + : !!mengsanState && mengsanState != "disabled"; + if (!mengsanInstallation && !mengsanState && typeof game.checkFile != "function") { hasMengsan = true; } - else if (!mengsanState && typeof game.checkFile == "function") { + else if (!mengsanInstallation && !mengsanState && typeof game.checkFile == "function") { hasMengsan = await new Promise(resolve => { game.checkFile("extension/术樱包/mengsan/register.js", result => resolve(result == 1), () => resolve(false)); diff --git a/mengsan/assets/card-overlays/infection-sprite.webp b/mengsan/assets/card-overlays/infection-sprite.webp new file mode 100644 index 0000000..f2825f2 Binary files /dev/null and b/mengsan/assets/card-overlays/infection-sprite.webp differ diff --git a/mengsan/assets/cards/README.md b/mengsan/assets/cards/README.md index 4666188..f5daee4 100644 --- a/mengsan/assets/cards/README.md +++ b/mengsan/assets/cards/README.md @@ -1,11 +1,13 @@ # 梦三卡牌立绘规范 +2026-10-03:铁甲战士94张已全部接入:新增67、沿用27、暂缓0;另有衍生巨石。68张新增完整5:7水墨立绘使用 `mengsan_ic_*.png`。刘备专属牌组与空无色牌组已分目录和牌组类,详情见 [制作清单](../../../../../sts2-card-capture-20261003/generation/铁甲战士制作清单.md)、[全部立绘预览](../../../../../sts2-card-capture-20261003/generation/铁甲战士立绘预览.md) 及 [牌组结构](../../cards/packs/README.md)。尚未进行实机验收。 + 用户于 2026-10-02 指定以截图 `C:\Users\Diuse\Downloads\Ai\49.png` 中“防御”为同系列基准。 - 后续新增及转换的梦三卡牌使用约 5:7 的完整竖幅立绘;手牌常规尺寸 112×156,窄屏 82×114。禁止直接使用横向拆包图作为运行时卡面,也不要拉伸横图。 - 画风:泛黄宣纸、黑褐水墨、飞白与淡赭色、中国古代人物或兵器;根据卡牌效果独立安排竖幅构图。拆包图只作题材参考,不修改拆包原件。 - 立绘不绘制名称、费用、花色、强化角标或边框。左上花色点数、左侧中段费用、右下名称由游戏原生节点和梦三样式叠加;左侧信息栏和右下名称需留空间。 -- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。新增卡名须加入 `ui/hand/style.css` 的本地完整卡面选择器;长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。 +- 卡牌定义使用 `fullimage: true` 与明确的 `image` 路径,不使用横图 `fullskin`。`mengsan_ic_`、`mengsan_colorless_`、`mengsan_event_`、`mengsan_status_`、`mengsan_curse_` 已按前缀接入 `ui/hand/style.css` 的本地完整卡面选择器;其他命名的新增卡牌须单独接入,长牌名同时加入长名称窄屏规则。保持 `cover`,不使用 `100% 100%` 拉伸。 - 费用和强化角标继续使用现有 `data-*` 与伪元素,避免向卡牌子节点写入额外文字而触发十周年 UI 扫描。 - 不改变卡牌费用、效果、强化、奖励与存档数据;不修改全局标准/军争牌或十周年 UI。 - 接入后检查完整立绘、花色/费用/名称、扇形叠牌、悬停放大和窄屏。文件检查、隔离行为测试及预览不代表实机验收。 @@ -39,3 +41,15 @@ 破灭:对应 HAVOC/havoc.png 的宽刃旋斩与黑披风主题;完整5:7竖幅宣纸水墨立绘。普通1费,当前另一状态0费;免费使用自己的抽牌堆顶部实体牌,再仅消耗该原牌。费用豁免绑定原生选择/使用父链和实体身份,不改卡牌费用或词缀,愤怒生成的副本正常进入自己的弃牌堆,不跟随原牌消耗(对应Wiki0.104.0修复)。原生可用性和目标规则保持有效,无法使用的堆顶牌仍消耗;目标由原生使用流程选择。空抽牌堆不洗牌、不触发抽牌不足失败。保留当前数值、ID、花色、词缀和永久牌组,防止换场/死亡期间陈旧使用。描述只显示当前费用和效果。 耸肩无视:对应 SHRUG_IT_OFF/shrug_it_off.png 的耸肩摊手、厚重肩甲弹开攻击主题;完整5:7竖幅宣纸水墨立绘。1费,先通过原生 changeHujia 获得8点格挡,再通过原生 draw 抽1张牌;当前牌直接显示其实际格挡值。沿用脆弱对格挡的衰减与个人牌堆抽牌/洗牌/不足失败流程;等待格挡完全结算,战斗结束、死亡、换场或个人牌堆被替换后不再抽牌,不触碰公共和其他角色牌堆,不修改永久牌组;无消耗关键词。描述只显示当前费用与效果。 + +武装:对应 ARMAMENTS/armaments.png 的系紧金属护臂、加固防护主题;完整5:7竖幅宣纸水墨立绘。1费,先获得5点格挡,再选择强化一张当时的手牌;当前全体状态强化所有当时的手牌。仅对现有强化规则允许的牌生效,已满级、状态牌、永恒词缀、消耗牌及本次使用的实体牌不参与;没有符合条件的手牌仍获得格挡。等待原生格挡及选牌事件,战斗结束、换场、死亡、牌堆替换或选择期间失去该牌后不提交陈旧选择。复制真实牌元数据,保留ID、花色、点数、词缀并刷新费用与+X角标;不写永久牌组,退出战斗恢复临时强化。描述只显示当前费用和效果。运行时PNG缩小并量化,拆包原件保留。 + +战栗:对应 TREMBLE/tremble.png 的蜷缩颤抖、恐惧易伤主题;完整5:7竖幅黑褐宣纸水墨立绘,不使用横向裁切,不烘焙文字或边框。1费,对一名存活敌人施加3层易伤;当前强化状态施加4层。沿用人工制品抵消、攻击伤害增加50%向上取整与自身回合末减少1层规则;技能牌不造成直接伤害,不承受缠结的攻击牌额外费用。使用后原生消耗,仅退出本场战斗;未使用而弃置仍进入弃牌堆,永久牌组保留。战斗结束、自己或目标死亡及非敌方目标不施加。描述只显示当前费用及当前效果,无强化资格或未来变化说明。运行时PNG保持比例缩小至480×672并量化压缩,拆包参考原件不改动。 + +血墙:对应 BLOOD_WALL/blood_wall.png 的赤红屏障抵挡攻击主题;完整5:7竖幅黑褐宣纸水墨与暗朱红立绘,不裁切横图,不烘焙文字或边框。2费,先失去2点生命,再获得16点格挡;当前强化状态获得20点。等待原生loseHp及濒死救援结算;存活且仍在同一场战斗和同一会话中的使用者才继续获得格挡。战斗结束、死亡、换场、会话替换或角色离场后不继续。生命流失不是攻击伤害,不被护甲抵消,也不受力量、虚弱或易伤修正;格挡沿用原生changeHujia及脆弱规则,无护甲上限。技能牌不承受缠结攻击牌额外费用,无固有消耗,不改永久牌组。描述只显示当前费用和效果,不列强化资格或预告。运行时PNG保持比例缩小至480×672并量化压缩,拆包参考原件不改动。 + +欺凌:对应 BULLY/bully.png 的强者压制、乘虚进攻主题;完整5:7黑褐宣纸水墨竖幅,不裁切横图,不烘焙名称费用边框。0费,对一名存活敌人造成4点基础伤害,目标每有1层当前易伤额外增加2点;当前强化状态每层增加3点。每次结算读取实际目标的当前层数,仅发起一次原生攻击,不消耗易伤、不读取使用者或其他目标层数,不使用回合剩余数量代替层数。基础数值进入既有力量、临时力量、易伤50%及其他攻击修正链,易伤增幅不由牌效果重复乘算。非法负数、非整数、非有限层数或伤害溢出拒绝发起伤害。战斗结束、角色死亡、自身或友方目标不结算;不限制距离,次数不限,无固有消耗。作为攻击牌,缠结时按既有规则增加1费用并显示当前费用,响应仍免费。当前描述不列强化资格或未来效果。运行时PNG保持比例缩小至480×672并量化压缩,拆包参考原件不改动。 + +旋风斩:WHIRLWIND/whirlwind.png 的环形刀风连击主题,完整5:7宣纸水墨竖幅,无烘焙文字费用边框。X费,对全部敌人造成5点伤害X次,当前强化状态为8点。原生useCard支付时消耗剩余费用并固定X,一条用牌只支付、提取一次,次数不随后续加费变化;0费可使用但没有伤害。缠结先扣1附加费用再以剩余费用作X;不足附加费用不可用。破灭授权免费使用时固定现有费用作X但不扣费;响应不收费。次数仅绑定最近原生用牌、使用者、原战斗和原会话,嵌套其他牌不得借用X。逐轮重新取存活敌人,死亡或离场目标跳过,新生成敌人从下一轮参与;死亡、结束、换场、会话替换或使用者离场停止。每次等待原生伤害并沿用力量、临时力量、易伤等,不合并伤害。图鉴新增X费筛选,手牌用空文本data叠层显示X。永久牌数据不写次数,不固有消耗。描述仅当前效果,运行时480×672量化压缩,拆包原件保留。 +怨恨:SPITE/spite.png 的金甲头盔、含毒巨刃与负伤反击主题;完整5:7宣纸水墨竖幅,无烘焙名称费用边框。0费,对一名存活敌人造成5点伤害;本回合失去过生命时攻击2次,当前强化状态攻击3次,每次仍为5点。读取引擎当前完整回合的changeHp历史,仅实际changedHp<0且属于本场根事件、本人时触发,护甲完全抵消或回复不触发,流失生命后再回复仍有效,不读取伤害请求量或永久状态。使用开始时固定次数,攻击期间新失血影响下一次用牌;逐次等待原生伤害并沿用力量、临时力量、易伤等修正。原战斗/会话/根事件及角色成员和存活敌对关系每次复核,死亡、结束、换场、离场后停止。无固有消耗,不写永久牌组;攻击牌沿用缠结增加1费用,响应免费。当前描述不列强化资格或预告。运行时480×672量化压缩,拆包参考原件保留。 +暴走:RAMPAGE/rampage.png 的碧甲狂兽、猩红螺旋眼与逐击增强主题;完整5:7黑褐宣纸水墨竖幅,无烘焙费用名称边框。按网页正式主表1费/9伤害/每次本场加5,当前强化状态每次加9;不采用带未正式发布标记的10伤害更新。先发起一次原生攻击,结算后仅增加原实体牌的下一次基础伤害,普通连续9/14/19;力量、临时力量、易伤等沿原生链修正,不把最终伤害当作累积量。累积以实体对象绑定战斗/会话,独立暴走及同ID的另一个实体不共享,弃牌、回收、重抽和本场强化保留;永久牌组和实体牌元数据不写累积。战斗牌堆快照只携带当前会话的纯数字展示数据,详情显示当前实际伤害;退出会话清除,延迟旧清理不覆盖新会话。绑定最近原生useCard及唯一同名原牌,一次用牌只结算一次,陈旧场次/会话/根事件、非敌方目标、死亡或原牌离开本人的牌堆后不继续。伤害前检查安全整数溢出,无固有消耗;沿用缠结增加1费用,响应免费。当前描述不列强化资格或未来变化。运行时480×672量化压缩,拆包参考原件保留。 diff --git a/mengsan/assets/cards/mengsan_baozou.png b/mengsan/assets/cards/mengsan_baozou.png new file mode 100644 index 0000000..bf4c966 Binary files /dev/null and b/mengsan/assets/cards/mengsan_baozou.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_alchemize.png b/mengsan/assets/cards/mengsan_colorless_alchemize.png new file mode 100644 index 0000000..f2e8c22 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_alchemize.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_anointed.png b/mengsan/assets/cards/mengsan_colorless_anointed.png new file mode 100644 index 0000000..9d0dfaf Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_anointed.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_automation.png b/mengsan/assets/cards/mengsan_colorless_automation.png new file mode 100644 index 0000000..bd9994b Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_automation.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png b/mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png new file mode 100644 index 0000000..20e47fe Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_beacon_of_hope.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_beat_down.png b/mengsan/assets/cards/mengsan_colorless_beat_down.png new file mode 100644 index 0000000..52925dd Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_beat_down.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_believe_in_you.png b/mengsan/assets/cards/mengsan_colorless_believe_in_you.png new file mode 100644 index 0000000..e75fa4c Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_believe_in_you.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_bolas.png b/mengsan/assets/cards/mengsan_colorless_bolas.png new file mode 100644 index 0000000..cf97f94 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_bolas.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_calamity.png b/mengsan/assets/cards/mengsan_colorless_calamity.png new file mode 100644 index 0000000..9900d6a Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_calamity.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_catastrophe.png b/mengsan/assets/cards/mengsan_colorless_catastrophe.png new file mode 100644 index 0000000..2110277 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_catastrophe.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_coordinate.png b/mengsan/assets/cards/mengsan_colorless_coordinate.png new file mode 100644 index 0000000..88efd93 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_coordinate.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_dark_shackles.png b/mengsan/assets/cards/mengsan_colorless_dark_shackles.png new file mode 100644 index 0000000..db28776 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_dark_shackles.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_discovery.png b/mengsan/assets/cards/mengsan_colorless_discovery.png new file mode 100644 index 0000000..6873613 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_discovery.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png b/mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png new file mode 100644 index 0000000..2264262 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_dramatic_entrance.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_entropy.png b/mengsan/assets/cards/mengsan_colorless_entropy.png new file mode 100644 index 0000000..30ab470 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_entropy.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_equilibrium.png b/mengsan/assets/cards/mengsan_colorless_equilibrium.png new file mode 100644 index 0000000..8515064 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_equilibrium.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_eternal_armor.png b/mengsan/assets/cards/mengsan_colorless_eternal_armor.png new file mode 100644 index 0000000..412f635 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_eternal_armor.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_fasten.png b/mengsan/assets/cards/mengsan_colorless_fasten.png new file mode 100644 index 0000000..65e6ee5 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_fasten.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_finesse.png b/mengsan/assets/cards/mengsan_colorless_finesse.png new file mode 100644 index 0000000..58b26e5 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_finesse.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_fisticuffs.png b/mengsan/assets/cards/mengsan_colorless_fisticuffs.png new file mode 100644 index 0000000..05210c6 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_fisticuffs.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_flash_of_steel.png b/mengsan/assets/cards/mengsan_colorless_flash_of_steel.png new file mode 100644 index 0000000..c506876 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_flash_of_steel.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_gang_up.png b/mengsan/assets/cards/mengsan_colorless_gang_up.png new file mode 100644 index 0000000..b998561 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_gang_up.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_gold_axe.png b/mengsan/assets/cards/mengsan_colorless_gold_axe.png new file mode 100644 index 0000000..11ab8a4 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_gold_axe.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_hand_of_greed.png b/mengsan/assets/cards/mengsan_colorless_hand_of_greed.png new file mode 100644 index 0000000..64e4d85 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_hand_of_greed.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_hidden_gem.png b/mengsan/assets/cards/mengsan_colorless_hidden_gem.png new file mode 100644 index 0000000..e1b5c42 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_hidden_gem.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_huddle_up.png b/mengsan/assets/cards/mengsan_colorless_huddle_up.png new file mode 100644 index 0000000..5b52bbd Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_huddle_up.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_impatience.png b/mengsan/assets/cards/mengsan_colorless_impatience.png new file mode 100644 index 0000000..25f5262 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_impatience.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_intercept.png b/mengsan/assets/cards/mengsan_colorless_intercept.png new file mode 100644 index 0000000..60b7be2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_intercept.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png b/mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png new file mode 100644 index 0000000..bff1ece Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_jack_of_all_trades.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_jackpot.png b/mengsan/assets/cards/mengsan_colorless_jackpot.png new file mode 100644 index 0000000..fa3f249 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_jackpot.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_knockdown.png b/mengsan/assets/cards/mengsan_colorless_knockdown.png new file mode 100644 index 0000000..8a387dc Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_knockdown.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_lift.png b/mengsan/assets/cards/mengsan_colorless_lift.png new file mode 100644 index 0000000..85512aa Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_lift.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_master_of_strategy.png b/mengsan/assets/cards/mengsan_colorless_master_of_strategy.png new file mode 100644 index 0000000..68906ef Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_master_of_strategy.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_mayhem.png b/mengsan/assets/cards/mengsan_colorless_mayhem.png new file mode 100644 index 0000000..0f4034f Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_mayhem.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_mimic.png b/mengsan/assets/cards/mengsan_colorless_mimic.png new file mode 100644 index 0000000..c87d354 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_mimic.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_mind_blast.png b/mengsan/assets/cards/mengsan_colorless_mind_blast.png new file mode 100644 index 0000000..4be7901 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_mind_blast.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_nostalgia.png b/mengsan/assets/cards/mengsan_colorless_nostalgia.png new file mode 100644 index 0000000..8f1cc6e Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_nostalgia.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_omnislice.png b/mengsan/assets/cards/mengsan_colorless_omnislice.png new file mode 100644 index 0000000..2ff8e8a Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_omnislice.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_panache.png b/mengsan/assets/cards/mengsan_colorless_panache.png new file mode 100644 index 0000000..7a778ac Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_panache.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_panic_button.png b/mengsan/assets/cards/mengsan_colorless_panic_button.png new file mode 100644 index 0000000..34dbbde Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_panic_button.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_prep_time.png b/mengsan/assets/cards/mengsan_colorless_prep_time.png new file mode 100644 index 0000000..6a2b12f Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_prep_time.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_production.png b/mengsan/assets/cards/mengsan_colorless_production.png new file mode 100644 index 0000000..4289ad9 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_production.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_prolong.png b/mengsan/assets/cards/mengsan_colorless_prolong.png new file mode 100644 index 0000000..4963f38 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_prolong.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_prowess.png b/mengsan/assets/cards/mengsan_colorless_prowess.png new file mode 100644 index 0000000..892e184 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_prowess.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_purity.png b/mengsan/assets/cards/mengsan_colorless_purity.png new file mode 100644 index 0000000..82736ab Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_purity.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_rally.png b/mengsan/assets/cards/mengsan_colorless_rally.png new file mode 100644 index 0000000..0a6b6da Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_rally.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_rend.png b/mengsan/assets/cards/mengsan_colorless_rend.png new file mode 100644 index 0000000..04abdb8 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_rend.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_restlessness.png b/mengsan/assets/cards/mengsan_colorless_restlessness.png new file mode 100644 index 0000000..370f974 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_restlessness.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_rolling_boulder.png b/mengsan/assets/cards/mengsan_colorless_rolling_boulder.png new file mode 100644 index 0000000..da7cbf4 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_rolling_boulder.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_salvo.png b/mengsan/assets/cards/mengsan_colorless_salvo.png new file mode 100644 index 0000000..417dce0 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_salvo.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_scrawl.png b/mengsan/assets/cards/mengsan_colorless_scrawl.png new file mode 100644 index 0000000..3fd9008 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_scrawl.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_secret_technique.png b/mengsan/assets/cards/mengsan_colorless_secret_technique.png new file mode 100644 index 0000000..482c424 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_secret_technique.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_secret_weapon.png b/mengsan/assets/cards/mengsan_colorless_secret_weapon.png new file mode 100644 index 0000000..2a1588e Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_secret_weapon.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_seeker_strike.png b/mengsan/assets/cards/mengsan_colorless_seeker_strike.png new file mode 100644 index 0000000..f0a959a Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_seeker_strike.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_shockwave.png b/mengsan/assets/cards/mengsan_colorless_shockwave.png new file mode 100644 index 0000000..89cc301 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_shockwave.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_splash.png b/mengsan/assets/cards/mengsan_colorless_splash.png new file mode 100644 index 0000000..62d24b2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_splash.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_stratagem.png b/mengsan/assets/cards/mengsan_colorless_stratagem.png new file mode 100644 index 0000000..a2c6574 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_stratagem.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_tag_team.png b/mengsan/assets/cards/mengsan_colorless_tag_team.png new file mode 100644 index 0000000..848a93c Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_tag_team.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_the_ball.png b/mengsan/assets/cards/mengsan_colorless_the_ball.png new file mode 100644 index 0000000..aed9b2d Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_the_ball.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_the_bomb.png b/mengsan/assets/cards/mengsan_colorless_the_bomb.png new file mode 100644 index 0000000..f8bfaca Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_the_bomb.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_the_gambit.png b/mengsan/assets/cards/mengsan_colorless_the_gambit.png new file mode 100644 index 0000000..b752d24 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_the_gambit.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_thinking_ahead.png b/mengsan/assets/cards/mengsan_colorless_thinking_ahead.png new file mode 100644 index 0000000..8c9cacc Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_thinking_ahead.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png b/mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png new file mode 100644 index 0000000..492ae9e Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_thrumming_hatchet.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_ultimate_defend.png b/mengsan/assets/cards/mengsan_colorless_ultimate_defend.png new file mode 100644 index 0000000..9ecc23c Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_ultimate_defend.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_ultimate_strike.png b/mengsan/assets/cards/mengsan_colorless_ultimate_strike.png new file mode 100644 index 0000000..1a7626b Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_ultimate_strike.png differ diff --git a/mengsan/assets/cards/mengsan_colorless_volley.png b/mengsan/assets/cards/mengsan_colorless_volley.png new file mode 100644 index 0000000..58474f7 Binary files /dev/null and b/mengsan/assets/cards/mengsan_colorless_volley.png differ diff --git a/mengsan/assets/cards/mengsan_curse_ascenders_bane.png b/mengsan/assets/cards/mengsan_curse_ascenders_bane.png new file mode 100644 index 0000000..5703af2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_ascenders_bane.png differ diff --git a/mengsan/assets/cards/mengsan_curse_bad_luck.png b/mengsan/assets/cards/mengsan_curse_bad_luck.png new file mode 100644 index 0000000..72d78dd Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_bad_luck.png differ diff --git a/mengsan/assets/cards/mengsan_curse_clumsy.png b/mengsan/assets/cards/mengsan_curse_clumsy.png new file mode 100644 index 0000000..4ef9dd4 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_clumsy.png differ diff --git a/mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png b/mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png new file mode 100644 index 0000000..dee04f8 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_curse_of_the_bell.png differ diff --git a/mengsan/assets/cards/mengsan_curse_debt.png b/mengsan/assets/cards/mengsan_curse_debt.png new file mode 100644 index 0000000..6408429 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_debt.png differ diff --git a/mengsan/assets/cards/mengsan_curse_decay.png b/mengsan/assets/cards/mengsan_curse_decay.png new file mode 100644 index 0000000..5da01bb Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_decay.png differ diff --git a/mengsan/assets/cards/mengsan_curse_doubt.png b/mengsan/assets/cards/mengsan_curse_doubt.png new file mode 100644 index 0000000..8400135 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_doubt.png differ diff --git a/mengsan/assets/cards/mengsan_curse_enthralled.png b/mengsan/assets/cards/mengsan_curse_enthralled.png new file mode 100644 index 0000000..e4b3f43 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_enthralled.png differ diff --git a/mengsan/assets/cards/mengsan_curse_folly.png b/mengsan/assets/cards/mengsan_curse_folly.png new file mode 100644 index 0000000..b9b7061 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_folly.png differ diff --git a/mengsan/assets/cards/mengsan_curse_greed.png b/mengsan/assets/cards/mengsan_curse_greed.png new file mode 100644 index 0000000..5f6da82 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_greed.png differ diff --git a/mengsan/assets/cards/mengsan_curse_guilty.png b/mengsan/assets/cards/mengsan_curse_guilty.png new file mode 100644 index 0000000..3cc6a71 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_guilty.png differ diff --git a/mengsan/assets/cards/mengsan_curse_injury.png b/mengsan/assets/cards/mengsan_curse_injury.png new file mode 100644 index 0000000..5153cd6 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_injury.png differ diff --git a/mengsan/assets/cards/mengsan_curse_normality.png b/mengsan/assets/cards/mengsan_curse_normality.png new file mode 100644 index 0000000..0eca634 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_normality.png differ diff --git a/mengsan/assets/cards/mengsan_curse_poor_sleep.png b/mengsan/assets/cards/mengsan_curse_poor_sleep.png new file mode 100644 index 0000000..cc8edbd Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_poor_sleep.png differ diff --git a/mengsan/assets/cards/mengsan_curse_regret.png b/mengsan/assets/cards/mengsan_curse_regret.png new file mode 100644 index 0000000..305ec48 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_regret.png differ diff --git a/mengsan/assets/cards/mengsan_curse_shame.png b/mengsan/assets/cards/mengsan_curse_shame.png new file mode 100644 index 0000000..becb1e0 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_shame.png differ diff --git a/mengsan/assets/cards/mengsan_curse_spore_mind.png b/mengsan/assets/cards/mengsan_curse_spore_mind.png new file mode 100644 index 0000000..4d96b06 Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_spore_mind.png differ diff --git a/mengsan/assets/cards/mengsan_curse_writhe.png b/mengsan/assets/cards/mengsan_curse_writhe.png new file mode 100644 index 0000000..2274cdb Binary files /dev/null and b/mengsan/assets/cards/mengsan_curse_writhe.png differ diff --git a/mengsan/assets/cards/mengsan_dazed_shuying.png b/mengsan/assets/cards/mengsan_dazed_shuying.png index 42b8646..26b73b4 100644 Binary files a/mengsan/assets/cards/mengsan_dazed_shuying.png and b/mengsan/assets/cards/mengsan_dazed_shuying.png differ diff --git a/mengsan/assets/cards/mengsan_event_abundance.png b/mengsan/assets/cards/mengsan_event_abundance.png new file mode 100644 index 0000000..7590745 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_abundance.png differ diff --git a/mengsan/assets/cards/mengsan_event_apotheosis.png b/mengsan/assets/cards/mengsan_event_apotheosis.png new file mode 100644 index 0000000..0a6bce0 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_apotheosis.png differ diff --git a/mengsan/assets/cards/mengsan_event_apparition.png b/mengsan/assets/cards/mengsan_event_apparition.png new file mode 100644 index 0000000..6263704 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_apparition.png differ diff --git a/mengsan/assets/cards/mengsan_event_brightest_flame.png b/mengsan/assets/cards/mengsan_event_brightest_flame.png new file mode 100644 index 0000000..e18e286 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_brightest_flame.png differ diff --git a/mengsan/assets/cards/mengsan_event_byrd_swoop.png b/mengsan/assets/cards/mengsan_event_byrd_swoop.png new file mode 100644 index 0000000..58e892a Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_byrd_swoop.png differ diff --git a/mengsan/assets/cards/mengsan_event_enlightenment.png b/mengsan/assets/cards/mengsan_event_enlightenment.png new file mode 100644 index 0000000..32707e3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_enlightenment.png differ diff --git a/mengsan/assets/cards/mengsan_event_exterminate.png b/mengsan/assets/cards/mengsan_event_exterminate.png new file mode 100644 index 0000000..9e09551 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_exterminate.png differ diff --git a/mengsan/assets/cards/mengsan_event_feeding_frenzy.png b/mengsan/assets/cards/mengsan_event_feeding_frenzy.png new file mode 100644 index 0000000..50e8878 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_feeding_frenzy.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_chaos.png b/mengsan/assets/cards/mengsan_event_mad_science_chaos.png new file mode 100644 index 0000000..b8e90e2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_chaos.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_choking.png b/mengsan/assets/cards/mengsan_event_mad_science_choking.png new file mode 100644 index 0000000..eee7b97 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_choking.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_curious.png b/mengsan/assets/cards/mengsan_event_mad_science_curious.png new file mode 100644 index 0000000..bdbfdf8 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_curious.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_energized.png b/mengsan/assets/cards/mengsan_event_mad_science_energized.png new file mode 100644 index 0000000..12f2982 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_energized.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_expertise.png b/mengsan/assets/cards/mengsan_event_mad_science_expertise.png new file mode 100644 index 0000000..22d5978 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_expertise.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_improvement.png b/mengsan/assets/cards/mengsan_event_mad_science_improvement.png new file mode 100644 index 0000000..e8ea930 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_improvement.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_sapping.png b/mengsan/assets/cards/mengsan_event_mad_science_sapping.png new file mode 100644 index 0000000..35f40af Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_sapping.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_violence.png b/mengsan/assets/cards/mengsan_event_mad_science_violence.png new file mode 100644 index 0000000..2df4028 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_violence.png differ diff --git a/mengsan/assets/cards/mengsan_event_mad_science_wisdom.png b/mengsan/assets/cards/mengsan_event_mad_science_wisdom.png new file mode 100644 index 0000000..d346807 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_mad_science_wisdom.png differ diff --git a/mengsan/assets/cards/mengsan_event_maul.png b/mengsan/assets/cards/mengsan_event_maul.png new file mode 100644 index 0000000..7a43883 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_maul.png differ diff --git a/mengsan/assets/cards/mengsan_event_metamorphosis.png b/mengsan/assets/cards/mengsan_event_metamorphosis.png new file mode 100644 index 0000000..0b28aae Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_metamorphosis.png differ diff --git a/mengsan/assets/cards/mengsan_event_neows_fury.png b/mengsan/assets/cards/mengsan_event_neows_fury.png new file mode 100644 index 0000000..68dbfda Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_neows_fury.png differ diff --git a/mengsan/assets/cards/mengsan_event_peck.png b/mengsan/assets/cards/mengsan_event_peck.png new file mode 100644 index 0000000..4942757 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_peck.png differ diff --git a/mengsan/assets/cards/mengsan_event_relax.png b/mengsan/assets/cards/mengsan_event_relax.png new file mode 100644 index 0000000..2e56757 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_relax.png differ diff --git a/mengsan/assets/cards/mengsan_event_squash.png b/mengsan/assets/cards/mengsan_event_squash.png new file mode 100644 index 0000000..826e857 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_squash.png differ diff --git a/mengsan/assets/cards/mengsan_event_toric_toughness.png b/mengsan/assets/cards/mengsan_event_toric_toughness.png new file mode 100644 index 0000000..a197837 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_toric_toughness.png differ diff --git a/mengsan/assets/cards/mengsan_event_whistle.png b/mengsan/assets/cards/mengsan_event_whistle.png new file mode 100644 index 0000000..03e8e89 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_whistle.png differ diff --git a/mengsan/assets/cards/mengsan_event_wish.png b/mengsan/assets/cards/mengsan_event_wish.png new file mode 100644 index 0000000..c596a46 Binary files /dev/null and b/mengsan/assets/cards/mengsan_event_wish.png differ diff --git a/mengsan/assets/cards/mengsan_fangxue.png b/mengsan/assets/cards/mengsan_fangxue.png index ed745e9..05d984e 100644 Binary files a/mengsan/assets/cards/mengsan_fangxue.png and b/mengsan/assets/cards/mengsan_fangxue.png differ diff --git a/mengsan/assets/cards/mengsan_fangyu.png b/mengsan/assets/cards/mengsan_fangyu.png index c8bd5f5..63e66d7 100644 Binary files a/mengsan/assets/cards/mengsan_fangyu.png and b/mengsan/assets/cards/mengsan_fangyu.png differ diff --git a/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png b/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png index 070098b..dafbc66 100644 Binary files a/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png and b/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png differ diff --git a/mengsan/assets/cards/mengsan_fennu.png b/mengsan/assets/cards/mengsan_fennu.png index 01341cd..93e3ec4 100644 Binary files a/mengsan/assets/cards/mengsan_fennu.png and b/mengsan/assets/cards/mengsan_fennu.png differ diff --git a/mengsan/assets/cards/mengsan_ic_ashen_strike.png b/mengsan/assets/cards/mengsan_ic_ashen_strike.png new file mode 100644 index 0000000..52031e8 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_ashen_strike.png differ diff --git a/mengsan/assets/cards/mengsan_ic_barricade.png b/mengsan/assets/cards/mengsan_ic_barricade.png new file mode 100644 index 0000000..526df88 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_barricade.png differ diff --git a/mengsan/assets/cards/mengsan_ic_battle_ready.png b/mengsan/assets/cards/mengsan_ic_battle_ready.png new file mode 100644 index 0000000..f905b2e Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_battle_ready.png differ diff --git a/mengsan/assets/cards/mengsan_ic_battle_trance.png b/mengsan/assets/cards/mengsan_ic_battle_trance.png new file mode 100644 index 0000000..ed2fb75 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_battle_trance.png differ diff --git a/mengsan/assets/cards/mengsan_ic_blaze.png b/mengsan/assets/cards/mengsan_ic_blaze.png new file mode 100644 index 0000000..68fb473 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_blaze.png differ diff --git a/mengsan/assets/cards/mengsan_ic_bludgeon.png b/mengsan/assets/cards/mengsan_ic_bludgeon.png new file mode 100644 index 0000000..d510760 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_bludgeon.png differ diff --git a/mengsan/assets/cards/mengsan_ic_bodyguard.png b/mengsan/assets/cards/mengsan_ic_bodyguard.png new file mode 100644 index 0000000..3fe4758 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_bodyguard.png differ diff --git a/mengsan/assets/cards/mengsan_ic_boulder.png b/mengsan/assets/cards/mengsan_ic_boulder.png new file mode 100644 index 0000000..99dab59 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_boulder.png differ diff --git a/mengsan/assets/cards/mengsan_ic_brand.png b/mengsan/assets/cards/mengsan_ic_brand.png new file mode 100644 index 0000000..6e4ce9f Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_brand.png differ diff --git a/mengsan/assets/cards/mengsan_ic_break.png b/mengsan/assets/cards/mengsan_ic_break.png new file mode 100644 index 0000000..d7ca0e9 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_break.png differ diff --git a/mengsan/assets/cards/mengsan_ic_bully.png b/mengsan/assets/cards/mengsan_ic_bully.png new file mode 100644 index 0000000..94f902b Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_bully.png differ diff --git a/mengsan/assets/cards/mengsan_ic_burning_pact.png b/mengsan/assets/cards/mengsan_ic_burning_pact.png new file mode 100644 index 0000000..60cbee3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_burning_pact.png differ diff --git a/mengsan/assets/cards/mengsan_ic_clash.png b/mengsan/assets/cards/mengsan_ic_clash.png new file mode 100644 index 0000000..5cd74e2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_clash.png differ diff --git a/mengsan/assets/cards/mengsan_ic_colossus.png b/mengsan/assets/cards/mengsan_ic_colossus.png new file mode 100644 index 0000000..556383b Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_colossus.png differ diff --git a/mengsan/assets/cards/mengsan_ic_conflagration.png b/mengsan/assets/cards/mengsan_ic_conflagration.png new file mode 100644 index 0000000..f161fc1 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_conflagration.png differ diff --git a/mengsan/assets/cards/mengsan_ic_corruption.png b/mengsan/assets/cards/mengsan_ic_corruption.png new file mode 100644 index 0000000..8117293 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_corruption.png differ diff --git a/mengsan/assets/cards/mengsan_ic_crimson_mantle.png b/mengsan/assets/cards/mengsan_ic_crimson_mantle.png new file mode 100644 index 0000000..322b925 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_crimson_mantle.png differ diff --git a/mengsan/assets/cards/mengsan_ic_cruelty.png b/mengsan/assets/cards/mengsan_ic_cruelty.png new file mode 100644 index 0000000..d7636b7 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_cruelty.png differ diff --git a/mengsan/assets/cards/mengsan_ic_dark_embrace.png b/mengsan/assets/cards/mengsan_ic_dark_embrace.png new file mode 100644 index 0000000..8614c5f Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_dark_embrace.png differ diff --git a/mengsan/assets/cards/mengsan_ic_demon_form.png b/mengsan/assets/cards/mengsan_ic_demon_form.png new file mode 100644 index 0000000..cb2ef63 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_demon_form.png differ diff --git a/mengsan/assets/cards/mengsan_ic_demonic_shield.png b/mengsan/assets/cards/mengsan_ic_demonic_shield.png new file mode 100644 index 0000000..56753dc Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_demonic_shield.png differ diff --git a/mengsan/assets/cards/mengsan_ic_dismantle.png b/mengsan/assets/cards/mengsan_ic_dismantle.png new file mode 100644 index 0000000..e99add3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_dismantle.png differ diff --git a/mengsan/assets/cards/mengsan_ic_dominate.png b/mengsan/assets/cards/mengsan_ic_dominate.png new file mode 100644 index 0000000..3fb6fa1 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_dominate.png differ diff --git a/mengsan/assets/cards/mengsan_ic_drum_of_battle.png b/mengsan/assets/cards/mengsan_ic_drum_of_battle.png new file mode 100644 index 0000000..925142d Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_drum_of_battle.png differ diff --git a/mengsan/assets/cards/mengsan_ic_dual_wield.png b/mengsan/assets/cards/mengsan_ic_dual_wield.png new file mode 100644 index 0000000..beec761 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_dual_wield.png differ diff --git a/mengsan/assets/cards/mengsan_ic_eager.png b/mengsan/assets/cards/mengsan_ic_eager.png new file mode 100644 index 0000000..4b051df Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_eager.png differ diff --git a/mengsan/assets/cards/mengsan_ic_entrench.png b/mengsan/assets/cards/mengsan_ic_entrench.png new file mode 100644 index 0000000..2876318 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_entrench.png differ diff --git a/mengsan/assets/cards/mengsan_ic_evil_eye.png b/mengsan/assets/cards/mengsan_ic_evil_eye.png new file mode 100644 index 0000000..eeb7e25 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_evil_eye.png differ diff --git a/mengsan/assets/cards/mengsan_ic_feed.png b/mengsan/assets/cards/mengsan_ic_feed.png new file mode 100644 index 0000000..ffe1b8c Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_feed.png differ diff --git a/mengsan/assets/cards/mengsan_ic_feel_no_pain.png b/mengsan/assets/cards/mengsan_ic_feel_no_pain.png new file mode 100644 index 0000000..141ea00 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_feel_no_pain.png differ diff --git a/mengsan/assets/cards/mengsan_ic_ferocity.png b/mengsan/assets/cards/mengsan_ic_ferocity.png new file mode 100644 index 0000000..6dee138 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_ferocity.png differ diff --git a/mengsan/assets/cards/mengsan_ic_fiend_fire.png b/mengsan/assets/cards/mengsan_ic_fiend_fire.png new file mode 100644 index 0000000..3f15c2f Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_fiend_fire.png differ diff --git a/mengsan/assets/cards/mengsan_ic_fight_me.png b/mengsan/assets/cards/mengsan_ic_fight_me.png new file mode 100644 index 0000000..3ee658d Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_fight_me.png differ diff --git a/mengsan/assets/cards/mengsan_ic_flame_barrier.png b/mengsan/assets/cards/mengsan_ic_flame_barrier.png new file mode 100644 index 0000000..9d41038 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_flame_barrier.png differ diff --git a/mengsan/assets/cards/mengsan_ic_forgotten_ritual.png b/mengsan/assets/cards/mengsan_ic_forgotten_ritual.png new file mode 100644 index 0000000..7b59b16 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_forgotten_ritual.png differ diff --git a/mengsan/assets/cards/mengsan_ic_grapple.png b/mengsan/assets/cards/mengsan_ic_grapple.png new file mode 100644 index 0000000..f82e19c Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_grapple.png differ diff --git a/mengsan/assets/cards/mengsan_ic_hellraiser.png b/mengsan/assets/cards/mengsan_ic_hellraiser.png new file mode 100644 index 0000000..3719b7e Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_hellraiser.png differ diff --git a/mengsan/assets/cards/mengsan_ic_hemokinesis.png b/mengsan/assets/cards/mengsan_ic_hemokinesis.png new file mode 100644 index 0000000..e9844e2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_hemokinesis.png differ diff --git a/mengsan/assets/cards/mengsan_ic_howl_from_beyond.png b/mengsan/assets/cards/mengsan_ic_howl_from_beyond.png new file mode 100644 index 0000000..8efb922 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_howl_from_beyond.png differ diff --git a/mengsan/assets/cards/mengsan_ic_impervious.png b/mengsan/assets/cards/mengsan_ic_impervious.png new file mode 100644 index 0000000..3528b38 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_impervious.png differ diff --git a/mengsan/assets/cards/mengsan_ic_infernal_blade.png b/mengsan/assets/cards/mengsan_ic_infernal_blade.png new file mode 100644 index 0000000..d13dcce Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_infernal_blade.png differ diff --git a/mengsan/assets/cards/mengsan_ic_inferno.png b/mengsan/assets/cards/mengsan_ic_inferno.png new file mode 100644 index 0000000..48aac43 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_inferno.png differ diff --git a/mengsan/assets/cards/mengsan_ic_inflame.png b/mengsan/assets/cards/mengsan_ic_inflame.png new file mode 100644 index 0000000..431648c Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_inflame.png differ diff --git a/mengsan/assets/cards/mengsan_ic_juggernaut.png b/mengsan/assets/cards/mengsan_ic_juggernaut.png new file mode 100644 index 0000000..e028f5a Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_juggernaut.png differ diff --git a/mengsan/assets/cards/mengsan_ic_juggle.png b/mengsan/assets/cards/mengsan_ic_juggle.png new file mode 100644 index 0000000..79172c2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_juggle.png differ diff --git a/mengsan/assets/cards/mengsan_ic_midnight.png b/mengsan/assets/cards/mengsan_ic_midnight.png new file mode 100644 index 0000000..940b278 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_midnight.png differ diff --git a/mengsan/assets/cards/mengsan_ic_not_yet.png b/mengsan/assets/cards/mengsan_ic_not_yet.png new file mode 100644 index 0000000..7b89091 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_not_yet.png differ diff --git a/mengsan/assets/cards/mengsan_ic_offering.png b/mengsan/assets/cards/mengsan_ic_offering.png new file mode 100644 index 0000000..34e05d3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_offering.png differ diff --git a/mengsan/assets/cards/mengsan_ic_pacts_end.png b/mengsan/assets/cards/mengsan_ic_pacts_end.png new file mode 100644 index 0000000..1c99f29 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_pacts_end.png differ diff --git a/mengsan/assets/cards/mengsan_ic_pillage.png b/mengsan/assets/cards/mengsan_ic_pillage.png new file mode 100644 index 0000000..dc0d289 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_pillage.png differ diff --git a/mengsan/assets/cards/mengsan_ic_primal_force.png b/mengsan/assets/cards/mengsan_ic_primal_force.png new file mode 100644 index 0000000..97e02ca Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_primal_force.png differ diff --git a/mengsan/assets/cards/mengsan_ic_pyre.png b/mengsan/assets/cards/mengsan_ic_pyre.png new file mode 100644 index 0000000..cb841b0 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_pyre.png differ diff --git a/mengsan/assets/cards/mengsan_ic_rage.png b/mengsan/assets/cards/mengsan_ic_rage.png new file mode 100644 index 0000000..78fa3fe Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_rage.png differ diff --git a/mengsan/assets/cards/mengsan_ic_rally.png b/mengsan/assets/cards/mengsan_ic_rally.png new file mode 100644 index 0000000..b180a71 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_rally.png differ diff --git a/mengsan/assets/cards/mengsan_ic_relentless_assault.png b/mengsan/assets/cards/mengsan_ic_relentless_assault.png new file mode 100644 index 0000000..be7f803 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_relentless_assault.png differ diff --git a/mengsan/assets/cards/mengsan_ic_rupture.png b/mengsan/assets/cards/mengsan_ic_rupture.png new file mode 100644 index 0000000..4867dd9 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_rupture.png differ diff --git a/mengsan/assets/cards/mengsan_ic_second_wind.png b/mengsan/assets/cards/mengsan_ic_second_wind.png new file mode 100644 index 0000000..49d9850 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_second_wind.png differ diff --git a/mengsan/assets/cards/mengsan_ic_stampede.png b/mengsan/assets/cards/mengsan_ic_stampede.png new file mode 100644 index 0000000..c9157a3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_stampede.png differ diff --git a/mengsan/assets/cards/mengsan_ic_stoke.png b/mengsan/assets/cards/mengsan_ic_stoke.png new file mode 100644 index 0000000..8fcc842 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_stoke.png differ diff --git a/mengsan/assets/cards/mengsan_ic_stomp.png b/mengsan/assets/cards/mengsan_ic_stomp.png new file mode 100644 index 0000000..a309f86 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_stomp.png differ diff --git a/mengsan/assets/cards/mengsan_ic_stone_armor.png b/mengsan/assets/cards/mengsan_ic_stone_armor.png new file mode 100644 index 0000000..55ab7d0 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_stone_armor.png differ diff --git a/mengsan/assets/cards/mengsan_ic_taunt.png b/mengsan/assets/cards/mengsan_ic_taunt.png new file mode 100644 index 0000000..2296063 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_taunt.png differ diff --git a/mengsan/assets/cards/mengsan_ic_tear_asunder.png b/mengsan/assets/cards/mengsan_ic_tear_asunder.png new file mode 100644 index 0000000..34bffc3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_tear_asunder.png differ diff --git a/mengsan/assets/cards/mengsan_ic_thrash.png b/mengsan/assets/cards/mengsan_ic_thrash.png new file mode 100644 index 0000000..5acb3b2 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_thrash.png differ diff --git a/mengsan/assets/cards/mengsan_ic_twin_strike.png b/mengsan/assets/cards/mengsan_ic_twin_strike.png new file mode 100644 index 0000000..d86c0a8 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_twin_strike.png differ diff --git a/mengsan/assets/cards/mengsan_ic_unleash.png b/mengsan/assets/cards/mengsan_ic_unleash.png new file mode 100644 index 0000000..9c28d59 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_unleash.png differ diff --git a/mengsan/assets/cards/mengsan_ic_unshakable.png b/mengsan/assets/cards/mengsan_ic_unshakable.png new file mode 100644 index 0000000..34ea044 Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_unshakable.png differ diff --git a/mengsan/assets/cards/mengsan_ic_uppercut.png b/mengsan/assets/cards/mengsan_ic_uppercut.png new file mode 100644 index 0000000..c36555f Binary files /dev/null and b/mengsan/assets/cards/mengsan_ic_uppercut.png differ diff --git a/mengsan/assets/cards/mengsan_infection_shuying.png b/mengsan/assets/cards/mengsan_infection_shuying.png index 58fba7c..b835221 100644 Binary files a/mengsan/assets/cards/mengsan_infection_shuying.png and b/mengsan/assets/cards/mengsan_infection_shuying.png differ diff --git a/mengsan/assets/cards/mengsan_jianbingdaji.png b/mengsan/assets/cards/mengsan_jianbingdaji.png index 87e1cc1..782ce9a 100644 Binary files a/mengsan/assets/cards/mengsan_jianbingdaji.png and b/mengsan/assets/cards/mengsan_jianbingdaji.png differ diff --git a/mengsan/assets/cards/mengsan_jianyi.png b/mengsan/assets/cards/mengsan_jianyi.png index e664ad6..f411152 100644 Binary files a/mengsan/assets/cards/mengsan_jianyi.png and b/mengsan/assets/cards/mengsan_jianyi.png differ diff --git a/mengsan/assets/cards/mengsan_pomie.png b/mengsan/assets/cards/mengsan_pomie.png index a6930d7..f5a4dbf 100644 Binary files a/mengsan/assets/cards/mengsan_pomie.png and b/mengsan/assets/cards/mengsan_pomie.png differ diff --git a/mengsan/assets/cards/mengsan_qiling.png b/mengsan/assets/cards/mengsan_qiling.png new file mode 100644 index 0000000..b166cc1 Binary files /dev/null and b/mengsan/assets/cards/mengsan_qiling.png differ diff --git a/mengsan/assets/cards/mengsan_quanshenzhuangji.png b/mengsan/assets/cards/mengsan_quanshenzhuangji.png index 856ce0c..7e31041 100644 Binary files a/mengsan/assets/cards/mengsan_quanshenzhuangji.png and b/mengsan/assets/cards/mengsan_quanshenzhuangji.png differ diff --git a/mengsan/assets/cards/mengsan_rongrongzhiquan.png b/mengsan/assets/cards/mengsan_rongrongzhiquan.png index af8a894..2f67dd1 100644 Binary files a/mengsan/assets/cards/mengsan_rongrongzhiquan.png and b/mengsan/assets/cards/mengsan_rongrongzhiquan.png differ diff --git a/mengsan/assets/cards/mengsan_shandianpili.png b/mengsan/assets/cards/mengsan_shandianpili.png index be4bac6..eea0ab7 100644 Binary files a/mengsan/assets/cards/mengsan_shandianpili.png and b/mengsan/assets/cards/mengsan_shandianpili.png differ diff --git a/mengsan/assets/cards/mengsan_shuangchongdaji.png b/mengsan/assets/cards/mengsan_shuangchongdaji.png index c9748ab..fb47d0b 100644 Binary files a/mengsan/assets/cards/mengsan_shuangchongdaji.png and b/mengsan/assets/cards/mengsan_shuangchongdaji.png differ diff --git a/mengsan/assets/cards/mengsan_slimed_shuying.png b/mengsan/assets/cards/mengsan_slimed_shuying.png index 375f8b1..b629ed5 100644 Binary files a/mengsan/assets/cards/mengsan_slimed_shuying.png and b/mengsan/assets/cards/mengsan_slimed_shuying.png differ diff --git a/mengsan/assets/cards/mengsan_songjianwushi.png b/mengsan/assets/cards/mengsan_songjianwushi.png index 698cf69..9beec24 100644 Binary files a/mengsan/assets/cards/mengsan_songjianwushi.png and b/mengsan/assets/cards/mengsan_songjianwushi.png differ diff --git a/mengsan/assets/cards/mengsan_status_beckon.png b/mengsan/assets/cards/mengsan_status_beckon.png new file mode 100644 index 0000000..4e7351f Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_beckon.png differ diff --git a/mengsan/assets/cards/mengsan_status_burn.png b/mengsan/assets/cards/mengsan_status_burn.png new file mode 100644 index 0000000..fa7b7ac Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_burn.png differ diff --git a/mengsan/assets/cards/mengsan_status_debris.png b/mengsan/assets/cards/mengsan_status_debris.png new file mode 100644 index 0000000..b7f7fed Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_debris.png differ diff --git a/mengsan/assets/cards/mengsan_status_deprecated_card.png b/mengsan/assets/cards/mengsan_status_deprecated_card.png new file mode 100644 index 0000000..3ad46bc Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_deprecated_card.png differ diff --git a/mengsan/assets/cards/mengsan_status_frantic_escape.png b/mengsan/assets/cards/mengsan_status_frantic_escape.png new file mode 100644 index 0000000..56eb496 Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_frantic_escape.png differ diff --git a/mengsan/assets/cards/mengsan_status_soot.png b/mengsan/assets/cards/mengsan_status_soot.png new file mode 100644 index 0000000..7d98d97 Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_soot.png differ diff --git a/mengsan/assets/cards/mengsan_status_toxic.png b/mengsan/assets/cards/mengsan_status_toxic.png new file mode 100644 index 0000000..98046aa Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_toxic.png differ diff --git a/mengsan/assets/cards/mengsan_status_void.png b/mengsan/assets/cards/mengsan_status_void.png new file mode 100644 index 0000000..6995447 Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_void.png differ diff --git a/mengsan/assets/cards/mengsan_status_wither.png b/mengsan/assets/cards/mengsan_status_wither.png new file mode 100644 index 0000000..c6f9abb Binary files /dev/null and b/mengsan/assets/cards/mengsan_status_wither.png differ diff --git a/mengsan/assets/cards/mengsan_tiezhanbo.png b/mengsan/assets/cards/mengsan_tiezhanbo.png index 6363e3b..813c146 100644 Binary files a/mengsan/assets/cards/mengsan_tiezhanbo.png and b/mengsan/assets/cards/mengsan_tiezhanbo.png differ diff --git a/mengsan/assets/cards/mengsan_touchui.png b/mengsan/assets/cards/mengsan_touchui.png index 1adb238..fa29d06 100644 Binary files a/mengsan/assets/cards/mengsan_touchui.png and b/mengsan/assets/cards/mengsan_touchui.png differ diff --git a/mengsan/assets/cards/mengsan_tupo.png b/mengsan/assets/cards/mengsan_tupo.png index 3f4b614..6291301 100644 Binary files a/mengsan/assets/cards/mengsan_tupo.png and b/mengsan/assets/cards/mengsan_tupo.png differ diff --git a/mengsan/assets/cards/mengsan_wanmeidaji.png b/mengsan/assets/cards/mengsan_wanmeidaji.png index 90de060..f68e026 100644 Binary files a/mengsan/assets/cards/mengsan_wanmeidaji.png and b/mengsan/assets/cards/mengsan_wanmeidaji.png differ diff --git a/mengsan/assets/cards/mengsan_wound_shuying.png b/mengsan/assets/cards/mengsan_wound_shuying.png new file mode 100644 index 0000000..0d31d2d Binary files /dev/null and b/mengsan/assets/cards/mengsan_wound_shuying.png differ diff --git a/mengsan/assets/cards/mengsan_wuzhuang.png b/mengsan/assets/cards/mengsan_wuzhuang.png new file mode 100644 index 0000000..776d0e3 Binary files /dev/null and b/mengsan/assets/cards/mengsan_wuzhuang.png differ diff --git a/mengsan/assets/cards/mengsan_xuanfengzhan.png b/mengsan/assets/cards/mengsan_xuanfengzhan.png new file mode 100644 index 0000000..818659c Binary files /dev/null and b/mengsan/assets/cards/mengsan_xuanfengzhan.png differ diff --git a/mengsan/assets/cards/mengsan_xueqiang.png b/mengsan/assets/cards/mengsan_xueqiang.png new file mode 100644 index 0000000..67627c1 Binary files /dev/null and b/mengsan/assets/cards/mengsan_xueqiang.png differ diff --git a/mengsan/assets/cards/mengsan_yuanhen.png b/mengsan/assets/cards/mengsan_yuanhen.png new file mode 100644 index 0000000..ee564af Binary files /dev/null and b/mengsan/assets/cards/mengsan_yuanhen.png differ diff --git a/mengsan/assets/cards/mengsan_yubeidaji.png b/mengsan/assets/cards/mengsan_yubeidaji.png index 9a02adf..b863044 100644 Binary files a/mengsan/assets/cards/mengsan_yubeidaji.png and b/mengsan/assets/cards/mengsan_yubeidaji.png differ diff --git a/mengsan/assets/cards/mengsan_yujin.png b/mengsan/assets/cards/mengsan_yujin.png index 5660696..25776ca 100644 Binary files a/mengsan/assets/cards/mengsan_yujin.png and b/mengsan/assets/cards/mengsan_yujin.png differ diff --git a/mengsan/assets/cards/mengsan_zhanli.png b/mengsan/assets/cards/mengsan_zhanli.png new file mode 100644 index 0000000..8875172 Binary files /dev/null and b/mengsan/assets/cards/mengsan_zhanli.png differ diff --git a/mengsan/assets/cards/mengsan_zhongsha.png b/mengsan/assets/cards/mengsan_zhongsha.png index d240746..2a6c16d 100644 Binary files a/mengsan/assets/cards/mengsan_zhongsha.png and b/mengsan/assets/cards/mengsan_zhongsha.png differ diff --git a/mengsan/assets/cards/wound-original.png b/mengsan/assets/cards/wound-original.png new file mode 100644 index 0000000..f9f5081 Binary files /dev/null and b/mengsan/assets/cards/wound-original.png differ diff --git a/mengsan/assets/portraits/ceremonial-beast-forest-portrait.png b/mengsan/assets/portraits/ceremonial-beast-forest-portrait.png new file mode 100644 index 0000000..b111e8f Binary files /dev/null and b/mengsan/assets/portraits/ceremonial-beast-forest-portrait.png differ diff --git a/mengsan/assets/portraits/ceremonial-beast-original.png b/mengsan/assets/portraits/ceremonial-beast-original.png new file mode 100644 index 0000000..caf75ed Binary files /dev/null and b/mengsan/assets/portraits/ceremonial-beast-original.png differ diff --git a/mengsan/assets/portraits/guanyu-portrait.jpg b/mengsan/assets/portraits/guanyu-portrait.jpg new file mode 100644 index 0000000..33444cf Binary files /dev/null and b/mengsan/assets/portraits/guanyu-portrait.jpg differ diff --git a/mengsan/assets/portraits/kin-follower-forest-portrait.png b/mengsan/assets/portraits/kin-follower-forest-portrait.png new file mode 100644 index 0000000..5bca41d Binary files /dev/null and b/mengsan/assets/portraits/kin-follower-forest-portrait.png differ diff --git a/mengsan/assets/portraits/kin-follower-original.webp b/mengsan/assets/portraits/kin-follower-original.webp new file mode 100644 index 0000000..344f537 Binary files /dev/null and b/mengsan/assets/portraits/kin-follower-original.webp differ diff --git a/mengsan/assets/portraits/kin-priest-forest-portrait.png b/mengsan/assets/portraits/kin-priest-forest-portrait.png new file mode 100644 index 0000000..8f6da99 Binary files /dev/null and b/mengsan/assets/portraits/kin-priest-forest-portrait.png differ diff --git a/mengsan/assets/portraits/kin-priest-original.png b/mengsan/assets/portraits/kin-priest-original.png new file mode 100644 index 0000000..2f69c3d Binary files /dev/null and b/mengsan/assets/portraits/kin-priest-original.png differ diff --git a/mengsan/assets/portraits/liubei-portrait.jpg b/mengsan/assets/portraits/liubei-portrait.jpg new file mode 100644 index 0000000..b550d4c Binary files /dev/null and b/mengsan/assets/portraits/liubei-portrait.jpg differ diff --git a/mengsan/assets/portraits/vantom-forest-portrait.png b/mengsan/assets/portraits/vantom-forest-portrait.png new file mode 100644 index 0000000..aa3dfc6 Binary files /dev/null and b/mengsan/assets/portraits/vantom-forest-portrait.png differ diff --git a/mengsan/assets/portraits/vantom-original.png b/mengsan/assets/portraits/vantom-original.png new file mode 100644 index 0000000..047b44d Binary files /dev/null and b/mengsan/assets/portraits/vantom-original.png differ diff --git a/mengsan/assets/portraits/zhangfei-portrait.jpg b/mengsan/assets/portraits/zhangfei-portrait.jpg new file mode 100644 index 0000000..cf0d9e0 Binary files /dev/null and b/mengsan/assets/portraits/zhangfei-portrait.jpg differ diff --git a/mengsan/battle/battle-flow.js b/mengsan/battle/battle-flow.js index bd72928..f789496 100644 --- a/mengsan/battle/battle-flow.js +++ b/mengsan/battle/battle-flow.js @@ -13,14 +13,13 @@ export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMa await presentVictory(copy(job.pending)); job.victoryPresented = true; } - // Also reopen a legacy chosen defeat: it never had a recovery choice. - if (job.pending.state !== "chosen" || - (job.pending.outcome === "defeat" && !job.pending.failureAction)) { + if (job.pending.state !== "chosen") { state = "choosing"; if (!job.hasChoice) { - job.choiceId = job.pending.outcome === "defeat" ? await chooseDefeat() + job.choiceId = job.pending.outcome === "defeat" ? await chooseDefeat(copy(job.pending.base)) : await chooseReward(copy(job.pending.choices), { boss: job.pending.boss, + elite: job.pending.elite, fixedRewards: copy(job.pending.fixedRewards || []), rewardPackage: copy(job.pending.rewardPackage || null), }); @@ -51,7 +50,7 @@ export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMa job.nextNode = await showMap(mapRun); job.committed.run = mapRun; } - else if (!["retry", "new"].includes(job.committed.route)) { + else if (job.committed.route !== "new") { await showEnding(job.committed.route, copy(job.committed.run)); } job.delivered = true; @@ -73,13 +72,12 @@ export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMa get error() { return error; }, async enterBattle({session, run, node, encounter, start}) { if (state !== "map") throw new Error("Map handoff not ready"); - if (run.battleFlow?.pending) throw new Error("Resume pending settlement before battle"); const saved = settlement.readRun(); if (!saved || saved.runId !== run.runId || saved.revision !== run.revision) throw new Error("Stale battle input"); state = "starting"; try { await quiesce(); - active = {session, run, retryRun:copy(run), node:copy(node), encounter:copy(encounter)};job = null;work = null; + active = {session, run, node:copy(node), encounter:copy(encounter)};job = null;work = null; // start MUST schedule only; it must not await the battle root's completion. state = "battle"; start(); } catch (cause) { state = "blocked"; error = cause; throw cause; } @@ -88,20 +86,12 @@ export function createBattleFlow({settlement, chooseReward, chooseDefeat, showMa if (state !== "battle" || session !== active.session) return false; if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown outcome"); if (!Number.isInteger(defeatedEnemies) || defeatedEnemies < 0) throw new Error("Invalid defeated enemy count"); - job = {input:{run:copy(active.run),retryRun:copy(active.retryRun),node:copy(active.node),encounter:{...copy(active.encounter),defeatedEnemies},hp,outcome}}; + job = {input:{run:copy(active.run),node:copy(active.node),encounter:{...copy(active.encounter),defeatedEnemies},hp,outcome}}; state = "stopping"; active.session.requestStop(); launch(); return true; // No game.pause/resume: stop loop, let current body unwind, then open rewards. }, - resumePending({session}) { - if (state !== "map") throw new Error("Cannot resume during another operation"); - const run = settlement.readRun(), item = run?.battleFlow?.pending; - if (!item) throw new Error("No durable pending settlement"); - active = {session}; - job = {input:{run}, pending:copy(item)}; - state = "stopping"; session.requestStop(); launch(); return work; - }, retry() { if (state !== "retryable") throw new Error("No failed settlement to retry"); launch(); return work; diff --git a/mengsan/battle/battle-settlement.js b/mengsan/battle/battle-settlement.js index 7f92d41..77ff6ce 100644 --- a/mengsan/battle/battle-settlement.js +++ b/mengsan/battle/battle-settlement.js @@ -1,6 +1,9 @@ -// Candidate settlement adapter. Dependencies are the current mode/state reward functions. +// 结算选择留在内存;玩家确认后只提交最终地图进度或结束征程。 import { applyBattleEndRelics } from "../relics/battle.js"; +import { applySharedBattleEndCards } from "../cards/shared-battle-end.js"; import { settleBondBattle } from "../bonds/state.js"; +import { recordNormalBattleVictory } from "../progression/encounter-progress.js"; +import { stripBattleProgress } from "../progression/map-checkpoint.js"; import { prepareRewardPackage, applyRewardPackage } from "../progression/reward-package.js"; @@ -10,185 +13,186 @@ export function createBattleSettlement({ store, config, getRandomRewardChoices, applyReward, completeNode, enterNextAct, createRun, now = Date.now, logRewardPackage = () => {}, }) { + let item = null, committed = null, commitPromise = null; const readRun = () => store.read()[config.saveKey]; function current(storage, runId) { const run = storage[config.saveKey]; - if (!run || run.runId !== runId) throw new Error("Stale run: settlement refused"); - run.battleFlow ||= { receipts: [], pending: null }; + if (!run || run.runId !== runId || run.status !== "running") throw new Error("Stale run: settlement refused"); return run; } - function pending(storage, runId, id) { - const run = current(storage, runId), item = run.battleFlow.pending; - if (!item || item.id !== id) throw new Error("Settlement checkpoint mismatch"); - return {run, item}; + function pending(runId, id) { + if (!item || item.id !== id || item.base.runId !== runId) throw new Error("Settlement checkpoint mismatch"); + return item; + } + function validateCheckpoint(run, snapshot, nodeId) { + if (run.actIndex !== snapshot.actIndex || run.revision !== snapshot.revision || + run.map?.nodes.find(node => node.id === nodeId)?.completed !== false) { + throw new Error("Stale node or revision"); + } } return Object.freeze({ readRun, - // Persist fixed candidates before opening choice UI. No mutation of the live run. - async prepare({run: snapshot, retryRun, node, encounter, hp, outcome = "victory"}) { + // 创建内存候选不调用存档写入;阵亡弹窗等待多久都不改上次地图存档。 + async prepare({run: snapshot, node, encounter, hp, outcome = "victory"}) { if (!["victory", "defeat"].includes(outcome)) throw new Error("Unknown battle outcome"); if (outcome === "victory" && (!Number.isFinite(hp) || (!encounter.rewardPackage && (!Number.isSafeInteger(encounter.gold) || encounter.gold < 0)))) throw new Error("Invalid battle result"); const id = idFor(snapshot, node); - return store.update(storage => { - const run = current(storage, snapshot.runId); - if (run.battleFlow.receipts.includes(id)) throw new Error("Battle already settled"); - if (run.battleFlow.pending) { - if (run.battleFlow.pending.id !== id) throw new Error("Another settlement is pending"); - return run.battleFlow.pending; - } - if (run.actIndex !== snapshot.actIndex || run.revision !== snapshot.revision || - run.map?.nodes.find(n => n.id === node.id)?.completed !== false) { - throw new Error("Stale node or revision"); - } - // Battle may have changed RNG/card state since the last checkpoint. - const base = copy(snapshot); delete base.battleFlow; - const bondGrowth = settleBondBattle(base, node); - if (outcome === "victory") { - base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp)); - base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1; - } - const relicRecovery = hp > 0 - ? applyBattleEndRelics(base, outcome) : 0; - const rewards = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, encounter.rewardPool).filter(c => c.name) : []; - // Persist guaranteed gold alongside loot; award it once in the chosen result. - const choices = outcome === "victory" && !encounter.rewardPackage - ? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : []; - const rewardPackage = outcome === "victory" && encounter.rewardPackage - ? prepareRewardPackage(base, encounter.rewardPackage, - getRandomRewardChoices) : null; - run.battleFlow.pending = {id, state:"awaitingChoice", nodeId:node.id, base, choices:copy(choices), fixedRewards:copy(encounter.fixedRewards || []), victoryDialogue:copy(encounter.victoryDialogue || []), boss:!!encounter.boss, outcome, relicRecovery, bondGrowth}; - run.battleFlow.pending.rewardPackage = copy(rewardPackage); - if (outcome === "victory" && !rewardPackage) { - run.battleFlow.pending.rewardMode = "one-or-skip"; - } - if (outcome === "defeat") { - const beforeBattle = retryRun || run; - if (beforeBattle.runId !== run.runId || - beforeBattle.actIndex !== run.actIndex || - beforeBattle.revision !== run.revision) throw new Error("Stale retry checkpoint"); - run.battleFlow.pending.retryRun = copy(beforeBattle); - run.battleFlow.pending.retryRun.battleFlow = { - receipts:copy(run.battleFlow.receipts), pending:null, - }; - run.battleFlow.pending.retryNode = copy(node); - run.battleFlow.pending.retryEncounter = copy(encounter); - } - return run.battleFlow.pending; - }); + const run = current(store.read(), snapshot.runId); + if (item) { + if (item.id !== id || item.outcome !== outcome) throw new Error("Another settlement is pending"); + return copy(item); + } + validateCheckpoint(run, snapshot, node.id); + // Battle may have changed RNG/card state since the last checkpoint. + const base = copy(snapshot); delete base.battleFlow; + applySharedBattleEndCards(base,outcome); + const bondGrowth = settleBondBattle(base, node); + if (outcome === "victory") { + recordNormalBattleVictory(base, node, encounter); + base.player.hp = Math.max(1, Math.min(hp, base.player.maxHp ?? hp)); + base.statistics.defeatedEnemies += encounter.defeatedEnemies ?? 1; + } + const relicRecovery = hp > 0 + ? applyBattleEndRelics(base, outcome) : 0; + const elite = !encounter.boss && encounter.tier === "elite"; + const rewardPool = encounter.boss ? "shared.pool.boss.premium" + : elite ? "shared.pool.battle.elite" : encounter.rewardPool; + const rewards = outcome === "victory" && !encounter.skipRandomReward ? getRandomRewardChoices(base, rewardPool, 3, { + allowSharedCards: !encounter.boss && encounter.allowSharedCardRewards === true, + }).filter(c => c.name) : []; + const fixedRewards = [...(encounter.fixedRewards || [])]; + // 同一内存候选固定这件遗物;选择或跳过卡牌均领取,不在 UI 抽取。 + if (outcome === "victory" && elite) { + const [relic] = getRandomRewardChoices(base, + encounter.guaranteedRelicPool || "shared.pool.elite.relics", 1); + if (relic) fixedRewards.push(relic.id); + } + // 金币随最终结果一次性提交。 + const choices = outcome === "victory" && !encounter.rewardPackage + ? [{id:"mengsan.reward.gold.shuying", kind:"gold", name:"金币", amount:encounter.gold}, ...rewards] : []; + const rewardPackage = outcome === "victory" && encounter.rewardPackage + ? prepareRewardPackage(base, encounter.rewardPackage, + getRandomRewardChoices) : null; + item = {id, state:"awaitingChoice", nodeId:node.id, base, choices:copy(choices), fixedRewards:copy(fixedRewards), victoryDialogue:copy(encounter.victoryDialogue || []), boss:!!encounter.boss, elite, outcome, relicRecovery, bondGrowth}; + item.rewardPackage = copy(rewardPackage); + if (outcome === "victory" && !rewardPackage) { + item.rewardMode = "one-or-skip"; + } + return copy(item); }, async choose(runId, id, choiceId) { - return store.update(storage => { - const {run, item} = pending(storage, runId, id); - if (item.outcome === "defeat") { - const action = typeof choiceId === "string" ? choiceId : choiceId?.action; - if (!["retry", "new"].includes(action)) throw new Error("Invalid defeat action"); - if (item.failureAction) { - if (item.failureAction !== action || - (action === "new" && item.choiceId.character !== choiceId.character)) { - throw new Error("Defeat choice already locked"); - } - return item; - } - if (action === "new") { - if (!config.characters.includes(choiceId.character) || typeof createRun !== "function") { - throw new Error("Invalid new journey character"); - } - // Generate once in the chosen checkpoint; a commit retry must not reroll it. - item.nextRun = createRun(choiceId.character); - if (!item.nextRun || item.nextRun.status !== "running" || item.nextRun.runId === runId) { - throw new Error("Invalid new journey"); - } - item.result = copy(item.base); - item.result.status = "failed"; - } else { - // Old pending saves have no retryRun: retain their last successful save. - item.result = copy(item.retryRun || run); - item.result.status = "running"; - item.result.battleFlow = {receipts:copy(run.battleFlow.receipts), pending:null}; - item.retryNode ||= copy(run.map?.nodes.find(node => node.id === item.nodeId)); - if (!item.retryNode || item.retryNode.completed) throw new Error("Missing retry battle node"); - } - item.state = "chosen"; item.choiceId = copy(choiceId); - item.failureAction = action; item.route = action; item.completedAt = now(); - return item; + const item = pending(runId, id); + const run = current(store.read(), runId); + validateCheckpoint(run, item.base, item.nodeId); + if (item.outcome === "defeat") { + const action = choiceId; + if (!["restart", "menu"].includes(action)) throw new Error("Invalid defeat action"); + if (item.failureAction) { + if (item.failureAction !== action) throw new Error("Defeat choice already locked"); + return copy(item); } - if (item.state === "chosen") { - const sameChoice = item.rewardPackage - ? item.choiceId?.cardId === choiceId?.cardId && - item.choiceId?.relicId === choiceId?.relicId - : item.choiceId === choiceId; - if (!sameChoice) throw new Error("Choice already locked"); - return item; - } - const result = copy(item.base); - let route = "defeat"; - if (item.outcome === "victory") { - if (item.rewardPackage) { - applyRewardPackage(result, item.rewardPackage, choiceId, - applyReward); + if (action === "restart") { + const character = item.base.player.character; + if (typeof createRun !== "function") { + throw new Error("Invalid new journey character"); } - if (!item.rewardPackage) { - const reward = item.choices.find(c => c.id === choiceId && c.kind !== "gold"); - if (choiceId !== null && !reward) { - throw new Error("Choose one pending reward or skip"); - } - // This result is persisted before commit; retries never credit gold twice. - for (const gold of item.choices.filter(c => c.kind === "gold")) { - if (!Number.isSafeInteger(gold.amount) || gold.amount < 0) throw new Error("Invalid pending gold reward"); - result.player.gold += gold.amount; - result.statistics.goldEarned += gold.amount; - } - if (reward) { - applyReward(result, reward.effectId || reward.id); - } - for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId); - } else { - for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId); + // 当前角色从头开始;保存失败后重试沿用同一新征程,不重新生成。 + const nextRun = createRun(character); + if (!nextRun || nextRun.player?.character !== character || + nextRun.status !== "running" || nextRun.runId === runId) { + throw new Error("Invalid new journey"); } - if (!completeNode(result, item.nodeId)) throw new Error("Node completion failed"); - route = item.boss && !enterNextAct(result) ? "victory" : "map"; - } else result.status = "failed"; - // Save the exact result (including random card ID/target) before final commit. - item.state = "chosen"; item.choiceId = choiceId; item.result = result; - item.route = route; item.completedAt = now(); - if (item.rewardPackage && item.outcome === "victory") { - logRewardPackage(item.rewardPackage, choiceId); + item.nextRun = stripBattleProgress(nextRun); } - return item; - }); + item.result = stripBattleProgress(copy(item.base)); + item.result.status = "failed"; + item.state = "chosen"; item.choiceId = copy(choiceId); + item.failureAction = action; item.route = action === "restart" ? "new" : "menu"; item.completedAt = now(); + return copy(item); + } + if (item.state === "chosen") { + const sameChoice = item.rewardPackage + ? item.choiceId?.cardId === choiceId?.cardId && + item.choiceId?.relicId === choiceId?.relicId + : item.choiceId === choiceId; + if (!sameChoice) throw new Error("Choice already locked"); + return copy(item); + } + const result = copy(item.base); + let route = "defeat"; + if (item.outcome === "victory") { + if (item.rewardPackage) { + applyRewardPackage(result, item.rewardPackage, choiceId, + applyReward); + } + if (!item.rewardPackage) { + const reward = item.choices.find(c => c.id === choiceId && c.kind !== "gold"); + if (choiceId !== null && !reward) { + throw new Error("Choose one pending reward or skip"); + } + // 结果只计算一次;保存失败后的重试不再次发放金币。 + for (const gold of item.choices.filter(c => c.kind === "gold")) { + if (!Number.isSafeInteger(gold.amount) || gold.amount < 0) throw new Error("Invalid pending gold reward"); + result.player.gold += gold.amount; + result.statistics.goldEarned += gold.amount; + } + if (reward) { + applyReward(result, reward.effectId || reward.id); + } + for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId); + } else { + for (const rewardId of item.fixedRewards || []) applyReward(result, rewardId); + } + if (!completeNode(result, item.nodeId)) throw new Error("Node completion failed"); + route = item.boss && !enterNextAct(result) ? "victory" : "map"; + } else result.status = "failed"; + // 在内存锁定最终结果(包括随机牌实例),只在 commit 写入地图进度。 + item.state = "chosen"; item.choiceId = copy(choiceId); item.result = stripBattleProgress(result); + item.route = route; item.completedAt = now(); + if (item.rewardPackage && item.outcome === "victory") { + logRewardPackage(item.rewardPackage, choiceId); + } + return copy(item); }, async commit(runId, id) { - return store.update(storage => { + const item = pending(runId, id); + if (item.state !== "chosen") throw new Error("Player choice required before saving"); + if (committed) return {...copy(committed), duplicate:true}; + if (commitPromise) return commitPromise; + commitPromise = store.update(storage => { const saved = storage[config.saveKey]; const prior = storage[config.profileKey]?.completedRuns?.find(r => r.runId === runId && r.settlementId === id); - if (!saved && prior) return {route:prior.outcome, run:null, duplicate:true}; - if (saved?.runId === runId && saved.battleFlow?.receipts?.includes(id)) return {route:"map", run:saved, duplicate:true}; - const {run, item} = pending(storage, runId, id); - if (item.state !== "chosen") throw new Error("Durable chosen result required"); - const result = copy(item.result); - if (item.route === "retry") { - // A failed attempt does not complete this node or add its victory receipt. - result.battleFlow = {receipts:copy(run.battleFlow.receipts), pending:null, - retry:{node:copy(item.retryNode), encounter:copy(item.retryEncounter || null)}}; - storage[config.saveKey] = result; - return {route:"retry", run:result, retryNode:copy(item.retryNode), - retryEncounter:copy(item.retryEncounter || null), duplicate:false}; + if (prior && item.route !== "map") { + if (item.route === "new" && saved?.runId !== item.nextRun.runId) { + throw new Error("Restart checkpoint changed"); + } + return {route:item.route, run:copy(item.route === "new" ? saved : item.result), duplicate:true}; } - result.battleFlow = {receipts:[...run.battleFlow.receipts, id], pending:null}; + if (item.route === "map" && saved?.runId === runId && + saved.map?.nodes.some(node => node.id === item.nodeId && node.completed)) { + return {route:"map", run:saved, duplicate:true}; + } + const run = current(storage, runId); + validateCheckpoint(run, item.base, item.nodeId); + const result = stripBattleProgress(copy(item.result)); if (item.route === "map") storage[config.saveKey] = result; else { const profile = storage[config.profileKey] ||= {wins:0, completedRuns:[]}; profile.completedRuns ||= []; if (!profile.completedRuns.some(r => r.runId === runId)) { if (item.route === "victory") profile.wins = (profile.wins || 0) + 1; - profile.completedRuns.push({runId, settlementId:id, outcome:item.route === "new" ? "defeat" : item.route, character:result.player.character, + profile.completedRuns.push({runId, settlementId:id, outcome:item.outcome, character:result.player.character, completedNodes:result.statistics.completedNodes, goldEarned:result.statistics.goldEarned, completedAt:item.completedAt}); } - if (item.route === "new") storage[config.saveKey] = copy(item.nextRun); + if (item.route === "new") storage[config.saveKey] = stripBattleProgress(copy(item.nextRun)); else delete storage[config.saveKey]; // Same transaction as profile; never a second clear write. } return {route:item.route, run:item.route === "new" ? copy(item.nextRun) : result, duplicate:false}; - }); + }).then(result => { + committed = copy(result); + return copy(committed); + }).finally(() => { commitPromise = null; }); + return commitPromise; }, }); } diff --git a/mengsan/battle/combat-rules.js b/mengsan/battle/combat-rules.js index 974a351..7f3a9e9 100644 --- a/mengsan/battle/combat-rules.js +++ b/mengsan/battle/combat-rules.js @@ -2,20 +2,31 @@ import { isAttackCard } from "../monsters/vine-tangled.js"; import { _status } from "../../../../noname.js"; import { cardUpgradeLevel, cardUpgradeRule } from "../cards/upgrades.js"; +import { cardPackCosts } from "../cards/packs/data.js"; +import { ironcladCardOwner, ironcladCost, ironcladCardIsFree } from "../cards/ironclad-hooks.js"; +import { sharedCardCost, sharedCardIsFree } from "../cards/shared-hooks.js"; export const PLAYER_ENERGY = 3; export const PLAYER_HAND_LIMIT = 3; export const SHA_DAMAGE = 6; export const TRICK_DAMAGE = 4; -const COSTS = Object.freeze({ sha: 1, mengsan_zhongsha: 2, mengsan_fangyu: 1, mengsan_fennu: 0, - mengsan_feijianhuixuanbiao: 1, mengsan_jianbingdaji: 1, mengsan_quanshenzhuangji: 1, mengsan_rongrongzhiquan: 1, mengsan_shandianpili: 1, mengsan_shuangchongdaji: 1, mengsan_tiezhanbo: 1, mengsan_touchui: 1, mengsan_tupo: 1, mengsan_yubeidaji: 1, mengsan_wanmeidaji: 2, mengsan_yujin: 2, mengsan_fangxue: 0, mengsan_jianyi: 1, mengsan_pomie: 1, mengsan_songjianwushi: 1, - tao: 1, jiu: 1, shan: 0, wuxie: 0, +const COSTS = Object.freeze({ sha: 1, mengsan_zhongsha: 2, tao: 1, jiu: 1, shan: 0, wuxie: 0, juedou: 2, nanman: 2, wanjian: 2, taoyuan: 2 }); +export const isXCostCard = card => cardPackCosts[card?.name] === "X"; +const xUses = new WeakMap(); +const xEnergy = player => player?.storage?.mengsanEnergy_shuying; +const validXEnergy = player => Number.isSafeInteger(xEnergy(player)) && xEnergy(player) >= 0; +const xTax = player => player?.storage?.mengsanTangled_shuying > 0 ? 1 : 0; export const cardCost = (card, player = null) => { + player ||= ironcladCardOwner(card); + if (ironcladCardIsFree(player,card) || sharedCardIsFree(player,card)) return 0; + // 数值接口用于支付;X 的展示标签由牌定义/图鉴和空文本手牌叠层提供。 + if (isXCostCard(card)) return validXEnergy(player) ? xEnergy(player) : 0; const rule = cardUpgradeLevel(card) ? cardUpgradeRule(card.name) : null; - const base = rule?.cost ?? COSTS[card?.name] ?? 1; - return base + (player?.storage?.mengsanTangled_shuying > 0 && isAttackCard(card) ? 1 : 0); + const base = rule?.cost ?? cardPackCosts[card?.name] ?? COSTS[card?.name] ?? 1; + return sharedCardCost(player,card,ironcladCost(player,card,base)) + + (player?.storage?.mengsanTangled_shuying > 0 && isAttackCard(card) ? 1 : 0); }; export const isActiveCardUse = (event, player) => { @@ -48,20 +59,54 @@ function isFreeCardUse(player, card, event) { const seen = new Set(); for (let current = event; current && !seen.has(current); current = current.parent) { seen.add(current); + // 精确授权的虚拟重放 useCard 没有实体 cards,但不能授权它的嵌套用牌。 + if (current === entry.choice) return true; if (current.name === "useCard" && (current.player !== player || current.cards?.length !== 1 || current.cards[0] !== entry.card)) break; - if (current === entry.choice) return true; } } return false; } -export const canPayCard = (player, card, event = _status.event) => - isFreeCardUse(player, card, event) || (player?.storage?.mengsanEnergy_shuying ?? Infinity) >= cardCost(card, player); +export const canPayCard = (player, card, event = _status.event) => { + if (isXCostCard(card)) return validXEnergy(player) && + (isFreeCardUse(player, card, event) || ironcladCardIsFree(player,card) || sharedCardIsFree(player,card) || xEnergy(player) >= xTax(player)); + return isFreeCardUse(player, card, event) || + (player?.storage?.mengsanEnergy_shuying ?? Infinity) >= cardCost(card, player); +}; -export const payCard = (player, card, event = _status.event) => { +export const payCard = (player, card, event = _status.event, battle = null) => { + if (isXCostCard(card)) { + if (!event || event.name !== "useCard" || event.player !== player || + event.card?.name !== card.name) return false; + const existing = xUses.get(event); + if (existing) return existing.player === player && existing.name === card.name; + if (!canPayCard(player, card, event)) return false; + const free = isFreeCardUse(player, card, event) || ironcladCardIsFree(player,card) || sharedCardIsFree(player,card); + const count = xEnergy(player) - (free ? 0 : xTax(player)); + xUses.set(event, { player, name: card.name, count, battle, + session: battle?.session, taken: false }); + if (!free) player.storage.mengsanEnergy_shuying = 0; + return true; + } if (isFreeCardUse(player, card, event)) return true; if (!canPayCard(player, card, event)) return false; player.storage.mengsanEnergy_shuying -= cardCost(card, player); return true; }; + +// X 仅属于最近一条真实 useCard;嵌套其他用牌不得借用外层次数。 +export function takeXCardUse(player, event, battle) { + const seen = new Set(); + for (let current = event; current && !seen.has(current); current = current.parent) { + seen.add(current); + if (current.name !== "useCard") continue; + const entry = xUses.get(current); + if (!entry || entry.taken || entry.player !== player || + current.player !== player || current.card?.name !== entry.name || + entry.battle !== battle || entry.session !== battle?.session) return null; + entry.taken = true; + return entry.count; + } + return null; +} diff --git a/mengsan/battle/intent-damage.js b/mengsan/battle/intent-damage.js index ca40852..273b5bf 100644 --- a/mengsan/battle/intent-damage.js +++ b/mengsan/battle/intent-damage.js @@ -1,5 +1,6 @@ import { slowAttackDamage } from "../monsters/effigy-slow.js"; import { shrinkAttackDamage } from "../monsters/shrinker-status.js"; +import { ironcladStrengthPenalty } from "../cards/ironclad-hooks.js"; export function attackHitCount(intent) { const hits = intent.hits ?? 1; @@ -15,7 +16,7 @@ export function outgoingAttackDamage(intent, source) { } const storedStrength = source?.storage?.mengsanStrength_shuying; const strength = Number.isInteger(storedStrength) ? storedStrength : 0; - let damage = Math.max(0, intent.damage + strength); + let damage = Math.max(0, intent.damage + strength - ironcladStrengthPenalty(source)); if (source?.storage?.mengsanWeak_shuying > 0) { damage = Math.floor(damage * 0.75); } diff --git a/mengsan/battle/intent-effects.js b/mengsan/battle/intent-effects.js index 55da42e..53d7a0c 100644 --- a/mengsan/battle/intent-effects.js +++ b/mengsan/battle/intent-effects.js @@ -11,10 +11,10 @@ export const getIntentHostiles = (game, source) => export function createIntentExecutor(game, getActiveBattle) { const active = source => source.isAlive() && Boolean(getActiveBattle()?.session.active); - const applyDebuff = (target, move) => { + const applyDebuff = (target, move, source) => { if (!move.debuff) return; if (move.debuff === "缠结") { applyTangled(getActiveBattle(), target, move.stacks); return; } - applyMengsanDebuff(target, move.debuff === "脆弱" ? "frail" : "vulnerable", move.stacks); + applyMengsanDebuff(target, move.debuff === "脆弱" ? "frail" : move.debuff === "虚弱" ? "weak" : "vulnerable", move.stacks, source); }; return async (source, move) => { for (const target of getIntentHostiles(game, source)) { @@ -34,7 +34,7 @@ export function createIntentExecutor(game, getActiveBattle) { await hit; } } - if (target.isAlive() && active(source)) applyDebuff(target, move); + if (target.isAlive() && active(source)) applyDebuff(target, move, source); } if (!active(source)) return; if (move.block) await source.changeHujia(move.block); diff --git a/mengsan/bonds/definitions.js b/mengsan/bonds/definitions.js index 6b74730..9aff3d0 100644 --- a/mengsan/bonds/definitions.js +++ b/mengsan/bonds/definitions.js @@ -10,42 +10,46 @@ export function getBondDeck(definition) { [...DEFAULT_BOND_DECK] : deck; } -const emptyGrowth = () => Array.from({ length: 10 }, () => ({ - maxHp: null, - hand: null, - draw: null, - handLimit: null, - energy: null, - skills: null, +// 当前关羽/张飞的可用基础配置;等级先只影响到场概率,数值曲线与新技能后续单独配置。 +const baseGrowth = () => Array.from({ length: 10 }, () => ({ + maxHp: 40, + hand: 4, + draw: 2, + handLimit: 5, + energy: 3, + skills: [], })); export const bondDefinitions = { guanyu: { - name: "关羽", character: "guanyu", portrait: null, - deck: null, intents: null, growth: emptyGrowth(), + name: "关羽", character: "mengsan_guanyu_shuying", + portrait: "extension/术樱包/mengsan/assets/portraits/guanyu-portrait.jpg", + deck: null, intents: null, growth: baseGrowth(), }, zhangfei: { - name: "张飞", character: "zhangfei", portrait: null, - deck: null, intents: null, growth: emptyGrowth(), + name: "张飞", character: "mengsan_zhangfei_shuying", + portrait: "extension/术樱包/mengsan/assets/portraits/zhangfei-portrait.jpg", + deck: null, intents: null, growth: baseGrowth(), }, }; export function getBondCombat(id, level) { - const definition = bondDefinitions[id]; + const definition = Object.hasOwn(bondDefinitions, id) ? bondDefinitions[id] : null; if (!definition || !Number.isInteger(level) || level < 1 || level > 10) { return null; } - const stats = definition.growth[level - 1]; + const unlocked = Array.isArray(definition.growth) ? Array.from(definition.growth.slice(0, level)) : []; + const stats = unlocked[level - 1]; if (!stats || !["maxHp", "hand", "draw", "handLimit", "energy"] - .every(key => Number.isInteger(stats[key]) && stats[key] >= 0) + .every(key => Number.isInteger(stats[key]) && stats[key] >= 0 && stats[key] <= 99) || stats.maxHp < 1 || stats.energy < 1 - || !Array.isArray(stats.skills) - || !stats.skills.every(skill => typeof skill === "string" && skill)) { + || unlocked.some(growth => !growth || !Array.isArray(growth.skills) || + !growth.skills.every(skill => typeof skill === "string" && skill))) { return null; } const skills = new Set(); - for (const growth of definition.growth.slice(0, level)) { - for (const skill of growth.skills || []) skills.add(skill); + for (const growth of unlocked) { + for (const skill of growth.skills) skills.add(skill); } return { id: `bond_${id}`, bondId: id, character: definition.character, diff --git a/mengsan/bonds/state.js b/mengsan/bonds/state.js index 3ec484d..16b32b9 100644 --- a/mengsan/bonds/state.js +++ b/mengsan/bonds/state.js @@ -1,9 +1,26 @@ import { bondDefinitions, getBondCombat } from "./definitions.js"; -export const bondProbability = level => (level - 1) / 9; +export const bondProbability = level => { + if (!Number.isInteger(level) || level < 1 || level > 10) throw new RangeError("羁绊等级必须为1至10"); + return (level - 1) / 9; +}; + +export function bondBattleStatus(battle) { + if (battle?.arrived) return "已到场"; + return ({opening: "角色专属首战不触发助战", down: "濒死,尚未救助", + unconfigured: "本等级战斗配置未补齐", chance: "本场未通过助战概率判定", + ready: "本场助战就绪"})[battle?.reason] || "本场未到场"; +} export function ensureBonds(run) { - return run.player.bonds ||= { selected: null, npcs: {} }; + const bonds = run.player.bonds ||= { selected: null, npcs: {} }; + // 只设置一次默认值;以后主动取消助战或改选张飞均不会被覆盖。 + if (run.player.character === "mengsan_liubei_shuying" && bonds.npcs.guanyu && + !bonds.innateSelectionInitialized) { + if (bonds.selected == null) bonds.selected = "guanyu"; + bonds.innateSelectionInitialized = true; + } + return bonds; } export function meetBond(run, id, level = 1) { @@ -20,12 +37,13 @@ export function initializeInnateBonds(run) { if (run.player.character === "mengsan_liubei_shuying") { meetBond(run, "guanyu", 8); meetBond(run, "zhangfei", 8); + ensureBonds(run); } } export function selectBond(run, id) { const bonds = ensureBonds(run); - if (id !== null && !Object.hasOwn(bonds.npcs, id)) { + if (id !== null && (!Object.hasOwn(bondDefinitions, id) || !Object.hasOwn(bonds.npcs, id))) { throw new Error("尚未结识该 NPC"); } bonds.selected = id; @@ -48,17 +66,22 @@ const isCharacterOpeningBattle = (run, node) => run.actIndex === 0 && node?.floor === 0; export function prepareBondBattle(run, random, node = null) { - if (isCharacterOpeningBattle(run, node)) { - run.bondBattle = null; - return null; - } const bonds = ensureBonds(run); const id = bonds.selected; - const npc = bonds.npcs[id]; - run.bondBattle = npc ? { id, arrived: false } : null; - if (!npc || npc.down) return null; + const npc = Object.hasOwn(bondDefinitions, id) && Object.hasOwn(bonds.npcs, id) ? bonds.npcs[id] : null; + run.bondBattle = npc ? { id, arrived: false, reason: "ready" } : null; + if (isCharacterOpeningBattle(run, node)) { + if (run.bondBattle) run.bondBattle.reason = "opening"; + return null; + } + if (!npc) return null; + if (npc.down) { run.bondBattle.reason = "down"; return null; } const combat = getBondCombat(id, npc.level); - if (!combat || random() >= bondProbability(npc.level)) return null; + if (!combat) { run.bondBattle.reason = "unconfigured"; return null; } + if (typeof random !== "function") throw new TypeError("羁绊助战缺少随机数接口"); + const roll = random(); + if (!Number.isFinite(roll) || roll < 0 || roll >= 1) throw new RangeError("羁绊助战随机数必须在[0,1)内"); + if (roll >= bondProbability(npc.level)) { run.bondBattle.reason = "chance"; return null; } return combat; } @@ -71,6 +94,10 @@ export function settleBondBattle(run, node = null) { if (!battle) return null; const npc = ensureBonds(run).npcs[battle.id]; if (!npc) throw new Error("助战羁绊记录丢失"); + if (!Number.isInteger(npc.level) || npc.level < 1 || npc.level > 10 || + !Number.isInteger(npc.progress) || npc.progress < 0 || npc.progress >= 100) { + throw new Error("羁绊等级或成长进度无效,拒绝结算"); + } const gain = battle.arrived ? 50 : 25; if (npc.level < 10) { npc.progress += gain; diff --git a/mengsan/cards/CLASSIFICATION.md b/mengsan/cards/CLASSIFICATION.md new file mode 100644 index 0000000..24abb16 --- /dev/null +++ b/mengsan/cards/CLASSIFICATION.md @@ -0,0 +1,22 @@ +# 卡牌类型与稀有度接入 + +`card-definitions.js` 是永久牌的唯一分类注册表。`cardType` 表示攻击、技能、能力等玩法类型,`rarity` 表示原版稀有度;无名引擎的 `type` 继续用于基本牌、锦囊、装备及状态的响应与结算。 + +| 字段 | 可用值 | +| --- | --- | +| cardType | attack / skill / power / status / curse / quest / equipment | +| rarity | basic / common / uncommon / rare / event / ancient / token / status / curse / quest | + +未确认的类型允许 `null`。稀有度未填写或为 `null` 时统一默认 `common`(普通),未接入注册表的 noname 原生牌在卡面读取时也默认普通。现有三国通用牌已按实际用途适配类型,除杀、重杀对应打击、痛击保留初始级别外,其余未填写级别的通用牌默认普通;明确填写的等级继续优先。 + +刘备当前94张原版牌与1张巨石衍生牌,分类存放于 `packs/liubei/traits.js`。来源记录在项目根目录 `sts2-card-capture-20261003/generation/卡牌分类快照.json`。本地拆包优先用于匹配,已发现的版本差异以Wiki正式版为准:放血普通,挑衅罕见,主宰罕见,残酷稀有;擒拿以Wiki卡牌导航补全为罕见攻击牌。巨石按 `GiantRock` 标记为衍生攻击牌;没有采用名称相似的 `RollingBoulder`。 + +后续曹操、孙权等牌组在各自 metadata 的条目中填写这两个字段;无色牌在 `packs/colorless/data.js` 填写相同字段,纯数据汇总入口自动传递。登记值会校验,并同步给模式卡定义、创建的牌组数据、个人牌堆实体的存储数据。旧存档进入个人牌堆时重新按牌名补全,外部传入的分类不能覆盖注册表。 + +攻击判定、攻击牌生成、腐化和双持读取实际类型;对未配置的旧牌保留原有兼容判断。图鉴与持有牌详情显示同一分类;图鉴支持类型与稀有度联合筛选。 + +卡面稀有度入口 `rarity-data.js` 自动从注册表派生,继续使用 `getCardRarity/applyCardRarity`。初始和衍生沿用中性配色,并保留各自级别名称;状态、诅咒、任务目前只输出级别,保留已有卡面视觉。 + +本次保留现有奖励随机概率、事件牌获取途径、添柴的全牌组随机规则,不依据稀有度新增排除规则或权重。巨石继续仅在战斗中生成。 + +核对边界:只做源码语法、字段覆盖和文件引用核对,不构成实机验收。 diff --git a/mengsan/cards/RARITY.md b/mengsan/cards/RARITY.md new file mode 100644 index 0000000..dd39431 --- /dev/null +++ b/mengsan/cards/RARITY.md @@ -0,0 +1,17 @@ +# 梦三卡牌稀有度 + +统一来源:各牌组 metadata 的 `rarity`,汇总到 `card-definitions.js`。`cards/rarity-data.js` 的 `cardRarities` 自动派生,卡面继续按真实牌名读取。 + +| 值 | 名称 | 参考视觉 | +| --- | --- | --- | +| common | 普通 | 银灰名称栏与框线 | +| uncommon | 罕见 | 青蓝名称栏与框线 | +| rare | 稀有 | 金黄名称栏与框线 | +| event | 事件 | 绿色名称栏与框线 | +| ancient | 先古之民 | 紫黑卡底与银色框线 | + +遗漏或 `null` 默认普通;未接入注册表的 noname 原生牌也统一按普通显示。已明确填写的等级继续优先。刘备94张原版牌与1张巨石衍生牌现已填写;后续职业填写各自 metadata,无色牌填写 `packs/colorless/data.js`。 +`basic`(初始)与 `token`(衍生)沿用普通的中性配色,但级别名称独立。`status/curse/quest` 提供状态、诅咒、任务名称,暂时保留已有卡面视觉。完整字段说明见 `CLASSIFICATION.md`。 +配置统一用于手牌、持有牌、图鉴、战斗牌堆和卡牌奖励。详情和无障碍信息提供级别名称,不只依赖颜色。 +事件与先古之民是来源类别,不是概率上的高低排序。暂不修改奖励掉率、费用、效果、升级规则或立绘。 +手牌装饰使用空文本节点、data 属性和 CSS,保留感染动态边框、原生 card::after、选择提示与点击区。 diff --git a/mengsan/cards/battle-energy.js b/mengsan/cards/battle-energy.js index fbef18a..a1e3bc3 100644 --- a/mengsan/cards/battle-energy.js +++ b/mengsan/cards/battle-energy.js @@ -1,4 +1,5 @@ // 只向同一场仍在进行的战斗中的存活角色增加当前费用,不改变上限。 +import { ironcladBlocksEnergy } from "./ironclad-hooks.js"; export function createBattleEnergy(game, getActiveBattle) { const valid = (player, battle) => Boolean(battle && battle === getActiveBattle() && battle.session?.active && @@ -21,6 +22,7 @@ export function createBattleEnergy(game, getActiveBattle) { }, grant(player, amount, battle) { if (!valid(player, battle)) return false; + if (ironcladBlocksEnergy(player)) return false; validateAmount(player, amount); player.storage.mengsanEnergy_shuying += amount; battle.energyUI.get(player)?.(); diff --git a/mengsan/cards/card-data.js b/mengsan/cards/card-data.js index 039fdd8..948c8c6 100644 --- a/mengsan/cards/card-data.js +++ b/mengsan/cards/card-data.js @@ -1,5 +1,6 @@ import { cardDefinitions, canAcquireCard } from "./card-definitions.js"; import { cardUpgradeRule } from "./upgrades.js"; +import { WITHER_CARD, MAX_WITHER_LEVEL } from "./wither-card.js"; const suits = ["spade", "heart", "club", "diamond"]; @@ -16,12 +17,18 @@ export function createCardData(spec, { character, enemy = false, registry = card (spec.nature != null && typeof spec.nature !== "string") || !Number.isInteger(spec.upgrade ?? 0) || (spec.upgrade ?? 0) < 0 || (spec.upgrade ?? 0) > (cardUpgradeRule(spec.name)?.maxLevel || 0) || - !Array.isArray(spec.affixes ?? []) || !(spec.affixes ?? []).every(value => typeof value === "string")) { + !Array.isArray(spec.affixes ?? []) || !(spec.affixes ?? []).every(value => typeof value === "string") || + (spec.guiltyBattles!=null && (!Number.isSafeInteger(spec.guiltyBattles) || spec.guiltyBattles<0 || spec.guiltyBattles>4)) || + (spec.witherLevel!=null && (!Number.isSafeInteger(spec.witherLevel) || spec.witherLevel<0 || spec.witherLevel>MAX_WITHER_LEVEL))) { throw new Error(`梦三牌数据无效:${spec.name}`); } return { id: spec.id, suit: spec.suit, number: spec.number, name: spec.name, + // 分类由注册表决定,旧存档无需预先迁移,外部 spec 不可覆盖。 + cardType: registry[spec.name].cardType ?? null, rarity: registry[spec.name].rarity ?? "common", nature: spec.nature ?? null, affixes: (spec.affixes ?? []).slice(), upgrade: spec.upgrade ?? 0, + ...(spec.name==="mengsan_curse_guilty"?{guiltyBattles:spec.guiltyBattles??0}:{}), + ...(spec.name===WITHER_CARD?{witherLevel:spec.witherLevel??0}:{}), }; } diff --git a/mengsan/cards/card-definitions.js b/mengsan/cards/card-definitions.js index c8e12e4..2a7a350 100644 --- a/mengsan/cards/card-definitions.js +++ b/mengsan/cards/card-definitions.js @@ -1,35 +1,21 @@ // 梦三永久牌组的可获得牌注册表。这里登记牌名与归属,不复制引擎中的效果定义。 // 新增武将专属牌时填写 owner 为该武将 ID;通用牌填写 owner: null。 +import { cardPackDefinitions } from "./packs/data.js"; +import { cardTypes, cardRarityLabels } from "./classification-types.js"; +import { liubeiCardTraits } from "./packs/liubei/traits.js"; const definitions = [ - { name: "mengsan_songjianwushi", category: "utility", owner: null }, - { name: "mengsan_pomie", category: "utility", owner: null }, - { name: "mengsan_jianyi", category: "utility", owner: null }, - { name: "mengsan_fangxue", category: "utility", owner: null }, - { name: "mengsan_yujin", category: "damage", owner: null }, - { name: "mengsan_wanmeidaji", category: "damage", owner: null }, - { name: "mengsan_yubeidaji", category: "damage", owner: null }, - { name: "mengsan_tupo", category: "damage", owner: null }, - { name: "mengsan_touchui", category: "damage", owner: null }, - { name: "mengsan_tiezhanbo", category: "damage", owner: null }, - { name: "mengsan_shuangchongdaji", category: "damage", owner: null }, - { name: "mengsan_shandianpili", category: "damage", owner: null }, - { name: "sha", category: "damage", owner: null }, - { name: "mengsan_zhongsha", category: "damage", owner: null }, - { name: "mengsan_fennu", category: "damage", owner: null }, - { name: "mengsan_rongrongzhiquan", category: "damage", owner: null }, - { name: "mengsan_quanshenzhuangji", category: "damage", owner: null }, - { name: "mengsan_jianbingdaji", category: "damage", owner: null }, - { name: "mengsan_feijianhuixuanbiao", category: "damage", owner: null }, - { name: "mengsan_fangyu", category: "utility", owner: null }, - { name: "shan", category: "utility", owner: null }, - { name: "tao", category: "recovery", owner: null }, - { name: "jiu", category: "recovery", owner: null }, - { name: "guohe", category: "utility", owner: null }, - { name: "wuzhong", category: "utility", owner: null }, - { name: "juedou", category: "damage", owner: null }, - { name: "bagua", category: "utility", owner: null }, + ...cardPackDefinitions, + { name: "sha", category: "damage", owner: null, ...liubeiCardTraits.sha }, + { name: "mengsan_zhongsha", category: "damage", owner: null, ...liubeiCardTraits.mengsan_zhongsha }, + { name: "shan", category: "utility", owner: null, cardType: "skill" }, + { name: "tao", category: "recovery", owner: null, cardType: "skill" }, + { name: "jiu", category: "recovery", owner: null, cardType: "skill" }, + { name: "guohe", category: "utility", owner: null, cardType: "skill" }, + { name: "wuzhong", category: "utility", owner: null, cardType: "skill" }, + { name: "juedou", category: "damage", owner: null, cardType: "attack" }, + { name: "bagua", category: "utility", owner: null, cardType: "equipment" }, // 当前仅刘备专属首关的固定奖励会永久获得【的卢】。 - { name: "dilu", category: "utility", owner: "mengsan_liubei_shuying" }, + { name: "dilu", category: "utility", owner: "mengsan_liubei_shuying", cardType: "equipment" }, ]; export function createCardRegistry(entries) { @@ -38,10 +24,16 @@ export function createCardRegistry(entries) { if (!entry || typeof entry.name !== "string" || !/^[a-z][a-z0-9_]*$/.test(entry.name) || !["damage", "recovery", "utility"].includes(entry.category) || !(entry.owner === null || typeof entry.owner === "string" && entry.owner.length > 0) || + !(entry.deck == null || typeof entry.deck === "string" && /^[a-z][a-z0-9_]*$/.test(entry.deck)) || + !(entry.generatedOnly == null || typeof entry.generatedOnly === "boolean") || + !(entry.cardType == null || typeof entry.cardType === "string" && Object.hasOwn(cardTypes, entry.cardType)) || + !(entry.rarity == null || typeof entry.rarity === "string" && Object.hasOwn(cardRarityLabels, entry.rarity)) || Object.hasOwn(registry, entry.name)) { throw new Error(`梦三牌定义无效或重复:${entry?.name}`); } - registry[entry.name] = Object.freeze({ name: entry.name, category: entry.category, owner: entry.owner }); + registry[entry.name] = Object.freeze({ name: entry.name, category: entry.category, owner: entry.owner, + cardType: entry.cardType ?? null, rarity: entry.rarity ?? "common", + deck: entry.deck || "legacy", generatedOnly: entry.generatedOnly === true }); } return Object.freeze(registry); } @@ -50,5 +42,6 @@ export const cardDefinitions = createCardRegistry(definitions); export function canAcquireCard(character, name, registry = cardDefinitions) { const definition = Object.hasOwn(registry, name) ? registry[name] : null; - return !!definition && (definition.owner === null || definition.owner === character); + return !!definition && !definition.generatedOnly && + (definition.owner === null || definition.owner === character); } diff --git a/mengsan/cards/card-pools.js b/mengsan/cards/card-pools.js new file mode 100644 index 0000000..42fc29c --- /dev/null +++ b/mengsan/cards/card-pools.js @@ -0,0 +1,9 @@ +// 牌组归属只管理正常获得和随机生成,不限制怪物/剧情显式使用实体牌。 +import { liubeiBaseCardNames } from "./packs/liubei/names.js"; +export { LIUBEI_CARD_OWNER, legacyIroncladNames } from "./packs/liubei/names.js"; +export function generationPool(registry, decks, {attacksOnly=false}={}) { + const chosen = new Set(decks); + return Object.values(registry).filter(c => (chosen.has(c.deck) || + chosen.has("liubei") && liubeiBaseCardNames.includes(c.name)) && !c.generatedOnly && + (!attacksOnly || (c.cardType ? c.cardType === "attack" : c.category === "damage"))).map(c=>c.name); +} diff --git a/mengsan/cards/classification-types.js b/mengsan/cards/classification-types.js new file mode 100644 index 0000000..d4928df --- /dev/null +++ b/mengsan/cards/classification-types.js @@ -0,0 +1,9 @@ +// 与无名引擎的 basic/trick/equip 分类分开,新增职业沿用这些稳定字段。 +export const cardTypes = Object.freeze({ + attack: "攻击", skill: "技能", power: "能力", status: "状态", curse: "诅咒", quest: "任务", + equipment: "装备", +}); +export const cardRarityLabels = Object.freeze({ + basic: "初始", common: "普通", uncommon: "罕见", rare: "稀有", event: "事件", + ancient: "先古之民", token: "衍生", status: "状态", curse: "诅咒", quest: "任务", +}); diff --git a/mengsan/cards/description.js b/mengsan/cards/description.js new file mode 100644 index 0000000..dcc0a71 --- /dev/null +++ b/mengsan/cards/description.js @@ -0,0 +1,8 @@ +// 普通使用费用由牌面单独显示;效果中的获能、减费与 X 消耗规则仍须保留。 +export function cleanCardDescription(value) { + if (typeof value !== "string") return value; + return value.trim() + .replace(/^梦三\s*[::]\s*/, "") + .replace(/^(?:主动使用消耗\s*\d+\s*(?:点)?费用|响应不消耗费用|\d+\s*(?:点)?费(?:用)?)[。;::,,]\s*/, "") + .replace(/^(?:主动使用消耗X费用|X费)[。;]\s*/, "消耗当前剩余费用,消耗量为X。"); +} diff --git a/mengsan/cards/hand-exhaust.js b/mengsan/cards/hand-exhaust.js index e406f75..d32ca9c 100644 --- a/mengsan/cards/hand-exhaust.js +++ b/mengsan/cards/hand-exhaust.js @@ -20,7 +20,7 @@ export function createHandExhauster(game, getActiveBattle, get, randomHandExhaus if (typeof randomHandExhaust?.exhaust !== "function") throw new Error("坚毅缺少随机手牌消耗接口"); return { battle, pile: getPile(player, battle) }; }, - async exhaust(player, event, token, choose) { + async exhaust(player, event, token, choose, prompt = "坚毅:选择消耗一张你的手牌") { const battle = token?.battle, pile = token?.pile; if (!valid(player, battle) || getPile(player, battle) !== pile) return null; if (typeof pile?.exhaustFromHand !== "function") throw new Error("坚毅缺少个人消耗牌堆接口"); @@ -30,7 +30,7 @@ export function createHandExhauster(game, getActiveBattle, get, randomHandExhaus let chosen = cards[0]; if (cards.length > 1) { const result = await player.chooseButton({ - createDialog: ["坚毅:选择消耗一张你的手牌", cards], + createDialog: [prompt, cards], forced: true, selectButton: 1, ai(button) { return -get.value(button.link, player); }, }).forResult(); diff --git a/mengsan/cards/hand-upgrade.js b/mengsan/cards/hand-upgrade.js new file mode 100644 index 0000000..06116a0 --- /dev/null +++ b/mengsan/cards/hand-upgrade.js @@ -0,0 +1,87 @@ +import { canUpgradeCard, cardUpgradeLevel } from "./upgrades.js"; + +// 武装只强化当前战斗的真实手牌;不写征程永久牌组。 +export function createHandUpgrader(game, getActiveBattle, get) { + const states = new WeakMap(); + const valid = (player, battle) => Boolean(battle && getActiveBattle() === battle && + battle.session?.active && battle.players?.has(player) && player?.isAlive?.()); + const getPile = (player, battle) => player === game.me + ? battle.personalPiles : battle.monsterPiles?.get(player); + function dataOf(card) { + return card.storage?.mengsanCard_shuying || { + name: card.name, suit: card.suit, number: card.number, nature: card.nature || null, + upgrade: cardUpgradeLevel(card), affixes: Array.isArray(card.affixes) ? card.affixes : [], + }; + } + function eligible(card) { + const data = dataOf(card); + return !card.storage?.mengsanExhausted_shuying && data.name === card.name && + canUpgradeCard(data); + } + const handCards = (player, event) => { + const playing = new Set(event?.cards || []); + return player.getCards("h").filter(card => !playing.has(card) && eligible(card)); + }; + function remember(battle, card, after) { + let cards = states.get(battle); + if (!cards) { + cards = new Map(); + states.set(battle, cards); + battle.session.ownResource(cards, () => { + // Do not overwrite a later replacement by another mechanic. + for (const [node, state] of cards) { + if (node.storage?.mengsanCard_shuying !== state.after) continue; + if (state.before === undefined) delete node.storage.mengsanCard_shuying; + else node.storage.mengsanCard_shuying = state.before; + } + cards.clear(); + states.delete(battle); + }); + } + const previous = cards.get(card); + cards.set(card, { before: previous ? previous.before : card.storage?.mengsanCard_shuying, after }); + } + return async function blockUpgrade(player, event, block, all) { + if (!Number.isSafeInteger(block) || block < 0 || typeof all !== "boolean") { + throw new TypeError("武装格挡与手牌强化参数无效"); + } + const battle = getActiveBattle(); + if (!valid(player, battle)) return []; + const pile = getPile(player, battle); + if (typeof pile?.take !== "function") throw new Error("武装缺少个人牌堆接口"); + await player.changeHujia(block); + if (!valid(player, battle) || getPile(player, battle) !== pile) return []; + const candidates = handCards(player, event); + if (!candidates.length) return []; + let chosen = candidates; + if (!all && candidates.length > 1) { + const result = await player.chooseButton({ + createDialog: ["武装:选择强化一张你的手牌", candidates], + forced: true, selectButton: 1, + ai(button) { return get.value(button.link, player); }, + }).forResult(); + if (!result.bool || result.links?.length !== 1 || !candidates.includes(result.links[0])) return []; + chosen = result.links; + } else if (!all) chosen = [candidates[0]]; + // No asynchronous work during commit: one current-hand snapshot avoids quadratic filtering. + const currentHand = new Set(player.getCards("h")); + const playing = new Set(event?.cards || []); + const changed = []; + for (const card of chosen) { + if (!valid(player, battle) || getPile(player, battle) !== pile) break; + if (!currentHand.has(card) || playing.has(card) || !eligible(card)) continue; + const data = dataOf(card); + const next = { ...data, affixes: data.affixes?.slice() || [], upgrade: (data.upgrade || 0) + 1 }; + remember(battle, card, next); + card.storage ||= {}; + card.storage.mengsanCard_shuying = next; + changed.push(card); + } + if (changed.length) { + battle.handUI?.refresh(); + battle.pilesUI?.refresh(); + game.log(player, "强化了", changed, "(仅本场战斗)"); + } + return changed; + }; +} diff --git a/mengsan/cards/intrinsic-affixes.js b/mengsan/cards/intrinsic-affixes.js new file mode 100644 index 0000000..0580f06 --- /dev/null +++ b/mengsan/cards/intrinsic-affixes.js @@ -0,0 +1,12 @@ +import { sharedByName } from "./packs/shared/data.js"; +import { cardUpgradeLevel } from "./upgrades.js"; +const keys = Object.freeze({ethereal:"void",retain:"retain",unplayable:"unplayable",eternal:"eternal",innate:"innate",exhaust:"exhaust"}); +export function cardAffixes(card) { + if (card?.cards?.length === 1 && card.cards[0]?.name === card.name) card=card.cards[0]; + const data=card?.storage?.mengsanCard_shuying || card || {}; + const spec=sharedByName[card?.name], r=spec && (cardUpgradeLevel(card) && spec.upgraded ? spec.upgraded : spec.base); + const result=new Set(Array.isArray(data.affixes) ? data.affixes : []); + if(r) for(const [flag,key] of Object.entries(keys)) if(r[flag])result.add(key); + return [...result]; +} +export const hasCardAffix = (card,key) => cardAffixes(card).includes(key); diff --git a/mengsan/cards/ironclad-advanced-data.js b/mengsan/cards/ironclad-advanced-data.js new file mode 100644 index 0000000..ae81ee0 --- /dev/null +++ b/mengsan/cards/ironclad-advanced-data.js @@ -0,0 +1,2 @@ +// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。 +export * from "./packs/liubei/advanced-data.js"; diff --git a/mengsan/cards/ironclad-data.js b/mengsan/cards/ironclad-data.js new file mode 100644 index 0000000..acac2da --- /dev/null +++ b/mengsan/cards/ironclad-data.js @@ -0,0 +1,2 @@ +// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。 +export * from "./packs/liubei/data.js"; diff --git a/mengsan/cards/ironclad-hooks.js b/mengsan/cards/ironclad-hooks.js new file mode 100644 index 0000000..03484fe --- /dev/null +++ b/mengsan/cards/ironclad-hooks.js @@ -0,0 +1,32 @@ +// 轻量桥接模块:不导入牌注册表或引擎,避免费用/牌堆/运行时循环依赖。 +import { sharedStrengthPenalty } from "./shared-hooks.js"; +const bindings = new WeakMap(), owners = new Map(); +export function bindIroncladHooks(player, session, hooks) { + const entry = {player,session,hooks}; + bindings.set(player,entry); owners.set(player.playerid,entry); + session.ownResource(entry, () => { + if (bindings.get(player) === entry) bindings.delete(player); + if (owners.get(player.playerid) === entry) owners.delete(player.playerid); + }); +} +export function ironcladHooks(player) { + const entry = bindings.get(player); + return entry?.session.active && entry.hooks.valid() ? entry.hooks : null; +} +export function ironcladCardOwner(card) { + const node = card?.cards?.length === 1 ? card.cards[0] : card; + const entry = owners.get(node?.storage?.mengsanOwnerId_shuying); + return entry?.session.active && entry.hooks.valid() ? entry.player : null; +} +export const ironcladBlocksDraw = player => Boolean(ironcladHooks(player)?.blocksDraw()); +export const ironcladBlocksEnergy = player => Boolean(ironcladHooks(player)?.blocksEnergy()); +export const ironcladPreservesBlock = player => Boolean(ironcladHooks(player)?.preservesBlock()); +export const ironcladSkillExhausts = (player,card) => Boolean(ironcladHooks(player)?.skillExhausts(card)); +export const ironcladCost = (player,card,cost) => ironcladHooks(player)?.cost(card,cost) ?? cost; +export const ironcladCardIsFree = (player,card) => Boolean(ironcladHooks(player)?.isFree(card)); +export const ironcladStrengthPenalty = player => (ironcladHooks(player)?.strengthPenalty() || 0) + sharedStrengthPenalty(player); +export const queueIroncladExhaust = (player,card) => ironcladHooks(player)?.queueExhaust(card); +export const flushIroncladExhaust = async player => { await ironcladHooks(player)?.flushExhaust(); }; +export const afterIroncladDraw = async (player,cards) => { await ironcladHooks(player)?.afterDraw(cards); }; +export const afterIroncladVulnerable = (source,target) => ironcladHooks(source)?.queueVulnerable(target); +export const beforeIroncladBlock = (player,num,event) => ironcladHooks(player)?.blockAmount(num,event) ?? num; diff --git a/mengsan/cards/ironclad.js b/mengsan/cards/ironclad.js new file mode 100644 index 0000000..a5689cb --- /dev/null +++ b/mengsan/cards/ironclad.js @@ -0,0 +1,2 @@ +// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。 +export * from "./packs/liubei/runtime.js"; diff --git a/mengsan/cards/life-loss-block.js b/mengsan/cards/life-loss-block.js new file mode 100644 index 0000000..66df094 --- /dev/null +++ b/mengsan/cards/life-loss-block.js @@ -0,0 +1,24 @@ +// 生命流失的原生事件(含濒死救援)完全结束后,才继续结算格挡。 +export function createLifeLossBlocker(getActiveBattle) { + const valid = (player, battle, session) => Boolean(battle && + getActiveBattle() === battle && battle.session === session && session?.active && + battle.players?.has(player) && player?.isAlive?.()); + return { + canUse(player) { + const battle = getActiveBattle(); + return valid(player, battle, battle?.session); + }, + async apply(player, loseHp, block) { + if (!Number.isSafeInteger(loseHp) || loseHp <= 0 || + !Number.isSafeInteger(block) || block < 0) { + throw new TypeError("生命流失格挡数值无效"); + } + const battle = getActiveBattle(), session = battle?.session; + if (!valid(player, battle, session)) return; + await player.loseHp(loseHp); + // 救援成功可以继续;死亡、结束、换场或原会话被替换时不授予格挡。 + if (!valid(player, battle, session)) return; + await player.changeHujia(block); + }, + }; +} diff --git a/mengsan/cards/mode-cards.js b/mengsan/cards/mode-cards.js index fe147dc..80d1114 100644 --- a/mengsan/cards/mode-cards.js +++ b/mengsan/cards/mode-cards.js @@ -2,23 +2,24 @@ // 保留原牌名,以兼容原有牌的响应、装备技能及转化牌引用。 import standard from "../../../../card/standard.js"; import extra from "../../../../card/extra.js"; -import { _status, game, get } from "../../../../noname.js"; +import { _status, game } from "../../../../noname.js"; import { cardCost, canPayCard, isActiveCardUse, SHA_DAMAGE, TRICK_DAMAGE } from "../battle/combat-rules.js"; import { cardUpgradeLevel, cardUpgradeRule } from "./upgrades.js"; +import { cardDefinitions } from "./card-definitions.js"; -import { applyMengsanDebuff } from "../monsters/artifact-status.js"; -import { isAttackCard } from "../monsters/vine-tangled.js"; +import { cleanCardDescription } from "./description.js"; import { DAZED_NAME, dazedDefinition } from "./status-cards.js"; import { SLIMED_NAME, slimedDefinition } from "./slimed-card.js"; import { INFECTION_NAME, infectionDefinition } from "./infection-card.js"; +import { WOUND_NAME, woundDefinition } from "./wound-card.js"; const packs = [standard, extra]; const names = [...new Set(packs.flatMap(pack => pack.list.map(entry => entry[2])))]; const sourceCard = name => standard.card[name] || extra.card[name]; const sourceText = name => extra.translate[`${name}_info`] || standard.translate[`${name}_info`]; -export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporaryStrength, countStrikeCards, randomHandExhaust, battleEnergy, handExhaust, playDrawTop, blockDraw, isBattleActive = () => true } = {}) { +export function createMengsanCards({ cardPacks, isBattleActive = () => true } = {}) { const card = {}; const translate = {}; for (const name of names) { @@ -61,693 +62,6 @@ export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporar `否则受到${damage}点伤害。每回合使用次数不限。`; }; translate.sha_info = `梦三:主动使用消耗1费用。出牌阶段,对攻击范围内的一名角色使用;其须使用【闪】,否则受到${SHA_DAMAGE}点伤害。每回合使用次数不限。`; - card.mengsan_zhongsha = { - type: "basic", - fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png", - mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }), - enable(used, player, event) { - return !isActiveCardUse(event || _status.event, player) || - canPayCard(player, used); - }, - usable: Infinity, - selectTarget: 1, - range: nativeSha.range, - filterTarget: nativeSha.filterTarget, - async content(event, trigger, player) { - const target = event.target; - const effect = cardUpgradeLevel(event.card) - ? cardUpgradeRule("mengsan_zhongsha") - : { damage: 8, vulnerable: 2 }; - const damage = target.damage(effect.damage, player); - damage.mengsanAttack_shuying = true; - await damage; - if (!target.isAlive()) return; - if (applyMengsanDebuff(target, "vulnerable", effect.vulnerable)) - game.log(target, `获得${effect.vulnerable}层易伤`); - }, - ai: { order: 4, result: { target: -1.5 }, - tag: { damage: 1 } }, - cardPrompt(used) { - const effect = cardUpgradeLevel(used) - ? cardUpgradeRule("mengsan_zhongsha") - : { damage: 8, vulnerable: 2 }; - return "梦三:主动使用消耗2费用。对攻击范围内的" + - `一名其他角色造成${effect.damage}点伤害,然后给予其` + - `${effect.vulnerable}层易伤。受到攻击伤害增加50%` + - "(向上取整),每个自身回合结束减少1层。" + - "每回合使用次数不限。"; - }, - }; - translate.mengsan_zhongsha = "重杀"; - translate.mengsan_zhongsha_info = - "梦三:主动使用消耗2费用。出牌阶段,对攻击范围内的" + - "一名其他角色造成8点伤害,然后给予其2层易伤。" + - "易伤使受到的攻击伤害增加50%(向上取整)," + - "每个自身回合结束减少1层。每回合使用次数不限。"; - // STS2 Setup Strike: the new temporary strength does not enhance its own hit. - // STS2 Cinder: damage first, then exhaust one random remaining hand card. - card.mengsan_yujin = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_yujin.png", - mengsanCost_shuying: cardCost({ name: "mengsan_yujin" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || - target === player || !player.isEnemyOf(target)) return; - if (!randomHandExhaust?.capture || !randomHandExhaust?.exhaust) throw new Error("余烬缺少随机消耗接口"); - const battle = randomHandExhaust.capture(); - if (!battle) return; - const rule = cardUpgradeRule("mengsan_yujin"); - const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player); - damage.mengsanAttack_shuying = true; - await damage; - if (isBattleActive() && player.isAlive()) await randomHandExhaust.exhaust(player, event, battle); - }, - ai: { order: 6, result: { target: -2 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_yujin"); - const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return `梦三:主动使用消耗2费用。对一名敌方角色造成${amount}点伤害,然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。每回合使用次数不限。`; - }, - }; - translate.mengsan_yujin = "余烬"; - translate.mengsan_yujin_info = card.mengsan_yujin.cardPrompt({ name: "mengsan_yujin" }); - // STS2 Perfected Strike counts current physical combat cards, including itself/exhaust. - card.mengsan_wanmeidaji = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_wanmeidaji.png", - mengsanCost_shuying: cardCost({ name: "mengsan_wanmeidaji" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || - target === player || !player.isEnemyOf(target)) return; - if (typeof countStrikeCards !== "function") throw new Error("完美打击缺少打击牌计数接口"); - const count = countStrikeCards(player, event); - if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("打击牌数量无效"); - const rule = cardUpgradeRule("mengsan_wanmeidaji"); - const amount = rule.baseDamage + count * (cardUpgradeLevel(event.card) ? rule.perStrike : rule.basePerStrike); - if (!Number.isSafeInteger(amount) || amount < 0) throw new RangeError("完美打击伤害无效"); - const damage = target.damage(amount, player); - damage.mengsanAttack_shuying = true; - damage.mengsanStrikeCount_shuying = count; - await damage; - }, - ai: { order: 7, result: { target: -2 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_wanmeidaji"); - const bonus = cardUpgradeLevel(used) ? rule.perStrike : rule.basePerStrike; - return `梦三:主动使用消耗2费用。对一名敌方角色造成6点伤害,本场战斗的牌中每有一张名称含“打击”的牌,伤害增加${bonus}点(包括手牌、个人抽牌堆、个人弃牌堆、消耗牌及打出区)。每回合使用次数不限。`; - }, - }; - translate.mengsan_wanmeidaji = "完美打击"; - translate.mengsan_wanmeidaji_info = card.mengsan_wanmeidaji.cardPrompt({ name: "mengsan_wanmeidaji" }); - card.mengsan_yubeidaji = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_yubeidaji.png", - mengsanCost_shuying: cardCost({ name: "mengsan_yubeidaji" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || - target === player || !player.isEnemyOf(target)) return; - if (!temporaryStrength?.capture || !temporaryStrength?.grant) throw new Error("预备打击缺少临时力量接口"); - const battle = temporaryStrength.capture(); - if (!battle) return; - const rule = cardUpgradeRule("mengsan_yubeidaji"); - const upgraded = cardUpgradeLevel(event.card); - const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player); - damage.mengsanAttack_shuying = true; - await damage; - if (isBattleActive() && player.isAlive()) { - temporaryStrength.grant(player, upgraded ? rule.strength : rule.baseStrength, event, battle); - } - }, - ai: { order: 8, result: { player: 0.8, target: -1.5 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_yubeidaji"); - const upgraded = cardUpgradeLevel(used); - return `梦三:主动使用消耗1费用。对一名敌方角色造成${upgraded ? rule.damage : rule.baseDamage}点伤害,然后在本回合内获得${upgraded ? rule.strength : rule.baseStrength}点力量。每回合使用次数不限。`; - }, - }; - translate.mengsan_yubeidaji = "预备打击"; - translate.mengsan_yubeidaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成7点伤害,然后在本回合内获得2点力量。每回合使用次数不限。"; - // STS2 Breakthrough: native life loss is paid once before the all-enemy attack. - card.mengsan_tupo = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_tupo.png", - mengsanCost_shuying: cardCost({ name: "mengsan_tupo" }), - notarget: true, usable: Infinity, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - if (!game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)).length) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - async content(event, trigger, player) { - if (!isBattleActive() || !player.isAlive()) return; - if (!game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)).length) return; - // loseHp bypasses block and awaits the normal dying/rescue process. - await player.loseHp(1); - if (!isBattleActive() || !player.isAlive()) return; - const rule = cardUpgradeRule("mengsan_tupo"); - const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; - const targets = game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)); - for (const target of targets) { - if (!isBattleActive() || !player.isAlive()) break; - if (!target.isAlive() || !player.isEnemyOf(target)) continue; - player.line(target, "green"); - const damage = target.damage(amount, player); - damage.mengsanAttack_shuying = true; - await damage; - } - }, - ai: { order: 7, result: { player(player) { return player.hp <= 1 ? -10 : 1; } }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_tupo"); - const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return `梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成${amount}点伤害。每回合使用次数不限。`; - }, - }; - translate.mengsan_tupo = "突破"; - translate.mengsan_tupo_info = "梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成9点伤害。每回合使用次数不限。"; - // STS2 Headbutt: damage first, then choose from the actor's own real discard pile. - card.mengsan_touchui = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_touchui.png", - mengsanCost_shuying: cardCost({ name: "mengsan_touchui" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || - target === player || !player.isEnemyOf(target)) return; - if (typeof reclaimFromDiscard !== "function") throw new Error("头槌缺少个人弃牌回收接口"); - const rule = cardUpgradeRule("mengsan_touchui"); - const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; - const damage = target.damage(amount, player); - damage.mengsanAttack_shuying = true; - await damage; - // Defeating one enemy does not prevent recovery unless combat actually ends. - if (isBattleActive() && player.isAlive()) await reclaimFromDiscard(player); - }, - ai: { order: 6, result: { player: 0.4, target: -1.5 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_touchui"); - const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return '梦三:主动使用消耗1费用。对一名敌方角色造成' + amount + - '点伤害,然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。'; - }, - }; - translate.mengsan_touchui = "头槌"; - translate.mengsan_touchui_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害," + - "然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。"; - // STS2 Iron Wave: obtain block before resolving the attack. - card.mengsan_tiezhanbo = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_tiezhanbo.png", - mengsanCost_shuying: cardCost({ name: "mengsan_tiezhanbo" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - const canResolve = () => isBattleActive() && player.isAlive() && - target?.isAlive() && target !== player && player.isEnemyOf(target); - if (!canResolve()) return; - const rule = cardUpgradeRule("mengsan_tiezhanbo"); - const upgraded = cardUpgradeLevel(event.card) > 0; - await player.changeHujia(upgraded ? rule.block : rule.baseBlock); - // Block triggers may end combat or remove the acting player/target. - if (!canResolve()) return; - const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player); - damage.mengsanAttack_shuying = true; - await damage; - }, - ai: { order: 7, result: { player: 0.6, target: -1.2 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_tiezhanbo"); - const upgraded = cardUpgradeLevel(used) > 0; - const block = upgraded ? rule.block : rule.baseBlock; - const damage = upgraded ? rule.damage : rule.baseDamage; - return '梦三:主动使用消耗1费用。获得' + block + - '点格挡(护甲),然后对一名敌方角色造成' + damage + - '点伤害。每回合使用次数不限。'; - }, - }; - translate.mengsan_tiezhanbo = "铁斩波"; - translate.mengsan_tiezhanbo_info = "梦三:主动使用消耗1费用。获得5点格挡(护甲)," + - "然后对一名敌方角色造成5点伤害。每回合使用次数不限。"; - // STS2 Twin Strike: each hit resolves on the same target separately. - card.mengsan_shuangchongdaji = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_shuangchongdaji.png", - mengsanCost_shuying: cardCost({ name: "mengsan_shuangchongdaji" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - const target = event.target; - const rule = cardUpgradeRule("mengsan_shuangchongdaji"); - const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; - for (let hitIndex = 0; hitIndex < rule.hits; hitIndex++) { - if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || - target === player || !player.isEnemyOf(target)) break; - const damage = target.damage(amount, player); - damage.mengsanAttack_shuying = true; - await damage; - } - }, - ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_shuangchongdaji"); - const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return '梦三:主动使用消耗1费用。对一名敌方角色造成' + - amount + '点伤害两次,两次伤害分别结算。每回合使用次数不限。'; - }, - }; - translate.mengsan_shuangchongdaji = "双重打击"; - translate.mengsan_shuangchongdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成5点伤害两次," + - "两次伤害分别结算。每回合使用次数不限。"; - // STS2 Thunderclap: hit each living enemy before adding new vulnerable. - card.mengsan_shandianpili = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_shandianpili.png", - mengsanCost_shuying: cardCost({ name: "mengsan_shandianpili" }), - notarget: true, usable: Infinity, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - if (!game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)).length) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - async content(event, trigger, player) { - if (!isBattleActive() || !player.isAlive()) return; - const rule = cardUpgradeRule("mengsan_shandianpili"); - const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; - const targets = game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)); - for (const target of targets) { - if (!isBattleActive() || !player.isAlive()) break; - if (!target.isAlive() || !player.isEnemyOf(target)) continue; - player.line(target, "green"); - const damage = target.damage(amount, player); - damage.mengsanAttack_shuying = true; - await damage; - } - // 伤害结束后重新取得存活敌人,包含异蛙亡语刚生成的扭动虫。 - for (const target of game.filterPlayer(target => target !== player && target.isAlive() && player.isEnemyOf(target))) { - if (!isBattleActive() || !player.isAlive()) break; - if (applyMengsanDebuff(target, "vulnerable", rule.vulnerable)) - game.log(target, `获得${rule.vulnerable}层易伤`); - } - }, - ai: { order: 7, result: { player: 1 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_shandianpili"); - const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return `梦三:主动使用消耗1费用。对所有敌方角色造成${amount}点伤害,然后给予其1层易伤。` + - "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。"; - }, - }; - translate.mengsan_shandianpili = "闪电霹雳"; - translate.mengsan_shandianpili_info = "梦三:主动使用消耗1费用。对所有敌方角色造成4点伤害,然后给予其1层易伤。" + - "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。"; - // Each throw resolves separately and samples the still-living enemies again. - card.mengsan_feijianhuixuanbiao = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png", - mengsanCost_shuying: cardCost({ name: "mengsan_feijianhuixuanbiao" }), - notarget: true, usable: Infinity, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - if (!game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)).length) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - async content(event, trigger, player) { - const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao"); - const count = cardUpgradeLevel(event.card) ? rule.hits : rule.baseHits; - for (let hitIndex = 0; hitIndex < count; hitIndex++) { - if (!isBattleActive() || !player.isAlive()) break; - const enemies = game.filterPlayer(target => target !== player && - target.isAlive() && player.isEnemyOf(target)); - if (!enemies.length) break; - // Use the engine helper with an inclusive index range. - const target = enemies[get.rand(0, enemies.length - 1)]; - player.line(target, "green"); - const damage = target.damage(rule.damage, player); - damage.mengsanAttack_shuying = true; - await damage; - } - }, - ai: { order: 6, result: { player: 1 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao"); - const count = cardUpgradeLevel(used) ? rule.hits : rule.baseHits; - return `梦三:主动使用消耗1费用。每次对随机一名敌方角色造成${rule.damage}点伤害,共${count}次。` + - "每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。" + - "每回合使用次数不限。"; - }, - }; - translate.mengsan_feijianhuixuanbiao = "飞剑回旋镖"; - translate.mengsan_feijianhuixuanbiao_info = "梦三:主动使用消耗1费用。随机对敌人造成3点伤害3次。" + - "每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。每回合使用次数不限。"; - // Resolve damage before drawing from the acting player's mode-owned pile. - card.mengsan_jianbingdaji = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_jianbingdaji.png", - mengsanCost_shuying: cardCost({ name: "mengsan_jianbingdaji" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - if (!isBattleActive() || !player.isAlive()) return; - const rule = cardUpgradeRule("mengsan_jianbingdaji"); - const upgraded = cardUpgradeLevel(event.card) > 0; - const damage = event.target.damage(upgraded ? rule.damage : rule.baseDamage, player); - damage.mengsanAttack_shuying = true; - await damage; - // Defeating the last enemy may dispose the personal pile during damage. - if (isBattleActive() && player.isAlive()) { - await player.draw(upgraded ? rule.draw : rule.baseDraw); - } - }, - ai: { order: 6, result: { player: 0.6, target: -1.5 }, tag: { damage: 1, draw: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_jianbingdaji"); - const upgraded = cardUpgradeLevel(used) > 0; - const damage = upgraded ? rule.damage : rule.baseDamage; - const draw = upgraded ? rule.draw : rule.baseDraw; - return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后抽${draw}张牌。` + - "每回合使用次数不限。"; - }, - }; - translate.mengsan_jianbingdaji = "剑柄打击"; - translate.mengsan_jianbingdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害,然后抽1张牌。" + - "每回合使用次数不限。"; - // Block determines base damage without spending the acting player's block. - card.mengsan_quanshenzhuangji = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_quanshenzhuangji.png", - mengsanCost_shuying: cardCost({ name: "mengsan_quanshenzhuangji" }), - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - if (!isBattleActive() || !player.isAlive()) return; - const block = Number.isFinite(player.hujia) ? Math.max(0, player.hujia) : 0; - const storedStrength = player.storage?.mengsanStrength_shuying; - const strength = Number.isInteger(storedStrength) ? storedStrength : 0; - // Native damage(0) stops before damageBegin1, so strength must be included here. - const temporary = temporaryStrength?.amount(player) || 0; - const damage = event.target.damage(Math.max(0, block + strength + temporary), player); - damage.mengsanAttack_shuying = true; - damage.mengsanCardStrengthApplied_shuying = true; - damage.mengsanSetupStrengthApplied_shuying = true; - await damage; - }, - ai: { - order: 5, - result: { target(used, player) { return player.hujia > 0 ? -1.5 : 0; } }, - tag: { damage: 1 }, - }, - cardPrompt(used) { - const cost = cardCost(used || { name: "mengsan_quanshenzhuangji" }); - return `梦三:主动使用消耗${cost}费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。` + - "不消耗格挡,可以受到力量加成。" + - "每回合使用次数不限。"; - }, - }; - translate.mengsan_quanshenzhuangji = "全身撞击"; - translate.mengsan_quanshenzhuangji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。" + - "不消耗格挡,可以受到力量加成。每回合使用次数不限。"; - // Double only the surviving target's existing vulnerable after damage resolves. - card.mengsan_rongrongzhiquan = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_rongrongzhiquan.png", - mengsanCost_shuying: cardCost({ name: "mengsan_rongrongzhiquan" }), - mengsanExhaust_shuying: true, - usable: Infinity, selectTarget: 1, - enable(used, player, event) { - if (!isBattleActive() || !player.isAlive()) return false; - return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); - }, - filterTarget(used, player, target) { - return target !== player && target.isAlive() && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - if (!isBattleActive() || !player.isAlive()) return; - const rule = cardUpgradeRule("mengsan_rongrongzhiquan"); - const target = event.target; - const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player); - damage.mengsanAttack_shuying = true; - await damage; - if (!target.isAlive() || !isBattleActive()) return; - const stacks = target.storage?.mengsanVulnerable_shuying; - if (!Number.isSafeInteger(stacks) || stacks <= 0) return; - const doubled = stacks * 2; - if (!Number.isSafeInteger(doubled)) throw new RangeError("易伤层数越界"); - if (applyMengsanDebuff(target, "vulnerable", stacks)) - game.log(target, `易伤层数从${stacks}翻倍为${doubled}`); - }, - ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_rongrongzhiquan"); - const damage = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; - return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后将该目标已有的易伤层数翻倍。` + - "没有易伤时不会新增易伤。消耗:使用后退出本场战斗,不进入弃牌堆或洗牌。"; - }, - }; - translate.mengsan_rongrongzhiquan = "熔融之拳"; - translate.mengsan_rongrongzhiquan_info = "梦三:主动使用消耗1费用。对一名敌方角色造成10点伤害,然后将该目标已有的易伤层数翻倍。" + - "消耗:使用后退出本场战斗。"; - // STS2【愤怒】:攻击后生成独立的战斗复制品,不扩充永久牌组。 - card.mengsan_fennu = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_fennu.png", - mengsanCost_shuying: cardCost({ name: "mengsan_fennu" }), - enable(used, player, event) { - return !isActiveCardUse(event || _status.event, player) || - canPayCard(player, used); - }, - usable: Infinity, selectTarget: 1, - filterTarget(used, player, target) { - return target !== player && player.isEnemyOf(target); - }, - async content(event, trigger, player) { - if (typeof copyToDiscard !== "function") throw new Error("愤怒缺少战斗复制接口"); - const amount = cardUpgradeLevel(event.card) - ? cardUpgradeRule("mengsan_fennu").damage : 6; - const damage = event.target.damage(amount, player); - damage.mengsanAttack_shuying = true; - await damage; - if (copyToDiscard(player, event.card, event.cards)) { - game.log(player, "将一张【愤怒】的复制品加入个人弃牌堆"); - } - }, - ai: { order: 6, result: { target: -1 }, tag: { damage: 1 } }, - cardPrompt(used) { - const amount = cardUpgradeLevel(used) - ? cardUpgradeRule("mengsan_fennu").damage : 6; - return `梦三:主动使用消耗0费用。对一名敌方角色造成${amount}点伤害,` + - "然后将一张此牌的复制品加入你的个人弃牌堆。" + - "复制品保留原牌状态和词缀,仅存在于本场战斗。"; - }, - }; - translate.mengsan_fennu = "愤怒"; - translate.mengsan_fennu_info = "梦三:主动使用消耗0费用。对一名敌方角色造成6点伤害," + - "然后将一张此牌的复制品加入你的个人弃牌堆。复制品保留原牌状态和词缀,仅存在于本场战斗。" + - "每回合使用次数不限。"; - // STS2【放血】:先等待原生生命流失及濒死救援,再增加当前战斗费用。 - card.mengsan_fangxue = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_fangxue.png", - mengsanCost_shuying: cardCost({ name: "mengsan_fangxue" }), - enable(used, player, event) { - return isBattleActive() && Boolean(battleEnergy?.canUse(player)) && - (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); - }, - usable: Infinity, toself: true, selectTarget: -1, - filterTarget(used, player, target) { return target === player; }, - async content(event, trigger, player) { - if (typeof battleEnergy?.capture !== "function" || typeof battleEnergy?.grant !== "function") { - throw new Error("放血缺少战斗费用接口"); - } - const rule = cardUpgradeRule("mengsan_fangxue"); - const amount = cardUpgradeLevel(event.card) ? rule.energy : rule.baseEnergy; - const battle = battleEnergy.capture(player, amount); - if (!battle) return; - await player.loseHp(rule.loseHp); - battleEnergy.grant(player, amount, battle); - }, - ai: { order: 9, result: { player: player => player.hp > 3 ? 1 : -1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_fangxue"); - const amount = cardUpgradeLevel(used) ? rule.energy : rule.baseEnergy; - return "梦三:主动使用消耗0费用。失去" + rule.loseHp + "点生命,然后获得" + amount + "点费用。"; - }, - }; - translate.mengsan_fangxue = "放血"; - translate.mengsan_fangxue_info = "梦三:主动使用消耗0费用。失去3点生命,然后获得2点费用。"; - // STS2【坚毅】:先获得格挡,再消耗一张自身当时的手牌。 - card.mengsan_jianyi = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_jianyi.png", - mengsanCost_shuying: cardCost({ name: "mengsan_jianyi" }), - enable(used, player, event) { - return isBattleActive() && Boolean(player?.isAlive?.()) && - (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); - }, - usable: Infinity, toself: true, selectTarget: -1, - filterTarget(used, player, target) { return target === player; }, - async content(event, trigger, player) { - if (typeof handExhaust?.capture !== "function" || typeof handExhaust?.exhaust !== "function") { - throw new Error("坚毅缺少手牌消耗接口"); - } - const battle = handExhaust.capture(player); - if (!battle) return; - const rule = cardUpgradeRule("mengsan_jianyi"); - const current = cardUpgradeLevel(event.card); - await player.changeHujia(current ? rule.block : rule.baseBlock); - await handExhaust.exhaust(player, event, battle, Boolean(current && rule.chooseExhaust)); - }, - ai: { order: 7, result: { player: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_jianyi"); - const current = cardUpgradeLevel(used); - const block = current ? rule.block : rule.baseBlock; - return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后" + - (current && rule.chooseExhaust ? "选择消耗" : "随机消耗") + - "一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。"; - }, - }; - translate.mengsan_jianyi = "坚毅"; - translate.mengsan_jianyi_info = "梦三:主动使用消耗1费用。获得7点格挡(护甲),然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。"; - // STS2【破灭】:免费使用自己的堆顶原牌,再将这一原牌消耗。 - card.mengsan_pomie = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_pomie.png", - mengsanCost_shuying: cardCost({ name: "mengsan_pomie" }), - enable(used, player, event) { - return isBattleActive() && Boolean(player?.isAlive?.()) && - (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); - }, - usable: Infinity, toself: true, selectTarget: -1, - filterTarget(used, player, target) { return target === player; }, - async content(event, trigger, player) { - if (typeof playDrawTop !== "function") throw new Error("破灭缺少堆顶使用接口"); - await playDrawTop(player); - }, - ai: { order: 6, result: { player: 1 } }, - cardPrompt(used) { - return "梦三:主动使用消耗" + cardCost(used) + - "费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。"; - }, - }; - translate.mengsan_pomie = "破灭"; - translate.mengsan_pomie_info = "梦三:主动使用消耗1费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。"; - // STS2【耸肩无视】:先获得格挡,再抽一张牌。 - card.mengsan_songjianwushi = { - type: "basic", fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_songjianwushi.png", - mengsanCost_shuying: cardCost({ name: "mengsan_songjianwushi" }), - enable(used, player, event) { - return isBattleActive() && Boolean(player?.isAlive?.()) && - (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); - }, - usable: Infinity, toself: true, selectTarget: -1, - filterTarget(used, player, target) { return target === player; }, - async content(event, trigger, player) { - if (typeof blockDraw !== "function") throw new Error("耸肩无视缺少格挡抽牌接口"); - const rule = cardUpgradeRule("mengsan_songjianwushi"); - await blockDraw(player, cardUpgradeLevel(event.card) ? rule.block : rule.baseBlock, rule.draw); - }, - ai: { order: 7, result: { player: 1 }, tag: { draw: 1 } }, - cardPrompt(used) { - const rule = cardUpgradeRule("mengsan_songjianwushi"); - const block = cardUpgradeLevel(used) ? rule.block : rule.baseBlock; - return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后抽" + rule.draw + "张牌。"; - }, - }; - translate.mengsan_songjianwushi = "耸肩无视"; - translate.mengsan_songjianwushi_info = "梦三:主动使用消耗1费用。获得8点格挡(护甲),然后抽1张牌。"; - // 【防御】沿用梦三护甲事件,保留脆弱与伤害抵扣等既有规则。 - card.mengsan_fangyu = { - type: "basic", - fullimage: true, - image: "ext:术樱包/mengsan/assets/cards/mengsan_fangyu.png", - mengsanCost_shuying: cardCost({ name: "mengsan_fangyu" }), - enable(used, player, event) { - return !isActiveCardUse(event || _status.event, player) || - canPayCard(player, used); - }, - usable: Infinity, - toself: true, - selectTarget: -1, - filterTarget(used, player, target) { return target === player; }, - async content(event, trigger, player) { - await player.changeHujia(5); - }, - ai: { order: 7, result: { target: 1 } }, - cardPrompt() { return translate.mengsan_fangyu_info; }, - }; - translate.mengsan_fangyu = "防御"; - translate.mengsan_fangyu_info = - "梦三:主动使用消耗1费用。出牌阶段,对自己使用,获得5点格挡(护甲)。" + - "每回合使用次数不限。"; - // 【桃】的回复量也由梦三牌本体提供;救援仍不消耗费用。 card.tao.content = async function (event) { await event.target.recover(8); }; translate.tao_info = "梦三:主动使用消耗1费用,回复8点生命;濒死救援不消耗费用,同样回复8点生命。"; card.taoyuan.content = async function (event) { await event.target.recover(8); }; @@ -779,6 +93,9 @@ export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporar }; translate.shandian_info = "梦三:主动使用消耗1费用。若判定为黑桃2~9,目标受到12点雷电伤害;否则移至下家的判定区。"; card.shandian.cardPrompt = () => translate.shandian_info; + card[WOUND_NAME] = { ...woundDefinition }; + translate[WOUND_NAME] = "伤口"; + translate[`${WOUND_NAME}_info`] = woundDefinition.cardPrompt(); card[INFECTION_NAME] = { ...infectionDefinition }; translate[INFECTION_NAME] = "感染"; translate[`${INFECTION_NAME}_info`] = infectionDefinition.cardPrompt(); @@ -788,15 +105,25 @@ export function createMengsanCards({ copyToDiscard, reclaimFromDiscard, temporar card[DAZED_NAME] = { ...dazedDefinition }; translate[DAZED_NAME] = "晕眩"; translate[`${DAZED_NAME}_info`] = dazedDefinition.cardPrompt(); + if (cardPacks) { + Object.assign(card, cardPacks.cards); + Object.assign(translate, cardPacks.translate); + } for (const [name, definition] of Object.entries(card)) { - if (!isAttackCard({ name }) || typeof definition.cardPrompt !== "function") continue; + const classification = cardDefinitions[name]; + definition.cardType = classification?.cardType ?? (definition.type === "status" ? "status" : null); + definition.rarity = classification?.rarity ?? definition.rarity ?? (definition.type === "status" ? "status" : "common"); + definition.mengsanPower_shuying = definition.cardType === "power" || + !definition.cardType && Boolean(definition.mengsanPower_shuying); + const infoKey = `${name}_info`; + if (typeof translate[infoKey] === "string") { + translate[infoKey] = cleanCardDescription(translate[infoKey]); + } + if (typeof definition.cardPrompt !== "function") continue; const prompt = definition.cardPrompt; definition.cardPrompt = function (used) { - const text = prompt.call(this, used); - const owner = used && typeof get.owner === "function" ? get.owner(used) : null; - if (!isBattleActive() || !owner?.storage?.mengsanTangled_shuying || typeof text !== "string") return text; - return text.replace(/主动使用消耗\d+费用/, `主动使用消耗${cardCost(used || { name }, owner)}费用`); + return cleanCardDescription(prompt.call(this, used)); }; } - return { card, translate, names: [...names, "mengsan_zhongsha", "mengsan_fangyu", "mengsan_fennu", "mengsan_feijianhuixuanbiao", "mengsan_jianbingdaji", "mengsan_quanshenzhuangji", "mengsan_rongrongzhiquan", "mengsan_shandianpili", "mengsan_shuangchongdaji", "mengsan_tiezhanbo", "mengsan_touchui", "mengsan_tupo", "mengsan_yubeidaji", "mengsan_wanmeidaji", "mengsan_yujin", "mengsan_fangxue", "mengsan_jianyi", "mengsan_pomie", "mengsan_songjianwushi"] }; + return { card, translate, names:[...new Set([...names, ...(cardPacks?.names || [])])] }; } diff --git a/mengsan/cards/packs/README.md b/mengsan/cards/packs/README.md new file mode 100644 index 0000000..896be5d --- /dev/null +++ b/mengsan/cards/packs/README.md @@ -0,0 +1,27 @@ +# 角色牌组 + +当前铁甲战士主表 94 张:新增 67 张、沿用 27 张;另有衍生牌【巨石】。专属牌归刘备,基础打击/痛击分别沿用杀/重杀。所有正常获得入口沿用 `canAcquireCard` 的角色检查。杀、重杀保留原有通用牌归属,并纳入刘备的随机生成牌池。旧存档的独立打击/痛击转换为杀/重杀,保留实例ID、强化和词缀;待结算候选保留原ID以避免冲突。 + +| 目录/文件 | 职责 | +| --- | --- | +| `liubei/index.js` | `LiuBeiCardPack`,组合刘备牌组运行时 | +| `liubei/data.js`、`advanced-data.js` | 新增 67 张及巨石的详情页数值 | +| `liubei/metadata.js`、`names.js` | 刘备专属牌的归属、费用、强化、原有牌名和基础牌映射 | +| `liubei/legacy.js` | 沿用的 25 张牌和兼容重杀定义 | +| `liubei/runtime.js`、`advanced.js` | 效果与本场持续状态 | +| `liubei/rewards.js` | 本次新增牌的永久奖励条目 | +| `colorless/` | `ColorlessCardPack`,所有角色共享,当前数据、效果和奖励为空 | +| `index.js` | 汇总各牌组类,检查重复定义,转发公共事件 | +| `data.js`、`rewards.js` | 汇总各牌组的纯数据与奖励 | + +未来加入曹操、孙权时各建目录和牌组类,在三个汇总入口登记;费用、强化和永久注册表读取汇总数据,`mode-cards.js` 只保留三国杀通用牌适配。原来的 `ironclad*.js` 导入路径保留兼容导出。 + +新增无色牌时:在 `colorless/data.js` 登记 `name/category/cost/base/upgraded`;在其牌组类返回实际的 `cards/translate/skills/names`,再补奖励条目。无色定义固定为 `owner:null, deck:"colorless"`,添柴的牌池会自动扩充。三国杀通用牌保留 `deck:"legacy"`,不会自动被算作无色牌。 + +个人牌堆、费用支付、消耗记录、临时免费用牌授权和抽牌入口属于共用机制。`ironclad-hooks.js` 目前是牌组向这些入口注册本场效果的轻量桥接模块,不导入引擎或牌数据;状态受当前战斗会话约束,结束即清理。 + +适配规则:添柴使用所有可正常生成的刘备牌与无色牌;巨石仅由原始力量生成,不进入永久奖励。多人效果按梦三存活角色处理,群情激愤向全部存活角色的个人弃牌堆加副本;肉盾作用于梦三友方阵营,采用详情页的双倍/减半数值。自动打出的牌沿用原生合法目标与牌效果。 + +本次仅做源码语法、引用和图片文件核对;实际对局与视觉验收由用户执行。 + +打击牌判断:杀对应基础打击,计入完美打击,并能被地狱狂徒自动打出;重杀对应痛击,不属于打击牌。 diff --git a/mengsan/cards/packs/colorless/data.js b/mengsan/cards/packs/colorless/data.js new file mode 100644 index 0000000..f8135a6 --- /dev/null +++ b/mengsan/cards/packs/colorless/data.js @@ -0,0 +1,1214 @@ +// Wiki Data:Card.tabx 保存快照;运行时效果由共用模块实现。 +export const colorlessCards = Object.freeze([ + { + "id": "volley", + "name": "mengsan_colorless_volley", + "title": "连射", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": "X", + "description": "随机对敌人造成10点伤害X次。", + "base": { + "damage": 10, + "hits": 1 + }, + "upgraded": { + "damage": 14, + "hits": 1, + "cost": "X", + "description": "随机对敌人造成14点伤害X次。" + } + }, + { + "id": "flash_of_steel", + "name": "mengsan_colorless_flash_of_steel", + "title": "亮剑", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 0, + "description": "造成5点伤害。抽1张牌。", + "base": { + "damage": 5, + "hits": 1, + "draw": 1 + }, + "upgraded": { + "damage": 8, + "hits": 1, + "draw": 1, + "cost": 0, + "description": "造成8点伤害。抽1张牌。" + } + }, + { + "id": "dramatic_entrance", + "name": "mengsan_colorless_dramatic_entrance", + "title": "闪亮登场", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 0, + "description": "固有。对所有敌人造成11点伤害。消耗。", + "base": { + "exhaust": true, + "innate": true, + "damage": 11, + "hits": 1, + "all": true + }, + "upgraded": { + "exhaust": true, + "innate": true, + "damage": 15, + "hits": 1, + "all": true, + "cost": 0, + "description": "固有。对所有敌人造成15点伤害。消耗。" + } + }, + { + "id": "omnislice", + "name": "mengsan_colorless_omnislice", + "title": "万向斩", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 0, + "description": "造成8点伤害。对所有其他敌人造成等量的伤害。", + "base": { + "damage": 8, + "hits": 1 + }, + "upgraded": { + "damage": 11, + "hits": 1, + "cost": 0, + "description": "造成11点伤害。对所有其他敌人造成等量的伤害。" + } + }, + { + "id": "ultimate_strike", + "name": "mengsan_colorless_ultimate_strike", + "title": "究极打击", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成14点伤害。", + "base": { + "damage": 14, + "hits": 1 + }, + "upgraded": { + "damage": 20, + "hits": 1, + "cost": 1, + "description": "造成20点伤害。" + } + }, + { + "id": "fisticuffs", + "name": "mengsan_colorless_fisticuffs", + "title": "拳斗", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成7点伤害。获得等量于所造成伤害的格挡。", + "base": { + "damage": 7, + "hits": 1 + }, + "upgraded": { + "damage": 9, + "hits": 1, + "cost": 1, + "description": "造成9点伤害。获得等量于所造成伤害的格挡。" + } + }, + { + "id": "gang_up", + "name": "mengsan_colorless_gang_up", + "title": "群起攻之", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成5点伤害。本回合其他玩家每攻击过一次该敌人,该牌造成的伤害就额外增加5点。", + "base": { + "damage": 5, + "hits": 1, + "perAllyAttack": 5 + }, + "upgraded": { + "damage": 5, + "hits": 1, + "perAllyAttack": 7, + "cost": 1, + "description": "造成5点伤害。本回合其他玩家每攻击过一次该敌人,该牌造成的伤害就额外增加7点。" + } + }, + { + "id": "seeker_strike", + "name": "mengsan_colorless_seeker_strike", + "title": "探寻打击", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成9点伤害。从抽牌堆的随机3张牌中选择一张加入你的手牌。", + "base": { + "damage": 9, + "hits": 1 + }, + "upgraded": { + "damage": 12, + "hits": 1, + "cost": 1, + "description": "造成12点伤害。从抽牌堆的随机3张牌中选择一张加入你的手牌。" + } + }, + { + "id": "thrumming_hatchet", + "name": "mengsan_colorless_thrumming_hatchet", + "title": "无休手斧", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成11点伤害。在你的下个回合开始时,将此卡返回你的手牌。", + "base": { + "damage": 11, + "hits": 1 + }, + "upgraded": { + "damage": 14, + "hits": 1, + "cost": 1, + "description": "造成14点伤害。在你的下个回合开始时,将此卡返回你的手牌。" + } + }, + { + "id": "mind_blast", + "name": "mengsan_colorless_mind_blast", + "title": "心灵震慑", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "固有。造成你抽牌堆中剩余牌数的伤害。", + "base": { + "innate": true, + "hits": 1 + }, + "upgraded": { + "innate": true, + "hits": 1, + "cost": 0, + "description": "固有。造成你抽牌堆中剩余牌数的伤害。" + } + }, + { + "id": "dark_shackles", + "name": "mengsan_colorless_dark_shackles", + "title": "黑暗镣铐", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "使一名敌人在本回合失去9点力量。消耗。", + "base": { + "exhaust": true, + "negativeStrength": 9 + }, + "upgraded": { + "exhaust": true, + "negativeStrength": 15, + "cost": 0, + "description": "使一名敌人在本回合失去15点力量。消耗。" + } + }, + { + "id": "jack_of_all_trades", + "name": "mengsan_colorless_jack_of_all_trades", + "title": "花样百出", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "将1张随机无色牌加入你的手牌。消耗。", + "base": { + "exhaust": true, + "generate": 1 + }, + "upgraded": { + "exhaust": true, + "generate": 2, + "cost": 0, + "description": "将2张随机无色牌加入你的手牌。消耗。" + } + }, + { + "id": "impatience", + "name": "mengsan_colorless_impatience", + "title": "急躁", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "如果你的手牌中没有攻击牌,抽2张牌。", + "base": { + "conditionalDraw": 2 + }, + "upgraded": { + "conditionalDraw": 3, + "cost": 0, + "description": "如果你的手牌中没有攻击牌,抽3张牌。" + } + }, + { + "id": "purity", + "name": "mengsan_colorless_purity", + "title": "净化", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "保留。从手牌中选择最多3张牌消耗。消耗。", + "base": { + "exhaust": true, + "retain": true, + "chooseExhaust": 3 + }, + "upgraded": { + "exhaust": true, + "retain": true, + "chooseExhaust": 5, + "cost": 0, + "description": "保留。从手牌中选择最多5张牌消耗。消耗。" + } + }, + { + "id": "finesse", + "name": "mengsan_colorless_finesse", + "title": "妙计", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "获得4点格挡。抽1张牌。", + "base": { + "block": 4, + "draw": 1 + }, + "upgraded": { + "block": 7, + "draw": 1, + "cost": 0, + "description": "获得7点格挡。抽1张牌。" + } + }, + { + "id": "thinking_ahead", + "name": "mengsan_colorless_thinking_ahead", + "title": "深谋远虑", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "抽2张牌。将手牌中的一张牌放到你的抽牌堆的顶端。消耗。", + "base": { + "exhaust": true, + "draw": 2 + }, + "upgraded": { + "draw": 2, + "cost": 0, + "description": "抽2张牌。将手牌中的一张牌放到你的抽牌堆的顶端。" + } + }, + { + "id": "production", + "name": "mengsan_colorless_production", + "title": "生产制造", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "获得2点能量。消耗。", + "base": { + "exhaust": true, + "energy": 2 + }, + "upgraded": { + "exhaust": true, + "energy": 3, + "cost": 0, + "description": "获得3点能量。消耗。" + } + }, + { + "id": "believe_in_you", + "name": "mengsan_colorless_believe_in_you", + "title": "相信着你", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "另一名玩家获得2点能量。", + "base": { + "allyEnergy": 2 + }, + "upgraded": { + "allyEnergy": 3, + "cost": 0, + "description": "另一名玩家获得3点能量。" + } + }, + { + "id": "restlessness", + "name": "mengsan_colorless_restlessness", + "title": "心神不宁", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "保留。如果你的手牌为空,则抽2张牌并获得2点能量。", + "base": { + "retain": true, + "emptyDraw": 2, + "emptyEnergy": 2 + }, + "upgraded": { + "retain": true, + "emptyDraw": 3, + "emptyEnergy": 3, + "cost": 0, + "description": "保留。如果你的手牌为空,则抽3张牌并获得3点能量。" + } + }, + { + "id": "prolong", + "name": "mengsan_colorless_prolong", + "title": "延伸", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "在下个回合获得等量于你当前格挡值的格挡。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "cost": 0, + "description": "在下个回合获得等量于你当前格挡值的格挡。" + } + }, + { + "id": "panic_button", + "name": "mengsan_colorless_panic_button", + "title": "应急按钮", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 0, + "description": "获得30点格挡。你在接下来的2回合内无法再从卡牌中获得格挡。消耗。", + "base": { + "exhaust": true, + "block": 30 + }, + "upgraded": { + "exhaust": true, + "block": 40, + "cost": 0, + "description": "获得40点格挡。你在接下来的2回合内无法再从卡牌中获得格挡。消耗。" + } + }, + { + "id": "huddle_up", + "name": "mengsan_colorless_huddle_up", + "title": "抱团", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "所有玩家抽2张牌。消耗。", + "base": { + "exhaust": true, + "teamDraw": 2 + }, + "upgraded": { + "exhaust": true, + "teamDraw": 3, + "cost": 1, + "description": "所有玩家抽3张牌。消耗。" + } + }, + { + "id": "discovery", + "name": "mengsan_colorless_discovery", + "title": "发现", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "从3张随机牌中选择1张加入你的手牌。这张牌在本回合可以免费打出。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "cost": 1, + "description": "从3张随机牌中选择1张加入你的手牌。这张牌在本回合可以免费打出。" + } + }, + { + "id": "ultimate_defend", + "name": "mengsan_colorless_ultimate_defend", + "title": "究极防御", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "获得11点格挡。", + "base": { + "block": 11 + }, + "upgraded": { + "block": 15, + "cost": 1, + "description": "获得15点格挡。" + } + }, + { + "id": "intercept", + "name": "mengsan_colorless_intercept", + "title": "拦截", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "获得9点格挡。将本回合所有要对另一名玩家发起的攻击转移到你的身上。", + "base": { + "block": 9 + }, + "upgraded": { + "block": 13, + "cost": 1, + "description": "获得13点格挡。将本回合所有要对另一名玩家发起的攻击转移到你的身上。" + } + }, + { + "id": "lift", + "name": "mengsan_colorless_lift", + "title": "托举", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "给另一名玩家11点格挡。", + "base": { + "allyBlock": 11 + }, + "upgraded": { + "allyBlock": 16, + "cost": 1, + "description": "给另一名玩家16点格挡。" + } + }, + { + "id": "coordinate", + "name": "mengsan_colorless_coordinate", + "title": "协同配合", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 1, + "description": "在本回合给予其他玩家5点力量。", + "base": { + "teamStrength": 5 + }, + "upgraded": { + "teamStrength": 8, + "cost": 1, + "description": "在本回合给予其他玩家8点力量。" + } + }, + { + "id": "catastrophe", + "name": "mengsan_colorless_catastrophe", + "title": "横祸", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 2, + "description": "从你的抽牌堆中随机打出2张牌。", + "base": { + "autoDraw": 2 + }, + "upgraded": { + "autoDraw": 3, + "cost": 2, + "description": "从你的抽牌堆中随机打出3张牌。" + } + }, + { + "id": "equilibrium", + "name": "mengsan_colorless_equilibrium", + "title": "均衡", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 2, + "description": "获得13点格挡。在本回合保留你的手牌。", + "base": { + "block": 13 + }, + "upgraded": { + "block": 16, + "cost": 2, + "description": "获得16点格挡。在本回合保留你的手牌。" + } + }, + { + "id": "the_bomb", + "name": "mengsan_colorless_the_bomb", + "title": "炸弹", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 2, + "description": "在3回合结束后,对所有敌人造成40点伤害。", + "base": { + "bombDamage": 40 + }, + "upgraded": { + "bombDamage": 50, + "cost": 2, + "description": "在3回合结束后,对所有敌人造成50点伤害。" + } + }, + { + "id": "shockwave", + "name": "mengsan_colorless_shockwave", + "title": "震荡波", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "uncommon", + "cost": 2, + "description": "给予所有敌人3层虚弱和易伤。消耗。", + "base": { + "exhaust": true, + "weak": 3, + "vulnerable": 3, + "all": true + }, + "upgraded": { + "exhaust": true, + "weak": 5, + "vulnerable": 5, + "all": true, + "cost": 2, + "description": "给予所有敌人5层虚弱和易伤。消耗。" + } + }, + { + "id": "panache", + "name": "mengsan_colorless_panache", + "title": "神气制胜", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "uncommon", + "cost": 0, + "description": "每当你在一回合内打出五张牌时,对所有敌人造成10点伤害。", + "base": { + "panache": 10 + }, + "upgraded": { + "panache": 14, + "cost": 0, + "description": "每当你在一回合内打出五张牌时,对所有敌人造成14点伤害。" + } + }, + { + "id": "prowess", + "name": "mengsan_colorless_prowess", + "title": "非凡技艺", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "uncommon", + "cost": 1, + "description": "获得1点力量。获得1点敏捷。", + "base": { + "strength": 1, + "dexterity": 1 + }, + "upgraded": { + "strength": 2, + "dexterity": 2, + "cost": 1, + "description": "获得2点力量。获得2点敏捷。" + } + }, + { + "id": "fasten", + "name": "mengsan_colorless_fasten", + "title": "勒紧", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "uncommon", + "cost": 1, + "description": "从“防御”牌中额外获得4点格挡。", + "base": { + "defendBonus": 4 + }, + "upgraded": { + "defendBonus": 6, + "cost": 1, + "description": "从“防御”牌中额外获得6点格挡。" + } + }, + { + "id": "prep_time", + "name": "mengsan_colorless_prep_time", + "title": "准备时间", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "uncommon", + "cost": 1, + "description": "在你的回合开始时,获得4点活力。", + "base": { + "turnVigor": 4 + }, + "upgraded": { + "turnVigor": 6, + "cost": 1, + "description": "在你的回合开始时,获得6点活力。" + } + }, + { + "id": "automation", + "name": "mengsan_colorless_automation", + "title": "自动化", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "uncommon", + "cost": 1, + "description": "你每抽10张牌,获得1点能量。", + "base": { + "automation": 1 + }, + "upgraded": { + "automation": 1, + "cost": 0, + "description": "你每抽10张牌,获得1点能量。" + } + }, + { + "id": "bolas", + "name": "mengsan_colorless_bolas", + "title": "流星锤", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 0, + "description": "造成3点伤害。在你的下个回合开始时,将此卡返回你的手牌。", + "base": { + "damage": 3, + "hits": 1 + }, + "upgraded": { + "damage": 4, + "hits": 1, + "cost": 0, + "description": "造成4点伤害。在你的下个回合开始时,将此卡返回你的手牌。" + } + }, + { + "id": "salvo", + "name": "mengsan_colorless_salvo", + "title": "箭雨", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 1, + "description": "造成12点伤害。在本回合保留你的手牌。", + "base": { + "damage": 12, + "hits": 1 + }, + "upgraded": { + "damage": 16, + "hits": 1, + "cost": 1, + "description": "造成16点伤害。在本回合保留你的手牌。" + } + }, + { + "id": "gold_axe", + "name": "mengsan_colorless_gold_axe", + "title": "金斧", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 1, + "description": "造成本场战斗中所打出牌数的伤害。", + "base": { + "hits": 1 + }, + "upgraded": { + "retain": true, + "hits": 1, + "cost": 1, + "description": "保留。造成本场战斗中所打出牌数的伤害。" + } + }, + { + "id": "rend", + "name": "mengsan_colorless_rend", + "title": "撕碎", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 2, + "description": "造成15点伤害。该名敌人身上每有一种负面效果,就额外造成5点伤害。", + "base": { + "damage": 15, + "hits": 1, + "perDebuff": 5 + }, + "upgraded": { + "damage": 18, + "hits": 1, + "perDebuff": 8, + "cost": 2, + "description": "造成18点伤害。该名敌人身上每有一种负面效果,就额外造成8点伤害。" + } + }, + { + "id": "hand_of_greed", + "name": "mengsan_colorless_hand_of_greed", + "title": "贪婪之手", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 2, + "description": "造成20点伤害。斩杀时,获得20金币。", + "base": { + "damage": 20, + "hits": 1, + "killGold": 20 + }, + "upgraded": { + "damage": 25, + "hits": 1, + "killGold": 25, + "cost": 2, + "description": "造成25点伤害。斩杀时,获得25金币。" + } + }, + { + "id": "jackpot", + "name": "mengsan_colorless_jackpot", + "title": "大奖", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 3, + "description": "造成25点伤害。将3张随机0能量的牌加入你的手牌。", + "base": { + "damage": 25, + "hits": 1, + "generateZero": 3 + }, + "upgraded": { + "damage": 30, + "hits": 1, + "generateZero": 3, + "generatedUpgrade": 1, + "cost": 3, + "description": "造成30点伤害。将3张随机升级过的0能量的牌加入你的手牌。" + } + }, + { + "id": "knockdown", + "name": "mengsan_colorless_knockdown", + "title": "击倒", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "rare", + "cost": 3, + "description": "造成10点伤害。该敌人在本回合受到的来自其他玩家的伤害变为两倍。", + "base": { + "damage": 10, + "hits": 1, + "allyDamageMultiplier": 2 + }, + "upgraded": { + "damage": 14, + "hits": 1, + "allyDamageMultiplier": 3, + "cost": 3, + "description": "造成14点伤害。该敌人在本回合受到的来自其他玩家的伤害变为三倍。" + } + }, + { + "id": "the_gambit", + "name": "mengsan_colorless_the_gambit", + "title": "孤注一掷", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 0, + "description": "获得50点格挡。如果你在本场战斗中受到未被格挡的攻击伤害,则立刻死亡。", + "base": { + "block": 50 + }, + "upgraded": { + "block": 75, + "cost": 0, + "description": "获得75点格挡。如果你在本场战斗中受到未被格挡的攻击伤害,则立刻死亡。" + } + }, + { + "id": "secret_technique", + "name": "mengsan_colorless_secret_technique", + "title": "秘密技法", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 0, + "description": "从抽牌堆中选择一张技能牌放入你的手牌。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "cost": 0, + "description": "从抽牌堆中选择一张技能牌放入你的手牌。" + } + }, + { + "id": "secret_weapon", + "name": "mengsan_colorless_secret_weapon", + "title": "秘密武器", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 0, + "description": "从抽牌堆中选择一张攻击牌放入你的手牌。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "cost": 0, + "description": "从抽牌堆中选择一张攻击牌放入你的手牌。" + } + }, + { + "id": "master_of_strategy", + "name": "mengsan_colorless_master_of_strategy", + "title": "战略大师", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 0, + "description": "抽3张牌。消耗。", + "base": { + "exhaust": true, + "draw": 3 + }, + "upgraded": { + "exhaust": true, + "draw": 4, + "cost": 0, + "description": "抽4张牌。消耗。" + } + }, + { + "id": "scrawl", + "name": "mengsan_colorless_scrawl", + "title": "潦草急就", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 1, + "description": "抽牌直到抽满手牌。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "exhaust": true, + "retain": true, + "cost": 1, + "description": "保留。抽牌直到抽满手牌。消耗。" + } + }, + { + "id": "mimic", + "name": "mengsan_colorless_mimic", + "title": "拟态", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 1, + "description": "获得等同于另一位玩家格挡值的格挡。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "cost": 1, + "description": "获得等同于另一位玩家格挡值的格挡。" + } + }, + { + "id": "anointed", + "name": "mengsan_colorless_anointed", + "title": "天选", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 1, + "description": "将你抽牌堆中的所有稀有牌放入你的手牌。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "exhaust": true, + "retain": true, + "cost": 1, + "description": "保留。将你抽牌堆中的所有稀有牌放入你的手牌。消耗。" + } + }, + { + "id": "rally", + "name": "mengsan_colorless_rally", + "title": "集结", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 2, + "description": "所有玩家获得12点格挡。", + "base": { + "teamBlock": 12 + }, + "upgraded": { + "teamBlock": 17, + "cost": 2, + "description": "所有玩家获得17点格挡。" + } + }, + { + "id": "beat_down", + "name": "mengsan_colorless_beat_down", + "title": "狠揍", + "pack": "colorless", + "category": "utility", + "cardType": "skill", + "rarity": "rare", + "cost": 3, + "description": "打出你弃牌堆中的3张随机攻击牌。", + "base": { + "autoDiscard": 3 + }, + "upgraded": { + "autoDiscard": 4, + "cost": 3, + "description": "打出你弃牌堆中的4张随机攻击牌。" + } + }, + { + "id": "nostalgia", + "name": "mengsan_colorless_nostalgia", + "title": "怀旧", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 1, + "description": "每回合首次打出攻击或技能牌时,将其置于你的抽牌堆顶端。", + "base": {}, + "upgraded": { + "cost": 0, + "description": "每回合首次打出攻击或技能牌时,将其置于你的抽牌堆顶端。" + } + }, + { + "id": "entropy", + "name": "mengsan_colorless_entropy", + "title": "熵", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 1, + "description": "在你的回合开始时,变化你手牌中的1张牌。", + "base": {}, + "upgraded": { + "innate": true, + "cost": 1, + "description": "固有。在你的回合开始时,变化你手牌中的1张牌。" + } + }, + { + "id": "beacon_of_hope", + "name": "mengsan_colorless_beacon_of_hope", + "title": "希望灯塔", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 1, + "description": "每当你在你的回合获得格挡时,其他玩家获得相应一半的格挡。", + "base": {}, + "upgraded": { + "innate": true, + "cost": 1, + "description": "固有。每当你在你的回合获得格挡时,其他玩家获得相应一半的格挡。" + } + }, + { + "id": "mayhem", + "name": "mengsan_colorless_mayhem", + "title": "乱战", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 2, + "description": "在你的回合开始时,打出你抽牌堆顶部的牌。", + "base": {}, + "upgraded": { + "cost": 1, + "description": "在你的回合开始时,打出你抽牌堆顶部的牌。" + } + }, + { + "id": "rolling_boulder", + "name": "mengsan_colorless_rolling_boulder", + "title": "滚石", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 3, + "description": "在你的回合开始时,对所有敌人造成5点伤害,然后将该伤害增加5点。", + "base": { + "boulder": 5 + }, + "upgraded": { + "boulder": 10, + "cost": 3, + "description": "在你的回合开始时,对所有敌人造成10点伤害,然后将该伤害增加5点。" + } + }, + { + "id": "calamity", + "name": "mengsan_colorless_calamity", + "title": "劫难", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 3, + "description": "每当你打出一张攻击牌时,将一张随机攻击牌添加到你的手牌。", + "base": {}, + "upgraded": { + "cost": 2, + "description": "每当你打出一张攻击牌时,将一张随机攻击牌添加到你的手牌。" + } + }, + { + "id": "eternal_armor", + "name": "mengsan_colorless_eternal_armor", + "title": "永恒铠甲", + "pack": "colorless", + "category": "utility", + "cardType": "power", + "rarity": "rare", + "cost": 3, + "description": "获得9层覆甲。", + "base": { + "plating": 9 + }, + "upgraded": { + "plating": 12, + "cost": 3, + "description": "获得12层覆甲。" + } + }, + { + "id": "the_ball", + "name": "mengsan_colorless_the_ball", + "title": "魔球", + "pack": "colorless", + "category": "damage", + "cardType": "attack", + "rarity": "uncommon", + "cost": 1, + "description": "造成10点伤害。将这张牌在本场战斗中的伤害增加15,然后将其交给一名随机盟友。", + "base": { + "damage": 10, + "hits": 1, + "ballGrowth": 15 + }, + "upgraded": { + "damage": 10, + "hits": 1, + "ballGrowth": 25, + "cost": 1, + "description": "造成10点伤害。将这张牌在本场战斗中的伤害增加25,然后将其交给一名随机盟友。" + } + } +]); diff --git a/mengsan/cards/packs/colorless/index.js b/mengsan/cards/packs/colorless/index.js new file mode 100644 index 0000000..0b201c3 --- /dev/null +++ b/mengsan/cards/packs/colorless/index.js @@ -0,0 +1,8 @@ +import { createSharedCardRuntime } from "../shared/runtime.js"; +// 四类通用牌分目录保存数据,共用一次战斗控制器,避免重复安装全局事件。 +export class ColorlessCardPack { + static id = "colorless"; + static owner = null; + constructor(context) { this.context = context; } + createRuntime() { return createSharedCardRuntime(this.context); } +} diff --git a/mengsan/cards/packs/colorless/rewards.js b/mengsan/cards/packs/colorless/rewards.js new file mode 100644 index 0000000..184edb2 --- /dev/null +++ b/mengsan/cards/packs/colorless/rewards.js @@ -0,0 +1,8 @@ +import { colorlessCards } from "./data.js"; +import { eventCards } from "../event/data.js"; +import { curseCards } from "../curse/data.js"; +export const colorlessRewards = Object.freeze(Object.fromEntries([...colorlessCards,...eventCards,...curseCards].map(card=>[ + `shared.reward.card.${card.name}`,{card:{name:card.name},name:`获得一张【${card.title}】`,description:card.description} +]))); +// 事件与诅咒只供明确指定的剧情/奖励调用,不混入普通奖励和无色生成池。 +export const colorlessRewardIds = Object.freeze(colorlessCards.map(card=>`shared.reward.card.${card.name}`)); diff --git a/mengsan/cards/packs/curse/data.js b/mengsan/cards/packs/curse/data.js new file mode 100644 index 0000000..ee1216b --- /dev/null +++ b/mengsan/cards/packs/curse/data.js @@ -0,0 +1,268 @@ +// Wiki Data:Card.tabx 保存快照;运行时效果由共用模块实现。 +export const curseCards = Object.freeze([ + { + "id": "spore_mind", + "name": "mengsan_curse_spore_mind", + "title": "孢子心灵", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": 1, + "description": "消耗。", + "base": { + "exhaust": true + } + }, + { + "id": "enthralled", + "name": "mengsan_curse_enthralled", + "title": "执迷", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": 2, + "description": "如果这张牌在你的手牌中,你必须优先打出这张牌。永恒。", + "base": { + "eternal": true + } + }, + { + "id": "clumsy", + "name": "mengsan_curse_clumsy", + "title": "笨拙", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。虚无。", + "base": { + "ethereal": true, + "unplayable": true + } + }, + { + "id": "normality", + "name": "mengsan_curse_normality", + "title": "凡庸", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。你在本回合不能打出超过3张牌。", + "base": { + "unplayable": true + } + }, + { + "id": "decay", + "name": "mengsan_curse_decay", + "title": "腐朽", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中, 你受到2点伤害。", + "base": { + "unplayable": true, + "endDamage": 2 + } + }, + { + "id": "regret", + "name": "mengsan_curse_regret", + "title": "悔恨", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,失去相当于手牌数量的生命。", + "base": { + "unplayable": true + } + }, + { + "id": "ascenders_bane", + "name": "mengsan_curse_ascenders_bane", + "title": "进阶之灾", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。虚无。永恒。", + "base": { + "ethereal": true, + "eternal": true, + "unplayable": true + } + }, + { + "id": "writhe", + "name": "mengsan_curse_writhe", + "title": "苦恼", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。固有。", + "base": { + "innate": true, + "unplayable": true + } + }, + { + "id": "guilty", + "name": "mengsan_curse_guilty", + "title": "愧疚", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在5场战斗后从你的牌组中移除。", + "base": { + "unplayable": true + } + }, + { + "id": "curse_of_the_bell", + "name": "mengsan_curse_curse_of_the_bell", + "title": "铃铛的诅咒", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。永恒。", + "base": { + "eternal": true, + "unplayable": true + } + }, + { + "id": "bad_luck", + "name": "mengsan_curse_bad_luck", + "title": "霉运", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,你失去13点生命。永恒。", + "base": { + "eternal": true, + "unplayable": true, + "endLoseHp": 13 + } + }, + { + "id": "injury", + "name": "mengsan_curse_injury", + "title": "受伤", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。", + "base": { + "unplayable": true + } + }, + { + "id": "poor_sleep", + "name": "mengsan_curse_poor_sleep", + "title": "睡眠不佳", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。保留。", + "base": { + "retain": true, + "unplayable": true + } + }, + { + "id": "greed", + "name": "mengsan_curse_greed", + "title": "贪婪", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。永恒。", + "base": { + "eternal": true, + "unplayable": true + } + }, + { + "id": "shame", + "name": "mengsan_curse_shame", + "title": "羞耻", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,则获得1层脆弱。", + "base": { + "unplayable": true + } + }, + { + "id": "doubt", + "name": "mengsan_curse_doubt", + "title": "疑虑", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,获得1层虚弱。", + "base": { + "unplayable": true + } + }, + { + "id": "folly", + "name": "mengsan_curse_folly", + "title": "愚行", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。固有。虚无。永恒。", + "base": { + "innate": true, + "ethereal": true, + "eternal": true, + "unplayable": true + } + }, + { + "id": "debt", + "name": "mengsan_curse_debt", + "title": "债务", + "pack": "curse", + "category": "utility", + "cardType": "curse", + "rarity": "curse", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,你失去10金币。", + "base": { + "unplayable": true + } + } +]); diff --git a/mengsan/cards/packs/data.js b/mengsan/cards/packs/data.js new file mode 100644 index 0000000..4b935eb --- /dev/null +++ b/mengsan/cards/packs/data.js @@ -0,0 +1,10 @@ +// 纯数据汇总入口:角色效果运行时不参与注册、费用和强化模块的导入。 +import { liubeiDefinitions, liubeiCosts, liubeiUpgradeRules } from "./liubei/metadata.js"; +import { sharedCards } from "./shared/data.js"; +const sharedDefinitions=sharedCards.map(card=>({name:card.name,category:card.category, + cardType:card.cardType,rarity:card.rarity, + owner:null,deck:card.pack,generatedOnly:card.pack==="status"&&card.id!=="deprecated_card"})); +export const cardPackDefinitions=Object.freeze([...liubeiDefinitions,...sharedDefinitions]); +export const cardPackCosts=Object.freeze({...liubeiCosts,...Object.fromEntries(sharedCards.map(card=>[card.name,card.cost]))}); +export const cardPackUpgradeRules=Object.freeze({...liubeiUpgradeRules, + ...Object.fromEntries(sharedCards.filter(card=>card.upgraded).map(card=>[card.name,Object.freeze({maxLevel:1,...card.upgraded})]))}); diff --git a/mengsan/cards/packs/event/data.js b/mengsan/cards/packs/event/data.js new file mode 100644 index 0000000..fc56f27 --- /dev/null +++ b/mengsan/cards/packs/event/data.js @@ -0,0 +1,571 @@ +// Wiki Data:Card.tabx 保存快照;运行时效果由共用模块实现。 +export const eventCards = Object.freeze([ + { + "id": "byrd_swoop", + "name": "mengsan_event_byrd_swoop", + "title": "异鸟扑击", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 0, + "description": "造成14点伤害。", + "base": { + "damage": 14, + "hits": 1 + }, + "upgraded": { + "damage": 18, + "hits": 1, + "cost": 0, + "description": "造成18点伤害。" + } + }, + { + "id": "mad_science-sapping", + "name": "mengsan_event_mad_science_sapping", + "title": "疯狂科学", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "造成12点伤害。给予2层虚弱。给予2层易伤。", + "base": { + "damage": 12, + "hits": 1, + "weak": 2, + "vulnerable": 2 + }, + "upgraded": { + "innate": true, + "damage": 12, + "hits": 1, + "weak": 2, + "vulnerable": 2, + "cost": 1, + "description": "固有。造成12点伤害。给予2层虚弱。给予2层易伤。" + } + }, + { + "id": "mad_science-violence", + "name": "mengsan_event_mad_science_violence", + "title": "疯狂科学", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "造成12点伤害3次。", + "base": { + "damage": 12, + "hits": 3 + }, + "upgraded": { + "innate": true, + "damage": 12, + "hits": 3, + "cost": 1, + "description": "固有。造成12点伤害3次。" + } + }, + { + "id": "mad_science-choking", + "name": "mengsan_event_mad_science_choking", + "title": "疯狂科学", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "造成12点伤害。本回合,你每打出一张牌,该敌人失去6点生命。", + "base": { + "damage": 12, + "hits": 1, + "choking": 6 + }, + "upgraded": { + "innate": true, + "damage": 12, + "hits": 1, + "choking": 6, + "cost": 1, + "description": "固有。造成12点伤害。本回合,你每打出一张牌,该敌人失去6点生命。" + } + }, + { + "id": "exterminate", + "name": "mengsan_event_exterminate", + "title": "杀灭", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "对所有敌人造成3点伤害4次。", + "base": { + "damage": 3, + "hits": 4, + "all": true + }, + "upgraded": { + "damage": 4, + "hits": 4, + "all": true, + "cost": 1, + "description": "对所有敌人造成4点伤害4次。" + } + }, + { + "id": "squash", + "name": "mengsan_event_squash", + "title": "压扁", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "造成10点伤害。给予2层易伤。", + "base": { + "damage": 10, + "hits": 1, + "vulnerable": 2 + }, + "upgraded": { + "damage": 12, + "hits": 1, + "vulnerable": 3, + "cost": 1, + "description": "造成12点伤害。给予3层易伤。" + } + }, + { + "id": "peck", + "name": "mengsan_event_peck", + "title": "啄击", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "event", + "cost": 1, + "description": "造成2点伤害3次。", + "base": { + "damage": 2, + "hits": 3 + }, + "upgraded": { + "damage": 2, + "hits": 4, + "cost": 1, + "description": "造成2点伤害4次。" + } + }, + { + "id": "feeding_frenzy", + "name": "mengsan_event_feeding_frenzy", + "title": "疯狂进食", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 0, + "description": "在本回合内获得5点力量。", + "base": { + "temporaryStrength": 5 + }, + "upgraded": { + "temporaryStrength": 7, + "cost": 0, + "description": "在本回合内获得7点力量。" + } + }, + { + "id": "enlightenment", + "name": "mengsan_event_enlightenment", + "title": "开悟", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 0, + "description": "在这个回合,你当前手牌中所有牌的耗能降低至1。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "exhaust": true, + "cost": 0, + "description": "在这场战斗,你当前手牌中所有牌的耗能降低至1。消耗。" + } + }, + { + "id": "mad_science-energized", + "name": "mengsan_event_mad_science_energized", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 1, + "description": "获得8点格挡。获得2点能量。", + "base": { + "block": 8, + "energy": 2 + }, + "upgraded": { + "innate": true, + "block": 8, + "energy": 2, + "cost": 1, + "description": "固有。获得8点格挡。获得2点能量。" + } + }, + { + "id": "mad_science-wisdom", + "name": "mengsan_event_mad_science_wisdom", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 1, + "description": "获得8点格挡。抽3张牌", + "base": { + "block": 8, + "draw": 3 + }, + "upgraded": { + "innate": true, + "block": 8, + "draw": 3, + "cost": 1, + "description": "固有。获得8点格挡。抽3张牌" + } + }, + { + "id": "mad_science-chaos", + "name": "mengsan_event_mad_science_chaos", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 1, + "description": "获得8点格挡。将一张随机牌放入你的手牌,这张牌在本回合可以免费打出。", + "base": { + "block": 8 + }, + "upgraded": { + "innate": true, + "block": 8, + "cost": 1, + "description": "固有。获得8点格挡。将一张随机牌放入你的手牌,这张牌在本回合可以免费打出。" + } + }, + { + "id": "toric_toughness", + "name": "mengsan_event_toric_toughness", + "title": "坚韧之环", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 2, + "description": "获得5点格挡。在接下来的2个回合开始时,获得5点格挡。", + "base": { + "block": 5, + "futureBlock": 5, + "futureTurns": 2 + }, + "upgraded": { + "block": 7, + "futureBlock": 7, + "futureTurns": 2, + "cost": 2, + "description": "获得7点格挡。在接下来的2个回合开始时,获得7点格挡。" + } + }, + { + "id": "metamorphosis", + "name": "mengsan_event_metamorphosis", + "title": "羽化", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "event", + "cost": 2, + "description": "在你的抽牌堆中加入3张随机攻击牌。它们在本场战斗中可以被免费打出。消耗。", + "base": { + "exhaust": true, + "generateDrawAttacks": 3 + }, + "upgraded": { + "exhaust": true, + "generateDrawAttacks": 5, + "cost": 2, + "description": "在你的抽牌堆中加入5张随机攻击牌。它们在本场战斗中可以被免费打出。消耗。" + } + }, + { + "id": "mad_science-expertise", + "name": "mengsan_event_mad_science_expertise", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "power", + "rarity": "event", + "cost": 1, + "description": "获得2点力量。获得2点敏捷。", + "base": { + "strength": 2, + "dexterity": 2 + }, + "upgraded": { + "innate": true, + "strength": 2, + "dexterity": 2, + "cost": 1, + "description": "固有。获得2点力量。获得2点敏捷。" + } + }, + { + "id": "mad_science-curious", + "name": "mengsan_event_mad_science_curious", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "power", + "rarity": "event", + "cost": 1, + "description": "能力牌的耗能减少1能量。", + "base": { + "powerDiscount": 1 + }, + "upgraded": { + "innate": true, + "powerDiscount": 1, + "cost": 1, + "description": "固有。能力牌的耗能减少1能量。" + } + }, + { + "id": "mad_science-improvement", + "name": "mengsan_event_mad_science_improvement", + "title": "疯狂科学", + "pack": "event", + "category": "utility", + "cardType": "power", + "rarity": "event", + "cost": 1, + "description": "在战斗结束时,升级你牌组中的一张随机牌。", + "base": { + "battleUpgrade": 1 + }, + "upgraded": { + "innate": true, + "battleUpgrade": 1, + "cost": 1, + "description": "固有。在战斗结束时,升级你牌组中的一张随机牌。" + } + }, + { + "id": "neows_fury", + "name": "mengsan_event_neows_fury", + "title": "涅奥之怒", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "ancient", + "cost": 1, + "description": "造成10点伤害。将你弃牌堆中的至多2张牌放入你的手牌。消耗。", + "base": { + "exhaust": true, + "damage": 10, + "hits": 1, + "chooseDiscard": 2 + }, + "upgraded": { + "exhaust": true, + "damage": 14, + "hits": 1, + "chooseDiscard": 3, + "cost": 1, + "description": "造成14点伤害。将你弃牌堆中的至多3张牌放入你的手牌。消耗。" + } + }, + { + "id": "maul", + "name": "mengsan_event_maul", + "title": "撕咬", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "ancient", + "cost": 1, + "description": "造成5点伤害两次。在这场战斗中,将所有“撕咬”牌的伤害增加1。", + "base": { + "damage": 5, + "hits": 2, + "maulGrowth": 1 + }, + "upgraded": { + "damage": 6, + "hits": 2, + "maulGrowth": 2, + "cost": 1, + "description": "造成6点伤害两次。在这场战斗中,将所有“撕咬”牌的伤害增加2。" + } + }, + { + "id": "whistle", + "name": "mengsan_event_whistle", + "title": "吹哨", + "pack": "event", + "category": "damage", + "cardType": "attack", + "rarity": "ancient", + "cost": 3, + "description": "造成33点伤害。击晕该敌人。消耗。", + "base": { + "exhaust": true, + "damage": 33, + "hits": 1 + }, + "upgraded": { + "exhaust": true, + "damage": 44, + "hits": 1, + "cost": 3, + "description": "造成44点伤害。击晕该敌人。消耗。" + } + }, + { + "id": "wish", + "name": "mengsan_event_wish", + "title": "许愿", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 0, + "description": "将你抽牌堆中的一张牌放入你的手牌。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "exhaust": true, + "retain": true, + "cost": 0, + "description": "保留。将你抽牌堆中的一张牌放入你的手牌。消耗。" + } + }, + { + "id": "brightest_flame", + "name": "mengsan_event_brightest_flame", + "title": "至亮之焰", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 0, + "description": "获得2点能量。抽2张牌。失去1点最大生命。", + "base": { + "draw": 2, + "energy": 2 + }, + "upgraded": { + "draw": 3, + "energy": 3, + "cost": 0, + "description": "获得3点能量。抽3张牌。失去1点最大生命。" + } + }, + { + "id": "apparition", + "name": "mengsan_event_apparition", + "title": "灵体", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 1, + "description": "虚无。获得1层无实体。消耗。", + "base": { + "exhaust": true, + "ethereal": true, + "intangible": 1 + }, + "upgraded": { + "exhaust": true, + "intangible": 1, + "cost": 1, + "description": "获得1层无实体。消耗。" + } + }, + { + "id": "apotheosis", + "name": "mengsan_event_apotheosis", + "title": "神化", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 2, + "description": "固有。升级你的全部卡牌。消耗。", + "base": { + "exhaust": true, + "innate": true + }, + "upgraded": { + "exhaust": true, + "innate": true, + "cost": 1, + "description": "固有。升级你的全部卡牌。消耗。" + } + }, + { + "id": "relax", + "name": "mengsan_event_relax", + "title": "放松", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 3, + "description": "获得15点格挡。下个回合,抽2张牌并获得2点能量。消耗。", + "base": { + "exhaust": true, + "block": 15, + "nextDraw": 2, + "nextEnergy": 2 + }, + "upgraded": { + "exhaust": true, + "block": 17, + "nextDraw": 3, + "nextEnergy": 3, + "cost": 3, + "description": "获得17点格挡。下个回合,抽3张牌并获得3点能量。消耗。" + } + }, + { + "id": "abundance", + "name": "mengsan_event_abundance", + "title": "富足", + "pack": "event", + "category": "utility", + "cardType": "skill", + "rarity": "ancient", + "cost": 1, + "description": "从3张能力牌中选择1张加入你的手牌。这张牌在本回合可以免费打出。消耗。", + "base": { + "exhaust": true + }, + "upgraded": { + "exhaust": true, + "generatedUpgrade": 1, + "cost": 1, + "description": "从3张升级过的能力牌中选择1张加入你的手牌。这张牌在本回合可以免费打出。消耗。" + } + } +]); diff --git a/mengsan/cards/packs/index.js b/mengsan/cards/packs/index.js new file mode 100644 index 0000000..ba293a5 --- /dev/null +++ b/mengsan/cards/packs/index.js @@ -0,0 +1,19 @@ +import { LiuBeiCardPack } from "./liubei/index.js"; +import { ColorlessCardPack } from "./colorless/index.js"; + +// 后续角色各有目录和牌组类,仅在此登记,不扩张 mode-cards.js。 +const packTypes = Object.freeze([LiuBeiCardPack,ColorlessCardPack]); +export function createCardPackRuntime(context) { + const result={cards:{},translate:{},skills:{},names:[]}, runtimes=[]; + for(const Pack of packTypes){ + const runtime=new Pack(context).createRuntime();runtimes.push(runtime); + for(const key of ["cards","skills"]){ + for(const name of Object.keys(runtime[key] || {}))if(Object.hasOwn(result[key],name))throw new Error(`重复的牌组定义:${Pack.id}/${name}`); + Object.assign(result[key],runtime[key]); + } + Object.assign(result.translate,runtime.translate);result.names.push(...runtime.names); + } + result.vulnerableBonus=(source,target)=>runtimes.reduce((sum,r)=>sum+(r.vulnerableBonus?.(source,target)||0),0); + result.onDeath=async event=>{for(const runtime of runtimes)await runtime.onDeath?.(event);}; + return result; +} diff --git a/mengsan/cards/packs/liubei/advanced-data.js b/mengsan/cards/packs/liubei/advanced-data.js new file mode 100644 index 0000000..fd9d576 --- /dev/null +++ b/mengsan/cards/packs/liubei/advanced-data.js @@ -0,0 +1,35 @@ +// 2026-10-03 灰机Wiki详情页主表。巨石为原始力量的衍生牌。 +export const ironcladAdvancedEntries = [ + ["relentless_assault", "无情猛攻", 2, {damage:14, nextFreeAttack:true}, {damage:20}], + ["howl_from_beyond", "彼岸咆哮", 3, {damage:16, all:true, exhaustReplay:true}, {damage:21}], + ["stomp", "踩踏", 3, {damage:12, all:true, attackDiscount:true}, {damage:15}], + ["battle_trance", "战斗专注", 0, {draw:3, stopDraw:true}, {draw:4}], + ["infernal_blade", "地狱之刃", 1, {generateAttack:true, exhaust:true}, {cost:0}], + ["drum_of_battle", "战鼓", 1, {draw:2, exhaustEnergy:2}, {exhaustEnergy:3}], + ["eager", "跃跃欲试", 2, {energyPerAttack:true, stopEnergy:true}, {cost:1}], + ["feel_no_pain", "无惧疼痛", 1, {power:"feelNoPain", exhaustBlock:3}, {exhaustBlock:4}], + ["ferocity", "凶恶", 1, {power:"ferocity", vulnerableDraw:1}, {vulnerableDraw:2}], + ["stone_armor", "岩石铠甲", 1, {power:"stoneArmor", plating:4}, {plating:6}], + ["juggle", "杂耍", 1, {power:"juggle", juggle:1}, {innate:true}], + ["stampede", "惊逃", 2, {power:"stampede", endAttack:1}, {cost:1}], + ["feed", "狂宴", 1, {damage:10, feed:3, exhaust:true}, {damage:12, feed:4}], + ["thrash", "痛殴", 1, {damage:4, hits:2, absorbAttack:true}, {damage:6}], + ["bully", "凌虐", 3, {damage:15, negativeStrength:10}, {damage:20, negativeStrength:15}], + ["unleash", "倾泻", "X", {topPlays:true, xBonus:0}, {xBonus:1}], + ["primal_force", "原始力量", 0, {transformAttacks:true}, {boulderUpgrade:1}], + ["twin_strike", "连环拳", 1, {doubleTap:1}, {doubleTap:2}], + ["stoke", "添柴", 1, {stoke:true}, {generatedUpgrade:1}], + ["battle_ready", "好勇斗狠", 1, {power:"battleReady", retrieveAttack:1}, {innate:true}], + ["bodyguard", "肉盾", 1, {power:"bodyguard", bodyguard:1}, {cost:0}], + ["hellraiser", "地狱狂徒", 2, {power:"hellraiser", autoStrike:1}, {cost:1}], + ["dark_embrace", "黑暗之拥", 2, {power:"darkEmbrace", exhaustDraw:1}, {cost:1}], + ["unshakable", "坚定不移", 2, {power:"unshakable", firstBlock:1}, {cost:1}], + ["juggernaut", "势不可当", 2, {power:"juggernaut", blockDamage:6}, {blockDamage:8}], + ["barricade", "壁垒", 3, {power:"barricade", barricade:1}, {cost:2}], + ["dual_wield", "双持", 1, {copies:1}, {copies:2}], + ["corruption", "腐化", 3, {power:"corruption", corruption:1}, {cost:2}], + ["midnight", "午夜", 12, {damage:99, exhaustDiscount:true}, {damage:120}], + ["rally", "群情激愤", 0, {damage:9, partyCopies:true}, {damage:13}], + ["grapple", "擒拿", 1, {damage:7, grapple:5}, {damage:9, grapple:7}], + ["boulder", "巨石", 1, {damage:16, generatedOnly:true}, {damage:20}], +].map(([id,title,cost,base,upgrade]) => [id,title,cost,{...base,advanced:true},{...upgrade}]); diff --git a/mengsan/cards/packs/liubei/advanced.js b/mengsan/cards/packs/liubei/advanced.js new file mode 100644 index 0000000..10aec3e --- /dev/null +++ b/mengsan/cards/packs/liubei/advanced.js @@ -0,0 +1,481 @@ +import { ironcladByName } from "./data.js"; +import { cardDefinitions } from "../../card-definitions.js"; +import { generationPool, legacyIroncladNames } from "../../card-pools.js"; +import { cardUpgradeLevel, cardUpgradeRule, canUpgradeCard } from "../../upgrades.js"; +import { createCardData } from "../../card-data.js"; +import { isStrikeCard } from "../../strike-counter.js"; +import { snapshotRampage, copyRampageGrowth } from "../../rampage.js"; +import { isAttackCard } from "../../../monsters/vine-tangled.js"; +import { authorizeFreeCardUse, takeXCardUse, cardCost } from "../../../battle/combat-rules.js"; +import { bindIroncladHooks } from "../../ironclad-hooks.js"; +import { sharedHooks } from "../../shared-hooks.js"; +import { temporaryStrengthAmount } from "../../temporary-strength.js"; +import { shrinkAttackDamage } from "../../../monsters/shrinker-status.js"; + +const MARK = "mengsan_ic_advanced_shuying"; +const BRIDGE = "mengsan_ic_bridge_shuying"; +const powerKeys = ["exhaustBlock","exhaustDraw","vulnerableDraw","plating","juggle","endAttack", + "retrieveAttack","bodyguard","autoStrike","firstBlock","blockDamage","barricade","corruption"]; +const safe = n => Number.isSafeInteger(n) && n >= 0; +function belongs(event,root) { + const seen=new Set(); + for(let current=event;current&&!seen.has(current);current=current.parent){ + if(current===root)return true; seen.add(current); + } + return false; +} +function nearestUse(event) { + const seen=new Set(); + for(let current=event;current&&!seen.has(current);current=current.parent){ + if(current.name==="useCard")return current; seen.add(current); + } + return null; +} +const originalOf = event => { + const use=nearestUse(event); + return use?.mengsanIcRepeatOriginal_shuying || use?.cards?.find(c=>c.name===use.card?.name) || event.card; +}; + +export function createAdvancedIronclad({game,get,lib,status,getBattle,battleEnergy,random}) { + const states=new WeakMap(), uses=new WeakMap(), hits=new WeakMap(), growth=new WeakMap(); + const seenUses=new WeakSet(), seenBlocks=new WeakSet(), seenDraws=new WeakSet(); + const valid=(player,token)=>Boolean(token && getBattle()===token.battle && token.session.active && + token.battle.session===token.session && token.battle.root===token.root && + token.battle.players.has(player) && player?.isAlive?.()); + const tokenOf=player=>{ + const battle=getBattle(); + if(!battle?.session?.active || !battle.root || !battle.players.has(player) || !player?.isAlive?.())return null; + return {battle,session:battle.session,root:battle.root,run:status.mengsanRun_shuying}; + }; + const pileOf=(player,token)=>player===game.me ? token.battle.personalPiles : token.battle.monsterPiles?.get(player); + const stateOf=player=>{ + const s=states.get(player); return s && valid(player,s.token) ? s : null; + }; + const add=(object,key,value)=>{ + const next=(object[key]||0)+value; + if(!safe(next))throw new RangeError("刘备牌组状态数值越界"); + object[key]=next; + }; + const rule=card=>{ + const spec=ironcladByName[card?.name]; + return spec ? cardUpgradeLevel(card) ? spec.upgraded : spec.base : null; + }; + const skillCard=card=>{ + const r=rule(card); + const info=lib.card[card?.name],definition=cardDefinitions[card?.name]; + if(definition?.cardType)return definition.cardType==="skill"; + return r ? !r.damage && !r.power : !isAttackCard(card) && !info?.mengsanPower_shuying && + (legacyIroncladNames.includes(card?.name) || definition?.deck==="colorless" && + info?.type!=="equip" && info?.type!=="status"); + }; + const refresh=s=>{if(s&&valid(s.player,s.token)){s.token.battle.handUI?.refresh();s.token.battle.pilesUI?.refresh();}}; + function ensure(player,{requiresPile=true}={}) { + const token=tokenOf(player); if(!token)return null; + // 全局钩子也接收公共牌堆友军的事件;他们没有个人消耗牌堆。 + // 被卡牌施加的临时状态可单独登记,不将目标当成个人牌堆使用者。 + if(requiresPile && !pileOf(player,token))return null; + let s=stateOf(player); if(s)return s; + s={player,token,powers:Object.create(null),turn:Object.create(null),queue:[],flushing:false, + autoQueue:[],autoBusy:false,freeCards:new WeakSet(),negativeStrength:0,autoCount:0}; + states.set(player,s); player.addSkill(MARK); + token.session.ownResource(s,()=>{if(states.get(player)===s){states.delete(player);player.removeSkill(MARK);s.queue.length=0;s.autoQueue.length=0;}}); + bindIroncladHooks(player,token.session,{ + valid:()=>valid(player,token), + blocksDraw:()=>Boolean(s.turn.noDraw), blocksEnergy:()=>Boolean(s.turn.noEnergy), + preservesBlock:()=>Boolean(s.powers.barricade), + skillExhausts:card=>Boolean(s.powers.corruption && skillCard(card)), + strengthPenalty:()=>s.negativeStrength, + isFree:card=>{ + const node=card?.cards?.length===1 ? card.cards[0] : card; + return Boolean(s.freeCards.has(node) || s.powers.freeAttack && isAttackCard(card) || s.powers.corruption && skillCard(card)); + }, + cost:(card,cost)=>{ + const r=rule(card); + if(r?.attackDiscount)cost=Math.max(0,cost-(s.turn.attacks||0)); + if(r?.exhaustDiscount)cost=Math.max(0,cost-totalExhaustions(token)); + const node=card?.cards?.length===1 ? card.cards[0] : card; + if(s.freeCards.has(node) || s.powers.freeAttack && isAttackCard(card) || s.powers.corruption && skillCard(card))return 0; + return cost; + }, + queueExhaust:card=>s.queue.push({type:"exhaust",card,energy:rule(card)?.exhaustEnergy||0, + block:s.powers.exhaustBlock||0,draw:s.powers.exhaustDraw||0}), + flushExhaust:()=>flush(s), afterDraw:cards=>afterDraw(s,cards), + snapshotCard:card=>card.name==="mengsan_ic_thrash" ? { + mengsanThrashBonus_shuying:growthOf(card,s),mengsanThrashSession_shuying:token.session.id} : {}, + queueVulnerable:target=>{if(s.powers.vulnerableDraw && valid(target,token))s.queue.push({type:"vulnerable",draw:s.powers.vulnerableDraw});}, + blockAmount:(num,event)=>{ + if(!(num>0) || !s.powers.firstBlock)return num; + let current=event;const visited=new Set(); + while(current&&!visited.has(current)){ + visited.add(current); + // 技能/遗物获得的格挡不借用其外层卡牌的来源。 + if(current.skill || lib.skill[current.name])return num; + if(current.name==="useCard" && current.player===player){ + const uses=s.turn.blockUses ||= new Set(); + if(uses.has(current))return num*2; + if(uses.size{ + if(!list.length)return null; + const value=random(s.token.run); + if(!Number.isFinite(value)||value<0||value>=1)throw new RangeError("战斗随机数无效"); + return list[Math.floor(value*list.length)]; + }; + function rememberGrowth(s,card,bonus) { + if(!safe(bonus))throw new RangeError("痛殴累积伤害越界"); + const entry={token:s.token,bonus};growth.set(card,entry); + s.token.session.ownResource(entry,()=>{if(growth.get(card)===entry)growth.delete(card);}); + } + function growthOf(card,s=null) { + if(card?.cards?.length===1 && card.cards[0].name===card.name)card=card.cards[0]; + const entry=growth.get(card); + return entry?.token.session.active && (!s || entry.token.battle===s.token.battle) ? entry.bonus : + !card?.storage && card?.mengsanThrashSession_shuying===getBattle()?.session.id ? card.mengsanThrashBonus_shuying||0 : 0; + } + async function copyToHand(s,source) { + if(!valid(s.player,s.token))return null; + const pile=pileOf(s.player,s.token);if(!pile)return null; + const copy=await pile.addToHand(generatedData(s,source.name,source)); + if(!copy || !valid(s.player,s.token))return copy; + copyRampageGrowth(source,copy,s.token.battle); + sharedHooks(s.player)?.copyCard?.(source,copy); + if(source.name==="mengsan_ic_thrash")rememberGrowth(s,copy,growthOf(source,s)); + // 实体牌副本继承永久词缀与当前强化,不继承本回合免费、已使用或消耗标记。 + return copy; + } + async function flush(s) { + if(!s || s.flushing)return; + s.flushing=true; + try { + while(s.queue.length && valid(s.player,s.token)){ + const entry=s.queue.shift(); + if(entry.energy)battleEnergy.grant(s.player,entry.energy,s.token.battle); + if(entry.block){await s.player.changeHujia(entry.block);if(!valid(s.player,s.token))break;} + if(entry.draw){await s.player.draw(entry.draw);if(!valid(s.player,s.token))break;} + } + refresh(s); + } finally {s.flushing=false;} + } + async function flushAll() { + const battle=getBattle();if(!battle?.session.active)return; + for(const player of [...battle.players])await flush(stateOf(player)); + } + function legalTargets(s,card) { + const info=lib.card[card.name];if(!info)return []; + return [...s.token.battle.players].filter(target=>valid(target,s.token) && + (typeof info.filterTarget!=="function" || info.filterTarget(card,s.player,target))); + } + async function autoPlay(s,card,{fromExhaust=false,fromDraw=false,echo=null,targets=null}={}) { + if(!valid(s.player,s.token))return false; + const info=lib.card[card.name],pile=pileOf(s.player,s.token); + if((fromExhaust||fromDraw) && !pile)return false; + if(++s.autoCount>1024)throw new Error("自动打牌超过安全结算上限,请检查递归组合"); + const skip=()=>{if(fromDraw && pile.beginAutoPlay(card,false))pile.finishAutoPlay(card);return false;}; + if(!info || info.enable===false || card.storage?.mengsanCard_shuying?.affixes?.includes("unplayable"))return skip(); + let selected=info.notarget ? [] : targets?.filter(target=>valid(target,s.token)) || null; + if(!selected){ + const candidates=legalTargets(s,card); + if(!candidates.length)return skip(); + selected=info.toself ? [s.player] : info.selectTarget===-1 ? candidates : [randomItem(s,candidates)]; + } + if(!selected.length && !info.notarget)return skip(); + if((fromExhaust||fromDraw) && !pile.beginAutoPlay(card,fromExhaust))return false; + let use,release=()=>{}; + try { + const played=echo ? {...echo.card,storage:{mengsanCard_shuying:{...echo.data,affixes:echo.data.affixes.slice()}}} : card; + use=s.player.useCard({card:played,cards:echo?[]:[card],targets:selected,addCount:false}); + if(echo){use.mengsanIcEcho_shuying=true;use.mengsanIcRepeatOriginal_shuying=echo.original;} + use.mengsanIcAuto_shuying=true;use.addCount=false; + release=authorizeFreeCardUse(s.player,use.card,use,()=>valid(s.player,s.token)); + // 在该原生 useCard 成为当前事件时校验,临时免费授权才能匹配正确父链。 + use.oncard=()=>{ + if(!valid(s.player,s.token) || !lib.filter.cardEnabled(use.card,s.player) || + selected.some(target=>!s.player.canUse(use.card,target,false)))use.cancel(); + }; + await use; + return !use.cancelled; + } finally {release();if(fromExhaust||fromDraw)pile.finishAutoPlay(card);} + } + async function afterDraw(s,cards) { + if(!s?.powers.autoStrike || !valid(s.player,s.token))return; + for(const card of cards||[])if(isAttackCard(card) && isStrikeCard(card,lib))s.autoQueue.push(card); + if(s.autoBusy)return; + s.autoBusy=true; + try { + while(s.autoQueue.length && valid(s.player,s.token)){ + const card=s.autoQueue.shift(); + if(s.player.getCards("h").includes(card))await autoPlay(s,card); + } + } finally {s.autoBusy=false;} + } + function baseDamage(card,player,event=null) { + const s=stateOf(player),r=rule(card),level=cardUpgradeLevel(card),upgrade=level?cardUpgradeRule(card.name):null; + if(r){ + let value=r.damage||0; + if(r.perExhaust)value+=r.perExhaust*(s ? pileOf(player,s.token)?.exhaustedCards().length||0 : 0); + if(card.name==="mengsan_ic_thrash")value+=growthOf(card,s); + return value; + } + const values={sha:level?9:6,mengsan_zhongsha:level?10:8,mengsan_fennu:level?8:6, + mengsan_feijianhuixuanbiao:3,mengsan_jianbingdaji:level?10:9,mengsan_rongrongzhiquan:level?14:10, + mengsan_shandianpili:level?7:4,mengsan_shuangchongdaji:level?7:5,mengsan_tiezhanbo:level?7:5, + mengsan_touchui:level?12:9,mengsan_tupo:level?13:9,mengsan_yubeidaji:level?9:7, + mengsan_yujin:level?24:18,mengsan_yuanhen:5,mengsan_xuanfengzhan:level?8:5}; + if(card.name==="mengsan_quanshenzhuangji")return player.hujia||0; + if(card.name==="mengsan_baozou")return 9+(snapshotRampage(card).mengsanRampageBonus_shuying||0); + if(card.name==="mengsan_qiling")return 4; // 无指定目标时,目标易伤项为0。 + if(card.name==="mengsan_wanmeidaji"){ + const all=s ? pileOf(player,s.token)?.battleCards(event?.cards||[])||[] : []; + return 6+all.filter(c=>isStrikeCard(c,lib)).length*(level?3:2); + } + return values[card.name]??upgrade?.damage??0; + } + async function play(spec,event,player) { + const s=ensure(player);if(!s || !belongs(event,s.token.root))return; + const r=cardUpgradeLevel(event.card)?spec.upgraded:spec.base,pile=pileOf(player,s.token); + if(r.damage){ + const source=originalOf(event),amount=r.damage+(r.absorbAttack?growthOf(source,s):0),hitsCount=r.hits||1; + for(let i=0;ivalid(t,s.token)&&player.isEnemyOf(t)) : [event.target]; + for(const target of targets){ + if(!valid(player,s.token))return; + if(!valid(target,s.token)||!player.isEnemyOf(target))continue; + const hit=target.damage(amount,player);hit.card=event.card;hit.mengsanAttack_shuying=true; + if(r.feed)hits.set(hit,{s,amount:r.feed,claimed:false}); + await hit; + } + } + } + if(!valid(player,s.token))return; + if(r.nextFreeAttack)add(s.powers,"freeAttack",1); + if(r.negativeStrength && valid(event.target,s.token)){ + const targetState=ensure(event.target,{requiresPile:false}); + if(targetState)add(targetState,"negativeStrength",r.negativeStrength); + } + if(r.absorbAttack){ + const candidate=randomItem(s,player.getCards("h").filter(c=>isAttackCard(c))); + if(candidate){ + let amount=Math.max(0,baseDamage(candidate,player,event)+(player.storage.mengsanStrength_shuying||0)+temporaryStrengthAmount(player,s.token.battle)-s.negativeStrength); + if(player.storage.mengsanWeak_shuying>0)amount=Math.floor(amount*0.75); + amount=shrinkAttackDamage(amount,player); + if(await pile.exhaustFromHand(candidate)){ + if(valid(player,s.token)){const source=originalOf(event);rememberGrowth(s,source,growthOf(source,s)+amount);} + } + } + } + if(r.draw){await player.draw(r.draw);if(!valid(player,s.token))return;} + if(r.stopDraw)s.turn.noDraw=true; + if(r.energyPerAttack){const count=player.getCards("h").filter(isAttackCard).length;if(count)battleEnergy.grant(player,count,s.token.battle);} + if(r.stopEnergy)s.turn.noEnergy=true; + if(r.doubleTap)add(s.turn,"doubleTap",r.doubleTap); + if(r.grapple){s.turn.grapple||=new Map();if(valid(event.target,s.token))addMap(s.turn.grapple,event.target,r.grapple);} + if(r.generateAttack){ + const name=randomItem(s,generationPool(cardDefinitions,["liubei"],{attacksOnly:true})); + if(name){const card=await pile.addToHand(generatedData(s,name));if(card&&valid(player,s.token))s.freeCards.add(card);} + } + if(r.copies){ + const candidates=player.getCards("h").filter(c=>isAttackCard(c)|| + (cardDefinitions[c.name]?.cardType ? cardDefinitions[c.name].cardType==="power" : lib.card[c.name]?.mengsanPower_shuying)); + let chosen=candidates[0]; + if(candidates.length>1){const result=await player.chooseButton({createDialog:["双持:选择一张攻击或能力牌",candidates],forced:true,selectButton:1,ai:button=>get.value(button.link,player)}).forResult();chosen=result.links?.[0];} + if(valid(player,s.token)&&candidates.includes(chosen)&&player.getCards("h").includes(chosen))for(let i=0;irule(c)?.exhaustReplay); + for(const card of howls){if(!valid(player,s.token))return;await autoPlay(s,card,{fromExhaust:true});} + for(let i=0;i<(s.powers.endAttack||0)&&valid(player,s.token);i++){ + const card=randomItem(s,player.getCards("h").filter(isAttackCard));if(!card)break;await autoPlay(s,card); + } + } + async function afterBlock(event) { + if(seenBlocks.has(event)||!(event.num>0)||event.type==="damage")return;seenBlocks.add(event); + const s=stateOf(event.player);if(!s)return; + if(s.powers.blockDamage){const target=randomItem(s,[...s.token.battle.players].filter(t=>valid(t,s.token)&&s.player.isEnemyOf(t))); + if(target){const damage=target.damage(s.powers.blockDamage,s.player,"nocard");damage.mengsanScriptedSkill_shuying=true;await damage;}} + for(const [target,amount] of s.turn.grapple||[]){ + if(!valid(s.player,s.token))return; + if(valid(target,s.token)){const damage=target.damage(amount,s.player,"nocard");damage.mengsanScriptedSkill_shuying=true;await damage;} + } + } + async function onDeath(event) { + let current=event;const visited=new Set(); + while(current&&!visited.has(current)){ + visited.add(current);const record=hits.get(current); + if(record){ + const {s,amount}=record; + if(record.claimed || !valid(s.player,s.token) || status.mengsanRun_shuying!==s.token.run)return; + record.claimed=true; + await s.player.gainMaxHp(amount); + if(s.player===game.me && status.mengsanRun_shuying===s.token.run){ + s.token.run.player.maxHp=s.player.maxHp;s.token.run.player.hp=s.player.hp; + await game.mengsanPersistRun_shuying(s.token.run); + } + return; + } + current=current.parent; + } + } + const skills={ + // 本场内部状态技能:保留触发与清理,不在武将状态栏显示。 + [MARK]:{charlotte:true,mark:false, + trigger:{source:"damageBegin1"},forced:true,silent:true,popup:false,priority:82, + filter(event,player){return Boolean(player!==game.me && !event.mengsanScriptedSkill_shuying && stateOf(player)?.negativeStrength && (event.mengsanAttack_shuying||isAttackCard(event.card)));}, + async content(event,trigger,player){trigger.num=Math.max(0,trigger.num-stateOf(player).negativeStrength);}}, + [BRIDGE]:{trigger:{global:["useCard1","useCardAfter","cardsDiscardAfter","loseAfter","phaseBefore","phaseJieshuBegin","phaseAfter","drawAfter","changeHujiaAfter","damageBegin2"]}, + forced:true,silent:true,popup:false,priority:60, + filter(event){const battle=getBattle();return Boolean(battle?.session.active&&belongs(event,battle.root));}, + async content(event,trigger){ + const name=event.triggername; + if(name==="useCard1")await onUse(trigger); + else if(name==="useCardAfter")await onUseAfter(trigger); + else if(name==="phaseBefore")await turnBegin(trigger.player); + else if(name==="phaseJieshuBegin")await turnEnd(trigger.player); + else if(name==="phaseAfter"){ + for(const player of getBattle().players){const s=stateOf(player);if(s){s.negativeStrength=0;s.freeCards=new WeakSet();s.turn=Object.create(null);refresh(s);}} + } else if(name==="drawAfter"){ + if(!seenDraws.has(trigger)&&!trigger.mengsanIcDrawHandled_shuying){seenDraws.add(trigger);await afterDraw(ensure(trigger.player),trigger.result?.cards||[]);} + } else if(name==="changeHujiaAfter")await afterBlock(trigger); + else if(name==="damageBegin2" && (trigger.mengsanAttack_shuying||isAttackCard(trigger.card))){ + let multiplier=1; + for(const holder of getBattle().players){const s=stateOf(holder);if(s?.powers.bodyguard&&valid(holder,s.token)&&holder.isFriendOf(trigger.player)&&holder.isEnemyOf(trigger.source)) + multiplier*=holder===trigger.player?2:0.5;} + if(multiplier!==1)trigger.num=Math.floor(trigger.num*multiplier); + } else await flushAll(); + }}, + }; + const describe=(spec,card)=>{ + const r=cardUpgradeLevel(card)?spec.upgraded:spec.base,parts=[]; + if(r.damage)parts.push(`对${r.all?"所有敌人":"一名敌人"}造成${r.damage+(r.absorbAttack?growthOf(card):0)}点伤害${r.hits?`${r.hits}次`:""}。`); + if(r.nextFreeAttack)parts.push("下一张攻击牌费用变为0。"); + if(r.exhaustReplay)parts.push("回合结束时,此牌在消耗牌堆中则免费打出。"); + if(r.attackDiscount)parts.push("本回合每打出一张攻击牌,费用减少1。"); + if(r.draw)parts.push(`抽${r.draw}张牌。`); + if(r.stopDraw)parts.push("本回合不能再抽牌。"); + if(r.generateAttack)parts.push("随机生成一张铁甲战士攻击牌至手牌,本回合费用为0。"); + if(r.exhaustEnergy)parts.push(`被消耗时获得${r.exhaustEnergy}费用。`); + if(r.energyPerAttack)parts.push("每张攻击手牌获得1费用。"); + if(r.stopEnergy)parts.push("本回合不能再获得费用。"); + if(r.exhaustBlock)parts.push(`每张牌被消耗时获得${r.exhaustBlock}点格挡。`); + if(r.exhaustDraw)parts.push(`每张牌被消耗时抽${r.exhaustDraw}张牌。`); + if(r.vulnerableDraw)parts.push(`每次成功施加易伤抽${r.vulnerableDraw}张牌。`); + if(r.plating)parts.push(`获得${r.plating}层覆甲:回合末获得等量格挡,下个自身回合开始减少1层。`); + if(r.juggle)parts.push("每个自身回合的第三张攻击牌复制一张至手牌。"); + if(r.innate)parts.push("固有。"); + if(r.endAttack)parts.push("回合结束时,随机免费打出一张攻击手牌,目标随机。"); + if(r.feed)parts.push(`斩杀敌人时永久增加${r.feed}点最大生命。`); + if(r.absorbAttack)parts.push("消耗随机一张攻击手牌,将其当前伤害(计入你的力量及攻击减益)累积给此牌,本场战斗保留。"); + if(r.negativeStrength)parts.push(`目标本回合失去${r.negativeStrength}点力量。`); + if(r.topPlays)parts.push(`免费打出抽牌堆顶的X${r.xBonus?"+1":""}张牌。`); + if(r.transformAttacks)parts.push(`所有攻击手牌变为巨石${r.boulderUpgrade?"+":""},仅本场战斗。`); + if(r.doubleTap)parts.push(`本回合接下来的${r.doubleTap}张攻击牌额外打出一次。`); + if(r.stoke)parts.push(`消耗所有手牌,每张随机生成一张${r.generatedUpgrade?"已强化":""}铁甲战士或无色牌至手牌。`); + if(r.retrieveAttack)parts.push("自身回合开始,从弃牌堆随机获得一张攻击牌并强化。"); + if(r.bodyguard)parts.push("你承受敌人双倍伤害,敌人对其他友方的伤害减半。"); + if(r.autoStrike)parts.push("抽到【杀】或名称含“打击”的攻击牌时,免费向随机敌人打出。"); + if(r.firstBlock)parts.push("每个自身回合首张获得格挡的卡牌,其获得的格挡翻倍;叠加时增加可翻倍的牌数。"); + if(r.blockDamage)parts.push(`每次获得格挡,对随机敌人造成${r.blockDamage}点非攻击伤害。`); + if(r.barricade)parts.push("自身回合开始时不清空格挡。"); + if(r.copies)parts.push(`选择一张攻击或能力手牌,复制${r.copies}张至手牌。`); + if(r.corruption)parts.push("技能牌费用变为0,打出后被消耗。"); + if(r.exhaustDiscount)parts.push("本场任意角色每消耗一张牌,费用减少1。"); + if(r.partyCopies)parts.push("在所有存活角色的个人弃牌堆加入此牌副本。"); + if(r.grapple)parts.push(`本回合每次获得格挡,对此目标造成${r.grapple}点非攻击伤害。`); + if(r.exhaust)parts.push("消耗。"); + if(r.power)parts.push("能力持续本场战斗,打出后退出牌堆。"); + return `梦三:主动使用消耗${spec.cost==="X"?"X":cardCost(card||{name:spec.name})}费用。`+parts.join(""); + }; + return {ensure,play,describe,skills,flushAll,baseDamage,onDeath}; +} diff --git a/mengsan/cards/packs/liubei/data.js b/mengsan/cards/packs/liubei/data.js new file mode 100644 index 0000000..668ef54 --- /dev/null +++ b/mengsan/cards/packs/liubei/data.js @@ -0,0 +1,52 @@ +// 2026-10-03 灰机Wiki铁甲战士详情页主表;所有值均为当前状态。 +// 纯数据模块可被费用、升级、注册表及运行时共同导入,不依赖引擎。 +import { ironcladAdvancedEntries } from "./advanced-data.js"; +import { liubeiCardTraits } from "./traits.js"; +const entries = [ + ["dismantle", "拆卸", 1, { damage: 8, vulnerableHits: 2 }, { damage: 10 }], + ["ashen_strike", "灰烬打击", 1, { damage: 6, perExhaust: 3 }, { perExhaust: 4 }], + ["pillage", "劫掠", 1, { damage: 6, drawUntilSkill: true }, { damage: 9 }], + ["hemokinesis", "御血术", 1, { damage: 15, loseHp: 2 }, { damage: 20 }], + ["uppercut", "上勾拳", 2, { damage: 13, weak: 1, vulnerable: 1 }, { weak: 2, vulnerable: 2 }], + ["fight_me", "与我一战!", 2, { damage: 5, hits: 2, strength: 3, enemyStrength: 1 }, { damage: 6, strength: 4 }], + ["bludgeon", "重锤", 3, { damage: 32 }, { damage: 42 }], + ["demonic_shield", "恶魔护盾", 0, { loseHp: 1, allyBlock: true, exhaust: true }, { exhaust: false }], + ["forgotten_ritual", "被遗忘的仪式", 1, { exhaustTurnEnergy: 3, exhaust: true }, { exhaustTurnEnergy: 4 }], + ["burning_pact", "燃烧契约", 1, { chooseExhaust: true, draw: 2 }, { draw: 3 }], + ["taunt", "挑衅", 1, { block: 7, vulnerable: 1 }, { block: 8, vulnerable: 2 }], + ["evil_eye", "邪眼", 1, { block: 8, exhaustTurnBlock: true }, { block: 11 }], + ["second_wind", "重振精神", 1, { exhaustNonAttacks: true, blockPerExhaust: 5 }, { blockPerExhaust: 7 }], + ["dominate", "主宰", 1, { vulnerable: 1, strengthFromVulnerable: true, exhaust: true }, { vulnerable: 2 }], + ["pacts_end", "契约终结", 0, { damage: 17, all: true, minExhaust: 3 }, { damage: 23 }], + ["conflagration", "焚烧", 1, { damage: 2, all: true, hits: 4 }, { hits: 5 }], + ["fiend_fire", "恶魔之焰", 2, { damage: 7, hitsPerExhaust: true, exhaust: true }, { damage: 10 }], + ["offering", "祭品", 0, { loseHp: 6, energy: 2, draw: 3, exhaust: true }, { draw: 5 }], + ["brand", "烙印", 0, { loseHp: 1, chooseExhaust: true, strength: 1 }, { strength: 2 }], + ["impervious", "岿然不动", 2, { block: 30, exhaust: true }, { block: 40 }], + ["not_yet", "时候未到", 2, { heal: 10, exhaust: true }, { heal: 13 }], + ["clash", "交锋", 0, { damage: 14, onlyAttacks: true }, { damage: 18 }], + ["entrench", "巩固", 2, { doubleBlock: true }, { cost: 1 }], + ["break", "破击", 1, { damage: 20, vulnerable: 5 }, { damage: 30, vulnerable: 7 }], + ["blaze", "炽焰", 2, { allyStrength: 5 }, { allyStrength: 7 }], + ["inflame", "燃烧", 1, { power: "inflame", strength: 2 }, { strength: 3 }], + ["demon_form", "恶魔形态", 3, { power: "demonForm", turnStrength: 2 }, { turnStrength: 3 }], + ["pyre", "薪火之源", 2, { power: "pyre", turnEnergy: 1 }, { turnEnergy: 2 }], + ["crimson_mantle", "绯红披风", 1, { power: "crimsonMantle", turnLoseHp: 1, turnBlock: 8 }, { turnBlock: 10 }], + ["rage", "狂怒", 0, { turnPower: "rage", attackBlock: 3 }, { attackBlock: 5 }], + ["rupture", "撕裂", 1, { power: "rupture", lossStrength: 1 }, { lossStrength: 2 }], + ["flame_barrier", "火焰屏障", 2, { block: 12, turnPower: "flameBarrier", retaliation: 4 }, { block: 16, retaliation: 6 }], + ["colossus", "巨像", 1, { block: 5, turnPower: "colossus", vulnerableReduction: 0.5 }, { block: 8 }], + ["cruelty", "残酷", 1, { power: "cruelty", vulnerableBonus: 0.25 }, { vulnerableBonus: 0.5 }], + ["inferno", "狱火", 1, { power: "inferno", infernoLoseHp: 1, lossDamage: 6 }, { lossDamage: 9 }], + ["tear_asunder", "扯碎", 2, { damage: 5, hitsPerHpLoss: true }, { damage: 7 }], + ...ironcladAdvancedEntries, +]; + +export const ironcladCards = Object.freeze(entries.map(([id, title, cost, base, upgraded]) => + Object.freeze({ id, title, name: `mengsan_ic_${id}`, cost, ...liubeiCardTraits[`mengsan_ic_${id}`], + base: Object.freeze(base), upgraded: Object.freeze({ ...base, ...upgraded }), + category: base.damage ? "damage" : base.heal ? "recovery" : "utility", + wiki: `https://sts2.huijiwiki.com/wiki/${encodeURIComponent(id === "corruption" ? "腐化(卡牌)" : title)}` }))); +export const ironcladByName = Object.freeze(Object.fromEntries(ironcladCards.map(c => [c.name, c]))); +export const ironcladUpgradeRules = Object.freeze(Object.fromEntries(ironcladCards.map(c => + [c.name, Object.freeze({ maxLevel: 1, ...c.upgraded })]))); diff --git a/mengsan/cards/packs/liubei/index.js b/mengsan/cards/packs/liubei/index.js new file mode 100644 index 0000000..c746c08 --- /dev/null +++ b/mengsan/cards/packs/liubei/index.js @@ -0,0 +1,15 @@ +import { createIroncladRuntime } from "./runtime.js"; +import { createLegacyLiuBeiCards } from "./legacy.js"; + +export class LiuBeiCardPack { + static id = "liubei"; + static owner = "mengsan_liubei_shuying"; + constructor(context) { this.context = context; } + createRuntime() { + const legacy = createLegacyLiuBeiCards(this.context); + const current = createIroncladRuntime(this.context); + return { ...current, cards:{...legacy.cards,...current.cards}, + translate:{...legacy.translate,...current.translate}, + names:[...legacy.names,...current.names] }; + } +} diff --git a/mengsan/cards/packs/liubei/legacy.js b/mengsan/cards/packs/liubei/legacy.js new file mode 100644 index 0000000..4e81f1f --- /dev/null +++ b/mengsan/cards/packs/liubei/legacy.js @@ -0,0 +1,910 @@ +// 刘备牌组沿用的 25 张铁甲牌及兼容重杀;保留原有卡名与效果接口。 +import standard from "../../../../../../card/standard.js"; +import { _status, game, get } from "../../../../../../noname.js"; +import { cardCost, canPayCard, isActiveCardUse } from "../../../battle/combat-rules.js"; +import { cardUpgradeLevel, cardUpgradeRule } from "../../upgrades.js"; +import { applyMengsanDebuff } from "../../../monsters/artifact-status.js"; +import { isAttackCard } from "../../../monsters/vine-tangled.js"; + +export function createLegacyLiuBeiCards({ copyToDiscard, reclaimFromDiscard, temporaryStrength, countStrikeCards, randomHandExhaust, battleEnergy, handExhaust, playDrawTop, blockDraw, blockUpgrade, lifeLossBlock, whirlwind, spite, rampage, isBattleActive = () => true } = {}) { + const card = {}, translate = {}, nativeSha = standard.card.sha; + card.mengsan_zhongsha = { + type: "basic", + fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_zhongsha.png", + mengsanCost_shuying: cardCost({ name: "mengsan_zhongsha" }), + enable(used, player, event) { + return !isActiveCardUse(event || _status.event, player) || + canPayCard(player, used); + }, + usable: Infinity, + selectTarget: 1, + range: nativeSha.range, + filterTarget: nativeSha.filterTarget, + async content(event, trigger, player) { + const target = event.target; + const effect = cardUpgradeLevel(event.card) + ? cardUpgradeRule("mengsan_zhongsha") + : { damage: 8, vulnerable: 2 }; + const damage = target.damage(effect.damage, player); + damage.mengsanAttack_shuying = true; + await damage; + if (!target.isAlive()) return; + if (applyMengsanDebuff(target, "vulnerable", effect.vulnerable, player)) + game.log(target, `获得${effect.vulnerable}层易伤`); + }, + ai: { order: 4, result: { target: -1.5 }, + tag: { damage: 1 } }, + cardPrompt(used) { + const effect = cardUpgradeLevel(used) + ? cardUpgradeRule("mengsan_zhongsha") + : { damage: 8, vulnerable: 2 }; + return "梦三:主动使用消耗2费用。对攻击范围内的" + + `一名其他角色造成${effect.damage}点伤害,然后给予其` + + `${effect.vulnerable}层易伤。受到攻击伤害增加50%` + + "(向上取整),每个自身回合结束减少1层。" + + "每回合使用次数不限。"; + }, + }; + translate.mengsan_zhongsha = "重杀"; + translate.mengsan_zhongsha_info = + "梦三:主动使用消耗2费用。出牌阶段,对攻击范围内的" + + "一名其他角色造成8点伤害,然后给予其2层易伤。" + + "易伤使受到的攻击伤害增加50%(向上取整)," + + "每个自身回合结束减少1层。每回合使用次数不限。"; + // STS2 Setup Strike: the new temporary strength does not enhance its own hit. + // STS2 Cinder: damage first, then exhaust one random remaining hand card. + card.mengsan_yujin = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_yujin.png", + mengsanCost_shuying: cardCost({ name: "mengsan_yujin" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + if (!randomHandExhaust?.capture || !randomHandExhaust?.exhaust) throw new Error("余烬缺少随机消耗接口"); + const battle = randomHandExhaust.capture(); + if (!battle) return; + const rule = cardUpgradeRule("mengsan_yujin"); + const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player); + damage.mengsanAttack_shuying = true; + await damage; + if (isBattleActive() && player.isAlive()) await randomHandExhaust.exhaust(player, event, battle); + }, + ai: { order: 6, result: { target: -2 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_yujin"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return `梦三:主动使用消耗2费用。对一名敌方角色造成${amount}点伤害,然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。每回合使用次数不限。`; + }, + }; + translate.mengsan_yujin = "余烬"; + translate.mengsan_yujin_info = card.mengsan_yujin.cardPrompt({ name: "mengsan_yujin" }); + // STS2 Perfected Strike counts current physical combat cards, including itself/exhaust. + card.mengsan_wanmeidaji = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_wanmeidaji.png", + mengsanCost_shuying: cardCost({ name: "mengsan_wanmeidaji" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + if (typeof countStrikeCards !== "function") throw new Error("完美打击缺少打击牌计数接口"); + const count = countStrikeCards(player, event); + if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("打击牌数量无效"); + const rule = cardUpgradeRule("mengsan_wanmeidaji"); + const amount = rule.baseDamage + count * (cardUpgradeLevel(event.card) ? rule.perStrike : rule.basePerStrike); + if (!Number.isSafeInteger(amount) || amount < 0) throw new RangeError("完美打击伤害无效"); + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + damage.mengsanStrikeCount_shuying = count; + await damage; + }, + ai: { order: 7, result: { target: -2 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_wanmeidaji"); + const bonus = cardUpgradeLevel(used) ? rule.perStrike : rule.basePerStrike; + return `梦三:主动使用消耗2费用。对一名敌方角色造成6点伤害,本场战斗的牌中每有一张【杀】或名称含“打击”的牌,伤害增加${bonus}点(包括手牌、个人抽牌堆、个人弃牌堆、消耗牌及打出区)。每回合使用次数不限。`; + }, + }; + translate.mengsan_wanmeidaji = "完美打击"; + translate.mengsan_wanmeidaji_info = card.mengsan_wanmeidaji.cardPrompt({ name: "mengsan_wanmeidaji" }); + card.mengsan_yubeidaji = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_yubeidaji.png", + mengsanCost_shuying: cardCost({ name: "mengsan_yubeidaji" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + if (!temporaryStrength?.capture || !temporaryStrength?.grant) throw new Error("预备打击缺少临时力量接口"); + const battle = temporaryStrength.capture(); + if (!battle) return; + const rule = cardUpgradeRule("mengsan_yubeidaji"); + const upgraded = cardUpgradeLevel(event.card); + const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player); + damage.mengsanAttack_shuying = true; + await damage; + if (isBattleActive() && player.isAlive()) { + temporaryStrength.grant(player, upgraded ? rule.strength : rule.baseStrength, event, battle); + } + }, + ai: { order: 8, result: { player: 0.8, target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_yubeidaji"); + const upgraded = cardUpgradeLevel(used); + return `梦三:主动使用消耗1费用。对一名敌方角色造成${upgraded ? rule.damage : rule.baseDamage}点伤害,然后在本回合内获得${upgraded ? rule.strength : rule.baseStrength}点力量。每回合使用次数不限。`; + }, + }; + translate.mengsan_yubeidaji = "预备打击"; + translate.mengsan_yubeidaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成7点伤害,然后在本回合内获得2点力量。每回合使用次数不限。"; + // STS2 Breakthrough: native life loss is paid once before the all-enemy attack. + card.mengsan_tupo = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_tupo.png", + mengsanCost_shuying: cardCost({ name: "mengsan_tupo" }), + notarget: true, usable: Infinity, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + if (!game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)).length) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + async content(event, trigger, player) { + if (!isBattleActive() || !player.isAlive()) return; + if (!game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)).length) return; + // loseHp bypasses block and awaits the normal dying/rescue process. + await player.loseHp(1); + if (!isBattleActive() || !player.isAlive()) return; + const rule = cardUpgradeRule("mengsan_tupo"); + const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; + const targets = game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)); + for (const target of targets) { + if (!isBattleActive() || !player.isAlive()) break; + if (!target.isAlive() || !player.isEnemyOf(target)) continue; + player.line(target, "green"); + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + } + }, + ai: { order: 7, result: { player(player) { return player.hp <= 1 ? -10 : 1; } }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_tupo"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return `梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成${amount}点伤害。每回合使用次数不限。`; + }, + }; + translate.mengsan_tupo = "突破"; + translate.mengsan_tupo_info = "梦三:主动使用消耗1费用。失去1点生命,然后对所有敌方角色造成9点伤害。每回合使用次数不限。"; + // STS2 Headbutt: damage first, then choose from the actor's own real discard pile. + card.mengsan_touchui = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_touchui.png", + mengsanCost_shuying: cardCost({ name: "mengsan_touchui" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + if (typeof reclaimFromDiscard !== "function") throw new Error("头槌缺少个人弃牌回收接口"); + const rule = cardUpgradeRule("mengsan_touchui"); + const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + // Defeating one enemy does not prevent recovery unless combat actually ends. + if (isBattleActive() && player.isAlive()) await reclaimFromDiscard(player); + }, + ai: { order: 6, result: { player: 0.4, target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_touchui"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return '梦三:主动使用消耗1费用。对一名敌方角色造成' + amount + + '点伤害,然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。'; + }, + }; + translate.mengsan_touchui = "头槌"; + translate.mengsan_touchui_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害," + + "然后选择个人弃牌堆中的一张牌放到个人抽牌堆顶部。每回合使用次数不限。"; + // STS2 Iron Wave: obtain block before resolving the attack. + card.mengsan_tiezhanbo = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_tiezhanbo.png", + mengsanCost_shuying: cardCost({ name: "mengsan_tiezhanbo" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + const canResolve = () => isBattleActive() && player.isAlive() && + target?.isAlive() && target !== player && player.isEnemyOf(target); + if (!canResolve()) return; + const rule = cardUpgradeRule("mengsan_tiezhanbo"); + const upgraded = cardUpgradeLevel(event.card) > 0; + await player.changeHujia(upgraded ? rule.block : rule.baseBlock); + // Block triggers may end combat or remove the acting player/target. + if (!canResolve()) return; + const damage = target.damage(upgraded ? rule.damage : rule.baseDamage, player); + damage.mengsanAttack_shuying = true; + await damage; + }, + ai: { order: 7, result: { player: 0.6, target: -1.2 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_tiezhanbo"); + const upgraded = cardUpgradeLevel(used) > 0; + const block = upgraded ? rule.block : rule.baseBlock; + const damage = upgraded ? rule.damage : rule.baseDamage; + return '梦三:主动使用消耗1费用。获得' + block + + '点格挡(护甲),然后对一名敌方角色造成' + damage + + '点伤害。每回合使用次数不限。'; + }, + }; + translate.mengsan_tiezhanbo = "铁斩波"; + translate.mengsan_tiezhanbo_info = "梦三:主动使用消耗1费用。获得5点格挡(护甲)," + + "然后对一名敌方角色造成5点伤害。每回合使用次数不限。"; + // STS2 Twin Strike: each hit resolves on the same target separately. + card.mengsan_shuangchongdaji = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_shuangchongdaji.png", + mengsanCost_shuying: cardCost({ name: "mengsan_shuangchongdaji" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + const rule = cardUpgradeRule("mengsan_shuangchongdaji"); + const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; + for (let hitIndex = 0; hitIndex < rule.hits; hitIndex++) { + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) break; + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + } + }, + ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_shuangchongdaji"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return '梦三:主动使用消耗1费用。对一名敌方角色造成' + + amount + '点伤害两次,两次伤害分别结算。每回合使用次数不限。'; + }, + }; + translate.mengsan_shuangchongdaji = "双重打击"; + translate.mengsan_shuangchongdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成5点伤害两次," + + "两次伤害分别结算。每回合使用次数不限。"; + // STS2 Thunderclap: hit each living enemy before adding new vulnerable. + card.mengsan_shandianpili = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_shandianpili.png", + mengsanCost_shuying: cardCost({ name: "mengsan_shandianpili" }), + notarget: true, usable: Infinity, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + if (!game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)).length) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + async content(event, trigger, player) { + if (!isBattleActive() || !player.isAlive()) return; + const rule = cardUpgradeRule("mengsan_shandianpili"); + const amount = cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage; + const targets = game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)); + for (const target of targets) { + if (!isBattleActive() || !player.isAlive()) break; + if (!target.isAlive() || !player.isEnemyOf(target)) continue; + player.line(target, "green"); + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + } + // 伤害结束后重新取得存活敌人,包含异蛙亡语刚生成的扭动虫。 + for (const target of game.filterPlayer(target => target !== player && target.isAlive() && player.isEnemyOf(target))) { + if (!isBattleActive() || !player.isAlive()) break; + if (applyMengsanDebuff(target, "vulnerable", rule.vulnerable, player)) + game.log(target, `获得${rule.vulnerable}层易伤`); + } + }, + ai: { order: 7, result: { player: 1 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_shandianpili"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return `梦三:主动使用消耗1费用。对所有敌方角色造成${amount}点伤害,然后给予其1层易伤。` + + "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。"; + }, + }; + translate.mengsan_shandianpili = "闪电霹雳"; + translate.mengsan_shandianpili_info = "梦三:主动使用消耗1费用。对所有敌方角色造成4点伤害,然后给予其1层易伤。" + + "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。每回合使用次数不限。"; + // Each throw resolves separately and samples the still-living enemies again. + card.mengsan_feijianhuixuanbiao = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_feijianhuixuanbiao.png", + mengsanCost_shuying: cardCost({ name: "mengsan_feijianhuixuanbiao" }), + notarget: true, usable: Infinity, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + if (!game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)).length) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + async content(event, trigger, player) { + const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao"); + const count = cardUpgradeLevel(event.card) ? rule.hits : rule.baseHits; + for (let hitIndex = 0; hitIndex < count; hitIndex++) { + if (!isBattleActive() || !player.isAlive()) break; + const enemies = game.filterPlayer(target => target !== player && + target.isAlive() && player.isEnemyOf(target)); + if (!enemies.length) break; + // Use the engine helper with an inclusive index range. + const target = enemies[get.rand(0, enemies.length - 1)]; + player.line(target, "green"); + const damage = target.damage(rule.damage, player); + damage.mengsanAttack_shuying = true; + await damage; + } + }, + ai: { order: 6, result: { player: 1 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_feijianhuixuanbiao"); + const count = cardUpgradeLevel(used) ? rule.hits : rule.baseHits; + return `梦三:主动使用消耗1费用。每次对随机一名敌方角色造成${rule.damage}点伤害,共${count}次。` + + "每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。" + + "每回合使用次数不限。"; + }, + }; + translate.mengsan_feijianhuixuanbiao = "飞剑回旋镖"; + translate.mengsan_feijianhuixuanbiao_info = "梦三:主动使用消耗1费用。随机对敌人造成3点伤害3次。" + + "每次攻击重新随机选择存活的敌人,可以重复命中同一敌人。每回合使用次数不限。"; + // Resolve damage before drawing from the acting player's mode-owned pile. + card.mengsan_jianbingdaji = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_jianbingdaji.png", + mengsanCost_shuying: cardCost({ name: "mengsan_jianbingdaji" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (!isBattleActive() || !player.isAlive()) return; + const rule = cardUpgradeRule("mengsan_jianbingdaji"); + const upgraded = cardUpgradeLevel(event.card) > 0; + const damage = event.target.damage(upgraded ? rule.damage : rule.baseDamage, player); + damage.mengsanAttack_shuying = true; + await damage; + // Defeating the last enemy may dispose the personal pile during damage. + if (isBattleActive() && player.isAlive()) { + await player.draw(upgraded ? rule.draw : rule.baseDraw); + } + }, + ai: { order: 6, result: { player: 0.6, target: -1.5 }, tag: { damage: 1, draw: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_jianbingdaji"); + const upgraded = cardUpgradeLevel(used) > 0; + const damage = upgraded ? rule.damage : rule.baseDamage; + const draw = upgraded ? rule.draw : rule.baseDraw; + return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后抽${draw}张牌。` + + "每回合使用次数不限。"; + }, + }; + translate.mengsan_jianbingdaji = "剑柄打击"; + translate.mengsan_jianbingdaji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成9点伤害,然后抽1张牌。" + + "每回合使用次数不限。"; + // Block determines base damage without spending the acting player's block. + card.mengsan_quanshenzhuangji = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_quanshenzhuangji.png", + mengsanCost_shuying: cardCost({ name: "mengsan_quanshenzhuangji" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (!isBattleActive() || !player.isAlive()) return; + const block = Number.isFinite(player.hujia) ? Math.max(0, player.hujia) : 0; + const storedStrength = player.storage?.mengsanStrength_shuying; + const strength = Number.isInteger(storedStrength) ? storedStrength : 0; + // Native damage(0) stops before damageBegin1, so strength must be included here. + const temporary = temporaryStrength?.amount(player) || 0; + const damage = event.target.damage(Math.max(0, block + strength + temporary), player); + damage.mengsanAttack_shuying = true; + damage.mengsanCardStrengthApplied_shuying = true; + damage.mengsanSetupStrengthApplied_shuying = true; + await damage; + }, + ai: { + order: 5, + result: { target(used, player) { return player.hujia > 0 ? -1.5 : 0; } }, + tag: { damage: 1 }, + }, + cardPrompt(used) { + const cost = cardCost(used || { name: "mengsan_quanshenzhuangji" }); + return `梦三:主动使用消耗${cost}费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。` + + "不消耗格挡,可以受到力量加成。" + + "每回合使用次数不限。"; + }, + }; + translate.mengsan_quanshenzhuangji = "全身撞击"; + translate.mengsan_quanshenzhuangji_info = "梦三:主动使用消耗1费用。对一名敌方角色造成等同于你当前格挡(护甲)值的伤害。" + + "不消耗格挡,可以受到力量加成。每回合使用次数不限。"; + // Double only the surviving target's existing vulnerable after damage resolves. + card.mengsan_rongrongzhiquan = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_rongrongzhiquan.png", + mengsanCost_shuying: cardCost({ name: "mengsan_rongrongzhiquan" }), + mengsanExhaust_shuying: true, + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + if (!isBattleActive() || !player.isAlive()) return false; + return !isActiveCardUse(event || _status.event, player) || canPayCard(player, used); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (!isBattleActive() || !player.isAlive()) return; + const rule = cardUpgradeRule("mengsan_rongrongzhiquan"); + const target = event.target; + const damage = target.damage(cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage, player); + damage.mengsanAttack_shuying = true; + await damage; + if (!target.isAlive() || !isBattleActive()) return; + const stacks = target.storage?.mengsanVulnerable_shuying; + if (!Number.isSafeInteger(stacks) || stacks <= 0) return; + const doubled = stacks * 2; + if (!Number.isSafeInteger(doubled)) throw new RangeError("易伤层数越界"); + if (applyMengsanDebuff(target, "vulnerable", stacks, player)) + game.log(target, `易伤层数从${stacks}翻倍为${doubled}`); + }, + ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_rongrongzhiquan"); + const damage = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return `梦三:主动使用消耗1费用。对一名敌方角色造成${damage}点伤害,然后将该目标已有的易伤层数翻倍。` + + "没有易伤时不会新增易伤。消耗:使用后退出本场战斗,不进入弃牌堆或洗牌。"; + }, + }; + translate.mengsan_rongrongzhiquan = "熔融之拳"; + translate.mengsan_rongrongzhiquan_info = "梦三:主动使用消耗1费用。对一名敌方角色造成10点伤害,然后将该目标已有的易伤层数翻倍。" + + "消耗:使用后退出本场战斗。"; + // STS2【愤怒】:攻击后生成独立的战斗复制品,不扩充永久牌组。 + card.mengsan_fennu = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_fennu.png", + mengsanCost_shuying: cardCost({ name: "mengsan_fennu" }), + enable(used, player, event) { + return !isActiveCardUse(event || _status.event, player) || + canPayCard(player, used); + }, + usable: Infinity, selectTarget: 1, + filterTarget(used, player, target) { + return target !== player && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (typeof copyToDiscard !== "function") throw new Error("愤怒缺少战斗复制接口"); + const amount = cardUpgradeLevel(event.card) + ? cardUpgradeRule("mengsan_fennu").damage : 6; + const damage = event.target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + if (copyToDiscard(player, event.card, event.cards)) { + game.log(player, "将一张【愤怒】的复制品加入个人弃牌堆"); + } + }, + ai: { order: 6, result: { target: -1 }, tag: { damage: 1 } }, + cardPrompt(used) { + const amount = cardUpgradeLevel(used) + ? cardUpgradeRule("mengsan_fennu").damage : 6; + return `梦三:主动使用消耗0费用。对一名敌方角色造成${amount}点伤害,` + + "然后将一张此牌的复制品加入你的个人弃牌堆。" + + "复制品保留原牌状态和词缀,仅存在于本场战斗。"; + }, + }; + translate.mengsan_fennu = "愤怒"; + translate.mengsan_fennu_info = "梦三:主动使用消耗0费用。对一名敌方角色造成6点伤害," + + "然后将一张此牌的复制品加入你的个人弃牌堆。复制品保留原牌状态和词缀,仅存在于本场战斗。" + + "每回合使用次数不限。"; + // STS2【放血】:先等待原生生命流失及濒死救援,再增加当前战斗费用。 + card.mengsan_fangxue = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_fangxue.png", + mengsanCost_shuying: cardCost({ name: "mengsan_fangxue" }), + enable(used, player, event) { + return isBattleActive() && Boolean(battleEnergy?.canUse(player)) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof battleEnergy?.capture !== "function" || typeof battleEnergy?.grant !== "function") { + throw new Error("放血缺少战斗费用接口"); + } + const rule = cardUpgradeRule("mengsan_fangxue"); + const amount = cardUpgradeLevel(event.card) ? rule.energy : rule.baseEnergy; + const battle = battleEnergy.capture(player, amount); + if (!battle) return; + await player.loseHp(rule.loseHp); + battleEnergy.grant(player, amount, battle); + }, + ai: { order: 9, result: { player: player => player.hp > 3 ? 1 : -1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_fangxue"); + const amount = cardUpgradeLevel(used) ? rule.energy : rule.baseEnergy; + return "梦三:主动使用消耗0费用。失去" + rule.loseHp + "点生命,然后获得" + amount + "点费用。"; + }, + }; + translate.mengsan_fangxue = "放血"; + translate.mengsan_fangxue_info = "梦三:主动使用消耗0费用。失去3点生命,然后获得2点费用。"; + // STS2【坚毅】:先获得格挡,再消耗一张自身当时的手牌。 + card.mengsan_jianyi = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_jianyi.png", + mengsanCost_shuying: cardCost({ name: "mengsan_jianyi" }), + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof handExhaust?.capture !== "function" || typeof handExhaust?.exhaust !== "function") { + throw new Error("坚毅缺少手牌消耗接口"); + } + const battle = handExhaust.capture(player); + if (!battle) return; + const rule = cardUpgradeRule("mengsan_jianyi"); + const current = cardUpgradeLevel(event.card); + await player.changeHujia(current ? rule.block : rule.baseBlock); + await handExhaust.exhaust(player, event, battle, Boolean(current && rule.chooseExhaust)); + }, + ai: { order: 7, result: { player: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_jianyi"); + const current = cardUpgradeLevel(used); + const block = current ? rule.block : rule.baseBlock; + return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后" + + (current && rule.chooseExhaust ? "选择消耗" : "随机消耗") + + "一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。"; + }, + }; + translate.mengsan_jianyi = "坚毅"; + translate.mengsan_jianyi_info = "梦三:主动使用消耗1费用。获得7点格挡(护甲),然后随机消耗一张你的手牌(退出本场战斗,不进入弃牌堆或洗牌)。没有手牌时不消耗牌。"; + // STS2【破灭】:免费使用自己的堆顶原牌,再将这一原牌消耗。 + card.mengsan_pomie = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_pomie.png", + mengsanCost_shuying: cardCost({ name: "mengsan_pomie" }), + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof playDrawTop !== "function") throw new Error("破灭缺少堆顶使用接口"); + await playDrawTop(player); + }, + ai: { order: 6, result: { player: 1 } }, + cardPrompt(used) { + return "梦三:主动使用消耗" + cardCost(used) + + "费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。"; + }, + }; + translate.mengsan_pomie = "破灭"; + translate.mengsan_pomie_info = "梦三:主动使用消耗1费用。免费使用你抽牌堆顶部的牌,然后将该原牌消耗(退出本场战斗,不进入弃牌堆或洗牌)。抽牌堆为空时不生效。"; + // STS2【耸肩无视】:先获得格挡,再抽一张牌。 + card.mengsan_songjianwushi = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_songjianwushi.png", + mengsanCost_shuying: cardCost({ name: "mengsan_songjianwushi" }), + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof blockDraw !== "function") throw new Error("耸肩无视缺少格挡抽牌接口"); + const rule = cardUpgradeRule("mengsan_songjianwushi"); + await blockDraw(player, cardUpgradeLevel(event.card) ? rule.block : rule.baseBlock, rule.draw); + }, + ai: { order: 7, result: { player: 1 }, tag: { draw: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_songjianwushi"); + const block = cardUpgradeLevel(used) ? rule.block : rule.baseBlock; + return "梦三:主动使用消耗1费用。获得" + block + "点格挡(护甲),然后抽" + rule.draw + "张牌。"; + }, + }; + translate.mengsan_songjianwushi = "耸肩无视"; + translate.mengsan_songjianwushi_info = "梦三:主动使用消耗1费用。获得8点格挡(护甲),然后抽1张牌。"; + // STS2【暴走】:先攻击,再仅增加本场这张实体牌的伤害。 + card.mengsan_baozou = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_baozou.png", + mengsanCost_shuying: cardCost({ name: "mengsan_baozou" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (typeof rampage?.apply !== "function") throw new Error("暴走缺少战斗累积接口"); + const rule = cardUpgradeRule("mengsan_baozou"); + const original = rampage.original(event) || event.card; + await rampage.apply(player, event, rule.damage, + cardUpgradeLevel(original) ? rule.increase : rule.baseIncrease); + }, + ai: { order: 5, result: { target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_baozou"); + const source = rampage?.card(used) || used; + const increase = cardUpgradeLevel(source) ? rule.increase : rule.baseIncrease; + const damage = rampage?.damage(used) ?? rule.damage; + return "梦三:主动使用消耗1费用。对一名敌方角色造成" + damage + + "点伤害,然后将这张牌在本场战斗中的伤害增加" + increase + "点。"; + }, + }; + translate.mengsan_baozou = "暴走"; + translate.mengsan_baozou_info = card.mengsan_baozou.cardPrompt({ name: "mengsan_baozou" }); + // STS2【怨恨】:读取本回合实际生命损失,固定次数后逐次原生攻击。 + card.mengsan_yuanhen = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_yuanhen.png", + mengsanCost_shuying: cardCost({ name: "mengsan_yuanhen" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + if (typeof spite !== "function") throw new Error("怨恨缺少战斗攻击接口"); + const rule = cardUpgradeRule("mengsan_yuanhen"); + await spite(player, event, rule.damage, + cardUpgradeLevel(event.card) ? rule.hits : rule.baseHits); + }, + ai: { order: 4, result: { target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_yuanhen"); + const hits = cardUpgradeLevel(used) ? rule.hits : rule.baseHits; + return "梦三:主动使用消耗0费用。对一名敌方角色造成5点伤害。" + + "如果你在本回合失去过生命值,则改为连续攻击" + hits + "次,每次造成5点伤害,分别结算。"; + }, + }; + translate.mengsan_yuanhen = "怨恨"; + translate.mengsan_yuanhen_info = card.mengsan_yuanhen.cardPrompt({ name: "mengsan_yuanhen" }); + // STS2【旋风斩】:X 在原生用牌支付时固定,逐次重取存活敌人。 + card.mengsan_xuanfengzhan = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_xuanfengzhan.png", + mengsanCost_shuying: "X", mengsanXCost_shuying: true, + notarget: true, usable: Infinity, + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + game.filterPlayer(target => target !== player && target.isAlive() && player.isEnemyOf(target)).length > 0 && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used, event || _status.event)); + }, + async content(event, trigger, player) { + if (typeof whirlwind !== "function") throw new Error("旋风斩缺少X费用攻击接口"); + const rule = cardUpgradeRule("mengsan_xuanfengzhan"); + await whirlwind(player, event, cardUpgradeLevel(event.card) ? rule.damage : rule.baseDamage); + }, + ai: { order: 3, result: { player: 1 }, tag: { damage: 1, multitarget: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_xuanfengzhan"); + const amount = cardUpgradeLevel(used) ? rule.damage : rule.baseDamage; + return "梦三:X费用。消耗当前剩余费用,对所有敌方角色各造成" + amount + + "点伤害X次,每次分别结算。X为本次用于攻击的费用;缠结时先扣除1点附加费用。X为0时不造成伤害。"; + }, + }; + translate.mengsan_xuanfengzhan = "旋风斩"; + translate.mengsan_xuanfengzhan_info = card.mengsan_xuanfengzhan.cardPrompt({ name: "mengsan_xuanfengzhan" }); + // STS2【欺凌】:按本次目标当前易伤层数计算一次基础攻击,不消耗易伤。 + card.mengsan_qiling = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_qiling.png", + mengsanCost_shuying: cardCost({ name: "mengsan_qiling" }), + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + const layers = target.storage?.mengsanVulnerable_shuying ?? 0; + if (!Number.isSafeInteger(layers) || layers < 0) throw new RangeError("欺凌易伤层数无效"); + const rule = cardUpgradeRule("mengsan_qiling"); + const perLayer = cardUpgradeLevel(event.card) ? rule.perVulnerable : rule.basePerVulnerable; + const amount = rule.baseDamage + layers * perLayer; + if (!Number.isSafeInteger(amount) || amount < 0) throw new RangeError("欺凌伤害越界"); + const damage = target.damage(amount, player); + damage.mengsanAttack_shuying = true; + await damage; + }, + ai: { order: 6, result: { target: -1.5 }, tag: { damage: 1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_qiling"); + const perLayer = cardUpgradeLevel(used) ? rule.perVulnerable : rule.basePerVulnerable; + return "梦三:主动使用消耗0费用。对一名敌方角色造成4点伤害," + + "该目标每有1层易伤,额外增加" + perLayer + "点伤害。不消耗易伤层数。每回合使用次数不限。"; + }, + }; + translate.mengsan_qiling = "欺凌"; + translate.mengsan_qiling_info = "梦三:主动使用消耗0费用。对一名敌方角色造成4点伤害," + + "该目标每有1层易伤,额外增加2点伤害。不消耗易伤层数。每回合使用次数不限。"; + // STS2【血墙】:等待原生生命流失及濒死救援,再给同一战斗中的使用者格挡。 + card.mengsan_xueqiang = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_xueqiang.png", + mengsanCost_shuying: cardCost({ name: "mengsan_xueqiang" }), + enable(used, player, event) { + return isBattleActive() && Boolean(lifeLossBlock?.canUse(player)) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof lifeLossBlock?.apply !== "function") throw new Error("血墙缺少生命流失格挡接口"); + const rule = cardUpgradeRule("mengsan_xueqiang"); + await lifeLossBlock.apply(player, rule.loseHp, + cardUpgradeLevel(event.card) ? rule.block : rule.baseBlock); + }, + ai: { order: 7, result: { player: player => player.hp > 2 ? 1 : -1 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_xueqiang"); + const block = cardUpgradeLevel(used) ? rule.block : rule.baseBlock; + return "梦三:主动使用消耗2费用。失去2点生命,然后获得" + block + "点格挡(护甲)。"; + }, + }; + translate.mengsan_xueqiang = "血墙"; + translate.mengsan_xueqiang_info = "梦三:主动使用消耗2费用。失去2点生命,然后获得16点格挡(护甲)。"; + // STS2【战栗】:施加易伤走既有人工制品入口,消耗走原生用牌及个人牌堆。 + card.mengsan_zhanli = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_zhanli.png", + mengsanCost_shuying: cardCost({ name: "mengsan_zhanli" }), + mengsanExhaust_shuying: true, + usable: Infinity, selectTarget: 1, + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + filterTarget(used, player, target) { + return target !== player && target.isAlive() && player.isEnemyOf(target); + }, + async content(event, trigger, player) { + const target = event.target; + if (!isBattleActive() || !player.isAlive() || !target?.isAlive() || + target === player || !player.isEnemyOf(target)) return; + const rule = cardUpgradeRule("mengsan_zhanli"); + const amount = cardUpgradeLevel(event.card) ? rule.vulnerable : rule.baseVulnerable; + if (applyMengsanDebuff(target, "vulnerable", amount, player)) + game.log(target, `获得${amount}层易伤`); + }, + ai: { order: 9, result: { target: -1.2 } }, + cardPrompt(used) { + const rule = cardUpgradeRule("mengsan_zhanli"); + const amount = cardUpgradeLevel(used) ? rule.vulnerable : rule.baseVulnerable; + return "梦三:主动使用消耗1费用。给予一名敌方角色" + amount + "层易伤。" + + "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。" + + "消耗(使用后退出本场战斗,不进入弃牌堆或洗牌)。"; + }, + }; + translate.mengsan_zhanli = "战栗"; + translate.mengsan_zhanli_info = "梦三:主动使用消耗1费用。给予一名敌方角色3层易伤。" + + "易伤使受到的攻击伤害增加50%(向上取整),每个自身回合结束减少1层。" + + "消耗(使用后退出本场战斗,不进入弃牌堆或洗牌)。"; + // STS2【武装】:格挡结算后只强化当前战斗的真实手牌。 + card.mengsan_wuzhuang = { + type: "basic", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_wuzhuang.png", + mengsanCost_shuying: cardCost({ name: "mengsan_wuzhuang" }), + enable(used, player, event) { + return isBattleActive() && Boolean(player?.isAlive?.()) && + (!isActiveCardUse(event || _status.event, player) || canPayCard(player, used)); + }, + usable: Infinity, toself: true, selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + if (typeof blockUpgrade !== "function") throw new Error("武装缺少格挡强化接口"); + await blockUpgrade(player, event, cardUpgradeRule("mengsan_wuzhuang").block, + Boolean(cardUpgradeLevel(event.card))); + }, + ai: { order: 8, result: { player: 1 } }, + cardPrompt(used) { + const selection = cardUpgradeLevel(used) ? "所有牌" : "一张牌"; + return "梦三:主动使用消耗1费用。获得5点格挡(护甲),然后强化你手牌中的" + selection + "(仅本场战斗)。"; + }, + }; + translate.mengsan_wuzhuang = "武装"; + translate.mengsan_wuzhuang_info = "梦三:主动使用消耗1费用。获得5点格挡(护甲),然后强化你手牌中的一张牌(仅本场战斗)。"; + // 【防御】沿用梦三护甲事件,保留脆弱与伤害抵扣等既有规则。 + card.mengsan_fangyu = { + type: "basic", + fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_fangyu.png", + mengsanCost_shuying: cardCost({ name: "mengsan_fangyu" }), + enable(used, player, event) { + return !isActiveCardUse(event || _status.event, player) || + canPayCard(player, used); + }, + usable: Infinity, + toself: true, + selectTarget: -1, + filterTarget(used, player, target) { return target === player; }, + async content(event, trigger, player) { + await player.changeHujia(cardUpgradeLevel(event.card) ? 8 : 5); + }, + ai: { order: 7, result: { target: 1 } }, + cardPrompt(used) { return `梦三:主动使用消耗1费用。获得${cardUpgradeLevel(used) ? 8 : 5}点格挡(护甲)。`; }, + }; + translate.mengsan_fangyu = "防御"; + translate.mengsan_fangyu_info = + "梦三:主动使用消耗1费用。出牌阶段,对自己使用,获得5点格挡(护甲)。" + + "每回合使用次数不限。"; + // 【桃】的回复量也由梦三牌本体提供;救援仍不消耗费用。 + return { cards:card, translate, names:Object.keys(card) }; +} diff --git a/mengsan/cards/packs/liubei/metadata.js b/mengsan/cards/packs/liubei/metadata.js new file mode 100644 index 0000000..3554b2f --- /dev/null +++ b/mengsan/cards/packs/liubei/metadata.js @@ -0,0 +1,140 @@ +import { ironcladCards, ironcladUpgradeRules } from "./data.js"; +import { LIUBEI_CARD_OWNER } from "./names.js"; +import { liubeiCardTraits } from "./traits.js"; +const legacyDefinitions = [ + { + "name": "mengsan_baozou", + "category": "damage" + }, + { + "name": "mengsan_yuanhen", + "category": "damage" + }, + { + "name": "mengsan_xuanfengzhan", + "category": "damage" + }, + { + "name": "mengsan_qiling", + "category": "damage" + }, + { + "name": "mengsan_xueqiang", + "category": "utility" + }, + { + "name": "mengsan_zhanli", + "category": "utility" + }, + { + "name": "mengsan_wuzhuang", + "category": "utility" + }, + { + "name": "mengsan_songjianwushi", + "category": "utility" + }, + { + "name": "mengsan_pomie", + "category": "utility" + }, + { + "name": "mengsan_jianyi", + "category": "utility" + }, + { + "name": "mengsan_fangxue", + "category": "utility" + }, + { + "name": "mengsan_yujin", + "category": "damage" + }, + { + "name": "mengsan_wanmeidaji", + "category": "damage" + }, + { + "name": "mengsan_yubeidaji", + "category": "damage" + }, + { + "name": "mengsan_tupo", + "category": "damage" + }, + { + "name": "mengsan_touchui", + "category": "damage" + }, + { + "name": "mengsan_tiezhanbo", + "category": "damage" + }, + { + "name": "mengsan_shuangchongdaji", + "category": "damage" + }, + { + "name": "mengsan_shandianpili", + "category": "damage" + }, + { + "name": "mengsan_fennu", + "category": "damage" + }, + { + "name": "mengsan_rongrongzhiquan", + "category": "damage" + }, + { + "name": "mengsan_quanshenzhuangji", + "category": "damage" + }, + { + "name": "mengsan_jianbingdaji", + "category": "damage" + }, + { + "name": "mengsan_feijianhuixuanbiao", + "category": "damage" + }, + { + "name": "mengsan_fangyu", + "category": "utility" + } +]; +export const liubeiDefinitions = Object.freeze([ + ...legacyDefinitions,...ironcladCards.map(({name,category,base})=>({name,category,generatedOnly:Boolean(base.generatedOnly)})), +].map(definition=>Object.freeze({...definition,...liubeiCardTraits[definition.name],owner:LIUBEI_CARD_OWNER,deck:"liubei"}))); +export const liubeiCosts = Object.freeze({ + ...{"mengsan_fangyu": 1, "mengsan_fennu": 0, "mengsan_feijianhuixuanbiao": 1, "mengsan_jianbingdaji": 1, "mengsan_quanshenzhuangji": 1, "mengsan_rongrongzhiquan": 1, "mengsan_shandianpili": 1, "mengsan_shuangchongdaji": 1, "mengsan_tiezhanbo": 1, "mengsan_touchui": 1, "mengsan_tupo": 1, "mengsan_yubeidaji": 1, "mengsan_wanmeidaji": 2, "mengsan_yujin": 2, "mengsan_fangxue": 0, "mengsan_jianyi": 1, "mengsan_pomie": 1, "mengsan_songjianwushi": 1, "mengsan_wuzhuang": 1, "mengsan_zhanli": 1, "mengsan_xueqiang": 2, "mengsan_qiling": 0, "mengsan_yuanhen": 0, "mengsan_baozou": 1, "mengsan_xuanfengzhan": "X"}, + ...Object.fromEntries(ironcladCards.map(card=>[card.name,card.cost])), +}); +export const liubeiUpgradeRules = Object.freeze({ + ...ironcladUpgradeRules, + mengsan_fangyu: Object.freeze({ maxLevel: 1, baseBlock: 5, block: 8 }), + mengsan_baozou: Object.freeze({ maxLevel: 1, damage: 9, baseIncrease: 5, increase: 9 }), + mengsan_yuanhen: Object.freeze({ maxLevel: 1, damage: 5, baseHits: 2, hits: 3 }), + mengsan_xuanfengzhan: Object.freeze({ maxLevel: 1, baseDamage: 5, damage: 8 }), + mengsan_qiling: Object.freeze({ maxLevel: 1, baseDamage: 4, basePerVulnerable: 2, perVulnerable: 3 }), + mengsan_xueqiang: Object.freeze({ maxLevel: 1, loseHp: 2, baseBlock: 16, block: 20 }), + mengsan_zhanli: Object.freeze({ maxLevel: 1, baseVulnerable: 3, vulnerable: 4 }), + mengsan_wuzhuang: Object.freeze({ maxLevel: 1, block: 5 }), + mengsan_songjianwushi: Object.freeze({ maxLevel: 1, baseBlock: 8, block: 11, draw: 1 }), + mengsan_pomie: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0 }), + mengsan_jianyi: Object.freeze({ maxLevel: 1, baseBlock: 7, block: 9, chooseExhaust: true }), + mengsan_fangxue: Object.freeze({ maxLevel: 1, loseHp: 3, baseEnergy: 2, energy: 3 }), + mengsan_yujin: Object.freeze({ maxLevel: 1, baseDamage: 18, damage: 24, exhaust: 1 }), + mengsan_wanmeidaji: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 6, basePerStrike: 2, perStrike: 3 }), + mengsan_yubeidaji: Object.freeze({ maxLevel: 1, baseDamage: 7, damage: 9, baseStrength: 2, strength: 3 }), + mengsan_tupo: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 13, loseHp: 1 }), + mengsan_touchui: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 12 }), + mengsan_tiezhanbo: Object.freeze({ maxLevel: 1, baseBlock: 5, block: 7, baseDamage: 5, damage: 7 }), + mengsan_shuangchongdaji: Object.freeze({ maxLevel: 1, baseDamage: 5, damage: 7, hits: 2 }), + mengsan_shandianpili: Object.freeze({ maxLevel: 1, baseDamage: 4, damage: 7, vulnerable: 1 }), + mengsan_rongrongzhiquan: Object.freeze({ maxLevel: 1, baseDamage: 10, damage: 14, exhaust: true }), + mengsan_quanshenzhuangji: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0, damageFromBlock: true }), + mengsan_jianbingdaji: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 10, baseDraw: 1, draw: 2 }), + mengsan_feijianhuixuanbiao: Object.freeze({ maxLevel: 1, damage: 3, baseHits: 3, hits: 4 }), + mengsan_fennu: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 8 }), +}); diff --git a/mengsan/cards/packs/liubei/names.js b/mengsan/cards/packs/liubei/names.js new file mode 100644 index 0000000..1442476 --- /dev/null +++ b/mengsan/cards/packs/liubei/names.js @@ -0,0 +1,12 @@ +export const LIUBEI_CARD_OWNER = "mengsan_liubei_shuying"; +// STS2 的基础打击/痛击分别沿用杀/重杀;保留原有通用牌归属。 +export const liubeiBaseCardNames = Object.freeze(["sha", "mengsan_zhongsha"]); +export const legacyIroncladNames = Object.freeze([ + "mengsan_fangyu","mengsan_fennu","mengsan_feijianhuixuanbiao","mengsan_jianbingdaji", + "mengsan_quanshenzhuangji","mengsan_rongrongzhiquan","mengsan_shandianpili", + "mengsan_shuangchongdaji","mengsan_tiezhanbo","mengsan_touchui","mengsan_tupo", + "mengsan_yubeidaji","mengsan_wanmeidaji","mengsan_yujin","mengsan_fangxue", + "mengsan_jianyi","mengsan_pomie","mengsan_songjianwushi","mengsan_wuzhuang", + "mengsan_zhanli","mengsan_xueqiang","mengsan_qiling","mengsan_xuanfengzhan", + "mengsan_yuanhen","mengsan_baozou", +]); diff --git a/mengsan/cards/packs/liubei/rewards.js b/mengsan/cards/packs/liubei/rewards.js new file mode 100644 index 0000000..928890a --- /dev/null +++ b/mengsan/cards/packs/liubei/rewards.js @@ -0,0 +1,71 @@ +export const liubeiRewards = { + "shared.reward.card.mengsan_ic_dismantle": {card:{name:"mengsan_ic_dismantle"},name:"获得一张【拆卸】",description:"1费:8伤害;目标已有易伤时攻击2次。"}, + "shared.reward.card.mengsan_ic_ashen_strike": {card:{name:"mengsan_ic_ashen_strike"},name:"获得一张【灰烬打击】",description:"1费:6伤害,每张消耗牌增加3伤害。"}, + "shared.reward.card.mengsan_ic_pillage": {card:{name:"mengsan_ic_pillage"},name:"获得一张【劫掠】",description:"1费:6伤害,抽牌至抽到非攻击牌。"}, + "shared.reward.card.mengsan_ic_hemokinesis": {card:{name:"mengsan_ic_hemokinesis"},name:"获得一张【御血术】",description:"1费:失去2生命,15伤害。"}, + "shared.reward.card.mengsan_ic_uppercut": {card:{name:"mengsan_ic_uppercut"},name:"获得一张【上勾拳】",description:"2费:13伤害,1虚弱,1易伤。"}, + "shared.reward.card.mengsan_ic_relentless_assault": {card:{name:"mengsan_ic_relentless_assault"},name:"获得一张【无情猛攻】",description:"2费:14伤害,下一张攻击费用变为0。"}, + "shared.reward.card.mengsan_ic_fight_me": {card:{name:"mengsan_ic_fight_me"},name:"获得一张【与我一战!】",description:"2费:5伤害2次,获得3力量,敌人获得1力量。"}, + "shared.reward.card.mengsan_ic_howl_from_beyond": {card:{name:"mengsan_ic_howl_from_beyond"},name:"获得一张【彼岸咆哮】",description:"3费:全体16伤害;回合末在消耗堆中则打出。"}, + "shared.reward.card.mengsan_ic_stomp": {card:{name:"mengsan_ic_stomp"},name:"获得一张【踩踏】",description:"3费:全体12伤害,本回合每打出过一张攻击费用减少1。"}, + "shared.reward.card.mengsan_ic_bludgeon": {card:{name:"mengsan_ic_bludgeon"},name:"获得一张【重锤】",description:"3费:32伤害。"}, + "shared.reward.card.mengsan_ic_demonic_shield": {card:{name:"mengsan_ic_demonic_shield"},name:"获得一张【恶魔护盾】",description:"0费:失去1生命,给予另一玩家等于自身当前格挡的格挡,消耗。"}, + "shared.reward.card.mengsan_ic_rage": {card:{name:"mengsan_ic_rage"},name:"获得一张【狂怒】",description:"0费:本回合每打出攻击获得3格挡。"}, + "shared.reward.card.mengsan_ic_battle_trance": {card:{name:"mengsan_ic_battle_trance"},name:"获得一张【战斗专注】",description:"0费:抽3,本回合不能再抽牌。"}, + "shared.reward.card.mengsan_ic_forgotten_ritual": {card:{name:"mengsan_ic_forgotten_ritual"},name:"获得一张【被遗忘的仪式】",description:"1费:本回合消耗过牌时获得3费用,消耗。"}, + "shared.reward.card.mengsan_ic_infernal_blade": {card:{name:"mengsan_ic_infernal_blade"},name:"获得一张【地狱之刃】",description:"1费:随机攻击加入手牌,本回合免费,消耗。"}, + "shared.reward.card.mengsan_ic_colossus": {card:{name:"mengsan_ic_colossus"},name:"获得一张【巨像】",description:"1费:5格挡,本回合易伤敌人的伤害降低50%。"}, + "shared.reward.card.mengsan_ic_burning_pact": {card:{name:"mengsan_ic_burning_pact"},name:"获得一张【燃烧契约】",description:"1费:消耗一张牌,抽2。"}, + "shared.reward.card.mengsan_ic_taunt": {card:{name:"mengsan_ic_taunt"},name:"获得一张【挑衅】",description:"1费:7格挡,1易伤。"}, + "shared.reward.card.mengsan_ic_evil_eye": {card:{name:"mengsan_ic_evil_eye"},name:"获得一张【邪眼】",description:"1费:8格挡;本回合消耗过牌时额外8格挡。"}, + "shared.reward.card.mengsan_ic_drum_of_battle": {card:{name:"mengsan_ic_drum_of_battle"},name:"获得一张【战鼓】",description:"1费:抽2;被消耗时获得2费用。"}, + "shared.reward.card.mengsan_ic_second_wind": {card:{name:"mengsan_ic_second_wind"},name:"获得一张【重振精神】",description:"1费:消耗所有非攻击手牌,每张获得5格挡。"}, + "shared.reward.card.mengsan_ic_dominate": {card:{name:"mengsan_ic_dominate"},name:"获得一张【主宰】",description:"1费:1易伤,目标每层易伤获得1力量,消耗。"}, + "shared.reward.card.mengsan_ic_flame_barrier": {card:{name:"mengsan_ic_flame_barrier"},name:"获得一张【火焰屏障】",description:"2费:12格挡,每受到一次攻击反击4伤害。"}, + "shared.reward.card.mengsan_ic_eager": {card:{name:"mengsan_ic_eager"},name:"获得一张【跃跃欲试】",description:"2费:每张攻击手牌获得1费用,本回合不能再获得费用。"}, + "shared.reward.card.mengsan_ic_inflame": {card:{name:"mengsan_ic_inflame"},name:"获得一张【燃烧】",description:"1费:获得2力量。"}, + "shared.reward.card.mengsan_ic_rupture": {card:{name:"mengsan_ic_rupture"},name:"获得一张【撕裂】",description:"1费:自身回合每次失血获得1力量。"}, + "shared.reward.card.mengsan_ic_feel_no_pain": {card:{name:"mengsan_ic_feel_no_pain"},name:"获得一张【无惧疼痛】",description:"1费:每张牌被消耗时获得3格挡。"}, + "shared.reward.card.mengsan_ic_ferocity": {card:{name:"mengsan_ic_ferocity"},name:"获得一张【凶恶】",description:"1费:每次给予易伤抽1。"}, + "shared.reward.card.mengsan_ic_stone_armor": {card:{name:"mengsan_ic_stone_armor"},name:"获得一张【岩石铠甲】",description:"1费:获得4覆甲。"}, + "shared.reward.card.mengsan_ic_inferno": {card:{name:"mengsan_ic_inferno"},name:"获得一张【狱火】",description:"1费:回合开始失去1生命,自身回合每次失血全体6伤害。"}, + "shared.reward.card.mengsan_ic_juggle": {card:{name:"mengsan_ic_juggle"},name:"获得一张【杂耍】",description:"1费:每回合第三张攻击复制至手牌。"}, + "shared.reward.card.mengsan_ic_stampede": {card:{name:"mengsan_ic_stampede"},name:"获得一张【惊逃】",description:"2费:回合末随机打出手牌攻击,目标随机。"}, + "shared.reward.card.mengsan_ic_pacts_end": {card:{name:"mengsan_ic_pacts_end"},name:"获得一张【契约终结】",description:"0费:消耗堆至少3张时全体17伤害。"}, + "shared.reward.card.mengsan_ic_conflagration": {card:{name:"mengsan_ic_conflagration"},name:"获得一张【焚烧】",description:"1费:全体2伤害4次。"}, + "shared.reward.card.mengsan_ic_feed": {card:{name:"mengsan_ic_feed"},name:"获得一张【狂宴】",description:"1费:10伤害,斩杀永久获得3最大生命,消耗。"}, + "shared.reward.card.mengsan_ic_thrash": {card:{name:"mengsan_ic_thrash"},name:"获得一张【痛殴】",description:"1费:4伤害2次,随机消耗攻击手牌并继承其伤害。"}, + "shared.reward.card.mengsan_ic_tear_asunder": {card:{name:"mengsan_ic_tear_asunder"},name:"获得一张【扯碎】",description:"2费:5伤害,本场每次失血增加一次攻击。"}, + "shared.reward.card.mengsan_ic_fiend_fire": {card:{name:"mengsan_ic_fiend_fire"},name:"获得一张【恶魔之焰】",description:"2费:消耗所有手牌,每张造成7伤害,消耗。"}, + "shared.reward.card.mengsan_ic_bully": {card:{name:"mengsan_ic_bully"},name:"获得一张【凌虐】",description:"3费:15伤害,敌人本回合失去10力量。"}, + "shared.reward.card.mengsan_ic_offering": {card:{name:"mengsan_ic_offering"},name:"获得一张【祭品】",description:"0费:失去6生命,获得2费用,抽3,消耗。"}, + "shared.reward.card.mengsan_ic_brand": {card:{name:"mengsan_ic_brand"},name:"获得一张【烙印】",description:"0费:失去1生命,消耗一张牌,获得1力量。"}, + "shared.reward.card.mengsan_ic_unleash": {card:{name:"mengsan_ic_unleash"},name:"获得一张【倾泻】",description:"X费:打出抽牌堆顶X张牌。"}, + "shared.reward.card.mengsan_ic_primal_force": {card:{name:"mengsan_ic_primal_force"},name:"获得一张【原始力量】",description:"0费:所有攻击手牌变为巨石。"}, + "shared.reward.card.mengsan_ic_twin_strike": {card:{name:"mengsan_ic_twin_strike"},name:"获得一张【连环拳】",description:"1费:下1张攻击额外打出一次。"}, + "shared.reward.card.mengsan_ic_stoke": {card:{name:"mengsan_ic_stoke"},name:"获得一张【添柴】",description:"1费:消耗所有手牌,每张生成一张随机牌至手牌。"}, + "shared.reward.card.mengsan_ic_impervious": {card:{name:"mengsan_ic_impervious"},name:"获得一张【岿然不动】",description:"2费:30格挡,消耗。"}, + "shared.reward.card.mengsan_ic_not_yet": {card:{name:"mengsan_ic_not_yet"},name:"获得一张【时候未到】",description:"2费:回复10生命,消耗。"}, + "shared.reward.card.mengsan_ic_cruelty": {card:{name:"mengsan_ic_cruelty"},name:"获得一张【残酷】",description:"1费:易伤敌人额外受到25%伤害。"}, + "shared.reward.card.mengsan_ic_crimson_mantle": {card:{name:"mengsan_ic_crimson_mantle"},name:"获得一张【绯红披风】",description:"1费:回合开始失去1生命并获得8格挡。"}, + "shared.reward.card.mengsan_ic_battle_ready": {card:{name:"mengsan_ic_battle_ready"},name:"获得一张【好勇斗狠】",description:"1费:回合开始从弃牌堆随机取一张攻击至手牌并升级。"}, + "shared.reward.card.mengsan_ic_bodyguard": {card:{name:"mengsan_ic_bodyguard"},name:"获得一张【肉盾】",description:"1费:承受敌人双倍伤害,敌人对盟友伤害减半。"}, + "shared.reward.card.mengsan_ic_hellraiser": {card:{name:"mengsan_ic_hellraiser"},name:"获得一张【地狱狂徒】",description:"2费:抽到名字含打击的牌时向随机敌人打出。"}, + "shared.reward.card.mengsan_ic_dark_embrace": {card:{name:"mengsan_ic_dark_embrace"},name:"获得一张【黑暗之拥】",description:"2费:每张牌被消耗时抽1。"}, + "shared.reward.card.mengsan_ic_unshakable": {card:{name:"mengsan_ic_unshakable"},name:"获得一张【坚定不移】",description:"2费:每回合第一次从卡牌获得的格挡翻倍。"}, + "shared.reward.card.mengsan_ic_juggernaut": {card:{name:"mengsan_ic_juggernaut"},name:"获得一张【势不可当】",description:"2费:每次获得格挡,对随机敌人造成6伤害。"}, + "shared.reward.card.mengsan_ic_pyre": {card:{name:"mengsan_ic_pyre"},name:"获得一张【薪火之源】",description:"2费:回合开始获得1费用。"}, + "shared.reward.card.mengsan_ic_barricade": {card:{name:"mengsan_ic_barricade"},name:"获得一张【壁垒】",description:"3费:回合开始不清空格挡。"}, + "shared.reward.card.mengsan_ic_demon_form": {card:{name:"mengsan_ic_demon_form"},name:"获得一张【恶魔形态】",description:"3费:回合开始获得2力量。"}, + "shared.reward.card.mengsan_ic_clash": {card:{name:"mengsan_ic_clash"},name:"获得一张【交锋】",description:"0费:手牌全部是攻击时可用,14伤害。"}, + "shared.reward.card.mengsan_ic_dual_wield": {card:{name:"mengsan_ic_dual_wield"},name:"获得一张【双持】",description:"1费:复制一张攻击或能力手牌至手牌。"}, + "shared.reward.card.mengsan_ic_entrench": {card:{name:"mengsan_ic_entrench"},name:"获得一张【巩固】",description:"2费:当前格挡翻倍。"}, + "shared.reward.card.mengsan_ic_break": {card:{name:"mengsan_ic_break"},name:"获得一张【破击】",description:"1费:20伤害,5易伤。"}, + "shared.reward.card.mengsan_ic_corruption": {card:{name:"mengsan_ic_corruption"},name:"获得一张【腐化】",description:"3费:技能费用变为0,打出的技能被消耗。"}, + "shared.reward.card.mengsan_ic_midnight": {card:{name:"mengsan_ic_midnight"},name:"获得一张【午夜】",description:"12费:99伤害;本场任何人每消耗一张牌费用减少1。"}, + "shared.reward.card.mengsan_ic_blaze": {card:{name:"mengsan_ic_blaze"},name:"获得一张【炽焰】",description:"2费:给予另一玩家5力量。"}, + "shared.reward.card.mengsan_ic_rally": {card:{name:"mengsan_ic_rally"},name:"获得一张【群情激愤】",description:"0费:9伤害,在所有人弃牌堆中加入本牌副本。"}, + "shared.reward.card.mengsan_ic_grapple": {card:{name:"mengsan_ic_grapple"},name:"获得一张【擒拿】",description:"1费:7伤害,本回合获得格挡时对该目标5伤害。"}, +}; +export const liubeiRewardIds = Object.freeze(Object.keys(liubeiRewards)); + diff --git a/mengsan/cards/packs/liubei/runtime.js b/mengsan/cards/packs/liubei/runtime.js new file mode 100644 index 0000000..c5d56c9 --- /dev/null +++ b/mengsan/cards/packs/liubei/runtime.js @@ -0,0 +1,320 @@ +import { ironcladCards } from "./data.js"; +import { cardUpgradeLevel } from "../../upgrades.js"; +import { canPayCard, cardCost, isActiveCardUse } from "../../../battle/combat-rules.js"; +import { isAttackCard } from "../../../monsters/vine-tangled.js"; +import { applyMengsanDebuff } from "../../../monsters/artifact-status.js"; +import { createAdvancedIronclad } from "./advanced.js"; + +const POWER_SKILL = "mengsan_ic_powers_shuying"; +const belongsTo = (event, root) => { + const seen = new Set(); + for (let e = event; e && !seen.has(e); e = e.parent) { + if (e === root) return true; + seen.add(e); + } + return false; +}; +const positive = n => Number.isSafeInteger(n) && n >= 0; + +export function createIroncladRuntime({ game, get, lib, status, getBattle, handExhaust, battleEnergy, random }) { + const advanced=createAdvancedIronclad({game,get,lib,status,getBattle,battleEnergy,random}); + const states = new WeakMap(); + const capture = player => { + const battle = getBattle(); + if (!battle?.session?.active || !battle.root || !battle.players?.has(player) || !player?.isAlive?.()) return null; + return { battle, session: battle.session, root: battle.root }; + }; + const valid = (player, token) => Boolean(token && getBattle() === token.battle && + token.battle.session === token.session && token.session.active && token.battle.root === token.root && + token.battle.players.has(player) && player?.isAlive?.()); + const pileOf = (player, token) => player === game.me ? token.battle.personalPiles : token.battle.monsterPiles?.get(player); + const stateOf = player => { + const s = states.get(player); + return s && valid(player, s.token) ? s : null; + }; + const ensure = (player, token) => { + let s = stateOf(player); + if (s) return s; + s = { token, powers: Object.create(null), turn: Object.create(null), defense: Object.create(null) }; + token.session.ownResource(s, () => { + if (states.get(player) !== s) return; + states.delete(player); + player.removeSkill(POWER_SKILL); + }); + states.set(player, s); + player.addSkill(POWER_SKILL); + return s; + }; + const add = (object, key, n) => { + const next = (object[key] || 0) + n; + if (!Number.isFinite(next) || next < 0 || next > Number.MAX_SAFE_INTEGER) throw new RangeError("铁甲战士效果数值越界"); + object[key] = next; + }; + const strength = (player, n, token) => { + if (!valid(player, token) || !positive(n)) return false; + const next = (player.storage.mengsanStrength_shuying || 0) + n; + if (!positive(next)) throw new RangeError("力量数值越界"); + player.storage.mengsanStrength_shuying = next; + player.addSkill("mengsan_raider_strength_shuying"); + player.markSkill("mengsan_raider_strength_shuying"); + return true; + }; + const exhaustCount = (player, token) => { + const cards = pileOf(player, token)?.battleCards?.() || []; + return cards.filter(c => c.storage?.mengsanExhausted_shuying).length; + }; + const exhaustedThisTurn = (player, token) => { + const lost = game.getGlobalHistory("cardMove"); + if (Array.isArray(lost) && lost.some(e => e.player === player && e.type === "mengsanExhaust" && + belongsTo(e, token.root) && e.cards?.some(c => c.storage?.mengsanExhausted_shuying))) return true; + return player.getHistory("useCard").some(e => belongsTo(e, token.root) && + e.cards?.some(c => c.storage?.mengsanExhausted_shuying)); + }; + const chooseExhaust = async (player, event, token) => { + const choice = handExhaust.capture(player); + if (!choice || choice.battle !== token.battle) return null; + return await handExhaust.exhaust(player, event, choice, true, "选择消耗一张你的手牌"); + }; + const exhaustHands = async (player, event, token, filter = () => true, afterEach = null) => { + const pile = pileOf(player, token); + if (typeof pile?.exhaustFromHand !== "function") throw new Error("铁甲战士缺少个人消耗接口"); + const playing = new Set(event.cards || []); + const cards = player.getCards("h").filter(c => !playing.has(c) && filter(c)); + let count = 0; + for (const c of cards) { + if (!valid(player, token) || pileOf(player, token) !== pile) break; + if (await pile.exhaustFromHand(c)) { + count++; + if (afterEach && valid(player, token)) await afterEach(); + } + } + return count; + }; + const ruleOf = (spec, card) => cardUpgradeLevel(card) ? spec.upgraded : spec.base; + const prompt = (spec, card) => { + if(spec.base.advanced)return advanced.describe(spec,card); + const r = ruleOf(spec, card), out = []; + if (r.onlyAttacks) out.push("只有手牌全部为攻击牌时才能打出。"); + if (r.loseHp) out.push(`失去${r.loseHp}点生命。`); + if (r.block) out.push(`获得${r.block}点格挡。`); + if (r.exhaustTurnBlock) out.push(`本回合消耗过卡牌时,再获得${r.block}点格挡。`); + if (r.doubleBlock) out.push("将当前格挡翻倍。"); + if (r.allyBlock) out.push("给予另一名友方角色等于自己当前格挡值的格挡。"); + if (r.chooseExhaust) out.push("消耗一张手牌。"); + if (r.exhaustNonAttacks) out.push(`消耗所有非攻击手牌,每张获得${r.blockPerExhaust}点格挡。`); + if (r.hitsPerExhaust) out.push(`消耗所有手牌,每张对一名敌人造成${r.damage}点伤害。`); + else if (r.damage) { + let text = `${r.all ? "对所有敌人" : "对一名敌人"}造成${r.damage}点伤害`; + if (r.hits) text += `${r.hits}次`; + out.push(text + "。"); + } + if (r.minExhaust) out.unshift(`仅当个人消耗牌堆至少${r.minExhaust}张时造成伤害。`); + if (r.vulnerableHits) out.push(`目标已有易伤时,攻击${r.vulnerableHits}次。`); + if (r.hitsPerHpLoss) out.push("本场战斗每次实际失去生命,额外攻击一次。"); + if (r.perExhaust) out.push(`每张个人消耗牌额外增加${r.perExhaust}点基础伤害。`); + if (r.drawUntilSkill) out.push("抽牌直到抽到一张非攻击牌。"); + if (r.vulnerable) out.push(`给予目标${r.vulnerable}层易伤。`); + if (r.weak) out.push(`给予目标${r.weak}层虚弱。`); + if (r.strengthFromVulnerable) out.push("目标每有一层易伤,获得1点力量。"); + if (r.strength) out.push(`获得${r.strength}点力量。`); + if (r.enemyStrength) out.push(`目标获得${r.enemyStrength}点力量。`); + if (r.allyStrength) out.push(`给予另一名友方角色${r.allyStrength}点力量。`); + if (r.energy) out.push(`获得${r.energy}费用。`); + if (r.exhaustTurnEnergy) out.push(`本回合消耗过卡牌时,获得${r.exhaustTurnEnergy}费用。`); + if (r.draw) out.push(`抽${r.draw}张牌。`); + if (r.heal) out.push(`回复${r.heal}点生命。`); + if (r.turnStrength) out.push(`每个自身回合开始时获得${r.turnStrength}点力量。`); + if (r.turnEnergy) out.push(`每个自身回合开始时获得${r.turnEnergy}费用。`); + if (r.turnBlock) out.push(`每个自身回合开始时失去${r.turnLoseHp}点生命,再获得${r.turnBlock}点格挡。`); + if (r.attackBlock) out.push(`本次自身回合内,每打出一张攻击牌获得${r.attackBlock}点格挡。`); + if (r.lossStrength) out.push(`在自身回合内每次实际失去生命,获得${r.lossStrength}点力量。`); + if (r.lossDamage) out.push(`自身回合开始时失去${r.infernoLoseHp}点生命;自身回合每次实际失血,对全部敌人造成${r.lossDamage}点非攻击伤害。`); + if (r.retaliation) out.push(`至下个自身回合开始,每受到一次攻击,对攻击者造成${r.retaliation}点伤害。`); + if (r.vulnerableReduction) out.push("至下个自身回合开始,易伤敌人对你的攻击伤害降低50%。"); + if (r.vulnerableBonus) out.push(`你攻击易伤敌人的易伤伤害倍率额外增加${r.vulnerableBonus * 100}%。`); + if (r.exhaust) out.push("消耗。"); + if (r.power) out.push("能力持续本场战斗,打出后退出牌堆。"); + return `梦三:主动使用消耗${cardCost(card || { name: spec.name })}费用。` + out.join(""); + }; + const cards = {}, translate = {}; + for (const spec of ironcladCards) { + const base = spec.base; + const enemyTarget = Boolean(base.damage && !base.all || base.vulnerable); + const allyTarget = Boolean(base.allyStrength || base.allyBlock); + cards[spec.name] = { + type: "basic", fullimage: true, + image: `ext:术樱包/mengsan/assets/cards/${spec.name}.png`, + mengsanCost_shuying: spec.cost, usable: Infinity, + mengsanXCost_shuying: spec.cost === "X", + mengsanInnate_shuying: card => Boolean(ruleOf(spec,card).innate), + mengsanExhaust_shuying: base.exhaust ? card => Boolean(ruleOf(spec, card).exhaust) : false, + mengsanPower_shuying: Boolean(base.power), + selectTarget: enemyTarget || allyTarget ? 1 : -1, + toself: !enemyTarget && !allyTarget, + enable(card, player, event) { + const token = capture(player); + if (!token || isActiveCardUse(event || status.event, player) && !canPayCard(player, card)) return false; + if (base.onlyAttacks && player.getCards("h").some(c => !isAttackCard(c))) return false; + return true; + }, + filterTarget(card, player, target) { + if (!target?.isAlive?.()) return false; + if (enemyTarget) return target !== player && player.isEnemyOf(target); + if (allyTarget) return target !== player && player.isFriendOf(target); + return target === player; + }, + async content(event, trigger, player) { + const token = capture(player), r = ruleOf(spec, event.card), target = event.target; + if (!token || !belongsTo(event, token.root)) return; + advanced.ensure(player); + if(r.advanced){await advanced.play(spec,event,player);return;} + const targetValid = () => valid(player, token) && valid(target, token) && + (enemyTarget ? player.isEnemyOf(target) : allyTarget ? target !== player && player.isFriendOf(target) : target === player); + if (!targetValid()) return; + if (r.minExhaust && exhaustCount(player, token) < r.minExhaust) return; + if (r.loseHp) { await player.loseHp(r.loseHp); if (!targetValid()) return; } + if (r.allyBlock && player.hujia > 0) { await target.changeHujia(player.hujia); if (!targetValid()) return; } + if (r.block) { await player.changeHujia(r.block); if (!targetValid()) return; } + if (r.exhaustTurnBlock && exhaustedThisTurn(player, token)) { + await player.changeHujia(r.block); if (!targetValid()) return; + } + if (r.doubleBlock && player.hujia > 0) { await player.changeHujia(player.hujia); if (!targetValid()) return; } + if (r.chooseExhaust) { await chooseExhaust(player, event, token); if (!targetValid()) return; } + if (r.exhaustNonAttacks) { + await exhaustHands(player, event, token, c => !isAttackCard(c), + () => player.changeHujia(r.blockPerExhaust)); + return; + } + let hits = r.hits || 1; + if (r.hitsPerHpLoss) hits += game.getAllGlobalHistory("changeHp").filter(e => + e.player === player && Number.isFinite(e.changedHp) && e.changedHp < 0 && belongsTo(e, token.root)).length; + if (r.hitsPerExhaust) hits = await exhaustHands(player, event, token); + else if (r.vulnerableHits && target.storage?.mengsanVulnerable_shuying > 0) hits = r.vulnerableHits; + const amount = (r.damage || 0) + (r.perExhaust ? r.perExhaust * exhaustCount(player, token) : 0); + if (!positive(amount) || !positive(hits)) throw new RangeError("铁甲战士攻击数值越界"); + if (amount > 0) for (let i = 0; i < hits && valid(player, token); i++) { + const targets = r.all ? [...token.battle.players].filter(p => p !== player && p.isAlive() && player.isEnemyOf(p)) : [target]; + for (const t of targets) { + if (!valid(player, token)) return; + if (!valid(t, token) || !player.isEnemyOf(t)) continue; + const damage = t.damage(amount, player); + damage.card = event.card; + damage.mengsanAttack_shuying = true; + await damage; + } + } + if (!valid(player, token)) return; + // 自身力量与抽牌不因目标被本牌击杀而丢失;目标状态只施加给存活目标。 + if (targetValid()) { + if (r.weak) applyMengsanDebuff(target, "weak", r.weak, player); + if (r.vulnerable) applyMengsanDebuff(target, "vulnerable", r.vulnerable, player); + if (r.enemyStrength) strength(target, r.enemyStrength, token); + if (r.allyStrength) strength(target, r.allyStrength, token); + if (r.strengthFromVulnerable) strength(player, target.storage?.mengsanVulnerable_shuying || 0, token); + } + await advanced.flushAll(); + if(!valid(player,token))return; + if (r.strength) strength(player, r.strength, token); + if (r.heal) { await player.recover(r.heal); if (!valid(player, token)) return; } + if (r.energy) battleEnergy.grant(player, r.energy, token.battle); + if (r.exhaustTurnEnergy && exhaustedThisTurn(player, token)) battleEnergy.grant(player, r.exhaustTurnEnergy, token.battle); + if (r.draw) { await player.draw(r.draw); if (!valid(player, token)) return; } + if (r.drawUntilSkill) { + const pile = pileOf(player, token); + if (typeof pile?.battleCards !== "function") throw new Error("劫掠缺少个人牌堆接口"); + // 固定当前牌数作为安全上界,抽牌过程不重复消耗空牌堆。 + const limit = pile.battleCards(event.cards).length; + for (let i = 0; i < limit && valid(player, token) && pileOf(player, token) === pile; i++) { + const draw = player.draw(1); + await draw; + const drawn = draw.result?.cards; + if (!Array.isArray(drawn) || !drawn.length || drawn.some(c => !isAttackCard(c))) break; + } + } + if ((r.power || r.turnPower) && valid(player, token)) { + const s = ensure(player, token); + if (r.turnStrength) add(s.powers, "turnStrength", r.turnStrength); + if (r.turnEnergy) add(s.powers, "turnEnergy", r.turnEnergy); + if (r.turnBlock) { add(s.powers, "turnBlock", r.turnBlock); add(s.powers, "turnLoseHp", r.turnLoseHp); } + if (r.lossStrength) add(s.powers, "lossStrength", r.lossStrength); + if (r.lossDamage) { add(s.powers, "lossDamage", r.lossDamage); add(s.powers, "infernoLoseHp", r.infernoLoseHp); } + if (r.vulnerableBonus) add(s.powers, "vulnerableBonus", r.vulnerableBonus); + if (r.attackBlock) add(s.turn, "attackBlock", r.attackBlock); + if (r.retaliation) add(s.defense, "retaliation", r.retaliation); + if (r.vulnerableReduction) s.defense.colossus = true; + player.markSkill(POWER_SKILL); + } + }, + cardPrompt(card) { return prompt(spec, card); }, + ai: { order: base.power ? 8 : 6, result: { target: enemyTarget ? -1 : 1 }, + tag: { damage: base.damage ? 1 : 0, recover: base.heal ? 1 : 0 } }, + }; + translate[spec.name] = spec.title; + translate[`${spec.name}_info`] = prompt(spec, { name: spec.name }); + } + const skills = { + ...advanced.skills, + [POWER_SKILL]: { + trigger: { player: ["phaseBegin", "phaseAfter", "changeHpAfter", "damageBegin3", "damageAfter", "useCardAfter"] }, + forced: true, silent: true, popup: false, priority: 50, + mark: true, marktext: "战", + intro: { content(storage, player) { + const s = stateOf(player); + if (!s) return "本场战斗的铁甲战士效果"; + const names = { turnStrength: "每回合力量", turnEnergy: "每回合费用", turnBlock: "每回合格挡", turnLoseHp: "披风每回合失血", infernoLoseHp: "狱火每回合失血", lossDamage: "失血全体伤害", lossStrength: "失血获得力量", vulnerableBonus: "易伤倍率加成", attackBlock: "攻击获得格挡", retaliation: "反击伤害", colossus: "巨像减伤" }; + return Object.entries({ ...s.powers, ...s.turn, ...s.defense }).map(([k, v]) => `${names[k] || k}:${v === true ? "50%" : k === "vulnerableBonus" ? `${v * 100}%` : v}`).join("
"); + } }, + filter(event, player) { const s = stateOf(player); return Boolean(s && belongsTo(event, s.token.root)); }, + async content(event, trigger, player) { + const s = stateOf(player); + if (!s) return; + const token = s.token; + if (event.triggername === "phaseBegin") { + s.defense = Object.create(null); + s.turn = Object.create(null); + if (s.powers.turnStrength) strength(player, s.powers.turnStrength, token); + if (s.powers.turnEnergy) battleEnergy.grant(player, s.powers.turnEnergy, token.battle); + if (s.powers.turnLoseHp) { await player.loseHp(s.powers.turnLoseHp); if (!valid(player, token)) return; } + if (s.powers.turnBlock) await player.changeHujia(s.powers.turnBlock); + if (valid(player, token) && s.powers.infernoLoseHp) await player.loseHp(s.powers.infernoLoseHp); + } else if (event.triggername === "phaseAfter") { + s.turn = Object.create(null); + } else if (event.triggername === "useCardAfter") { + if (s.turn.attackBlock && status.currentPhase === player && isAttackCard(trigger.card)) await player.changeHujia(s.turn.attackBlock); + } else if (event.triggername === "changeHpAfter") { + const lostHp = status.currentPhase === player && Number.isFinite(trigger.changedHp) && trigger.changedHp < 0; + if (s.powers.lossStrength && lostHp) strength(player, s.powers.lossStrength, token); + if (s.powers.lossDamage && lostHp) { + for (const target of [...token.battle.players]) { + if (!valid(player, token)) return; + if (!valid(target, token) || !player.isEnemyOf(target)) continue; + const damage = target.damage(s.powers.lossDamage, player, "nocard"); + damage.mengsanScriptedSkill_shuying = true; + await damage; + } + } + } else if (event.triggername === "damageBegin3") { + if (s.defense.colossus && trigger.source?.storage?.mengsanVulnerable_shuying > 0 && + (trigger.mengsanAttack_shuying || isAttackCard(trigger.card))) trigger.num = Math.floor(trigger.num * 0.5); + } else if (event.triggername === "damageAfter") { + const attacker = trigger.source; + if (s.defense.retaliation && valid(attacker, token) && attacker !== player && + (trigger.mengsanAttack_shuying || isAttackCard(trigger.card))) { + const damage = attacker.damage(s.defense.retaliation, player, "nocard"); + damage.mengsanScriptedSkill_shuying = true; + await damage; + } + } + if (valid(player, token)) player.markSkill(POWER_SKILL); + }, + }, + }; + translate[POWER_SKILL] = "铁甲战意"; + translate.mengsan_ic_advanced_shuying = "刘备牌组状态"; + translate.mengsan_ic_bridge_shuying = "刘备牌组结算"; + return { cards, translate, skills, names: ironcladCards.map(c => c.name), + onDeath:advanced.onDeath, + vulnerableBonus(source, target) { + return source !== target && source?.isEnemyOf?.(target) ? stateOf(source)?.powers.vulnerableBonus || 0 : 0; + } }; +} diff --git a/mengsan/cards/packs/liubei/traits.js b/mengsan/cards/packs/liubei/traits.js new file mode 100644 index 0000000..6b508fd --- /dev/null +++ b/mengsan/cards/packs/liubei/traits.js @@ -0,0 +1,99 @@ +// 原版分类;Wiki正式版优先于较新测试分支。来源见卡牌分类快照.json。 +// type 留给无名引擎使用;cardType 才是攻击/技能/能力分类。 +export const liubeiCardTraits = Object.freeze({ + sha: Object.freeze({"cardType": "attack", "rarity": "basic"}), + mengsan_zhongsha: Object.freeze({"cardType": "attack", "rarity": "basic"}), + mengsan_fangyu: Object.freeze({"cardType": "skill", "rarity": "basic"}), + mengsan_fennu: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_feijianhuixuanbiao: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_jianbingdaji: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_quanshenzhuangji: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_rongrongzhiquan: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_shandianpili: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_shuangchongdaji: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_tiezhanbo: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_touchui: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_tupo: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_yubeidaji: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_wanmeidaji: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_yujin: Object.freeze({"cardType": "attack", "rarity": "common"}), + mengsan_fangxue: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_jianyi: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_pomie: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_songjianwushi: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_wuzhuang: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_zhanli: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_xueqiang: Object.freeze({"cardType": "skill", "rarity": "common"}), + mengsan_qiling: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_xuanfengzhan: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_yuanhen: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_baozou: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_dismantle: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_ashen_strike: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_pillage: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_hemokinesis: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_uppercut: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_relentless_assault: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_fight_me: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_howl_from_beyond: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_stomp: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_bludgeon: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_demonic_shield: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_rage: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_battle_trance: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_forgotten_ritual: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_infernal_blade: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_colossus: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_burning_pact: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_taunt: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_evil_eye: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_drum_of_battle: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_second_wind: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_dominate: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_flame_barrier: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_eager: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_inflame: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_rupture: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_feel_no_pain: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_ferocity: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_stone_armor: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_inferno: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_juggle: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_stampede: Object.freeze({"cardType": "power", "rarity": "uncommon"}), + mengsan_ic_pacts_end: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_conflagration: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_feed: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_thrash: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_tear_asunder: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_fiend_fire: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_bully: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_offering: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_brand: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_unleash: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_primal_force: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_twin_strike: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_stoke: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_impervious: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_not_yet: Object.freeze({"cardType": "skill", "rarity": "rare"}), + mengsan_ic_cruelty: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_crimson_mantle: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_battle_ready: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_bodyguard: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_hellraiser: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_dark_embrace: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_unshakable: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_juggernaut: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_pyre: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_barricade: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_demon_form: Object.freeze({"cardType": "power", "rarity": "rare"}), + mengsan_ic_clash: Object.freeze({"cardType": "attack", "rarity": "event"}), + mengsan_ic_dual_wield: Object.freeze({"cardType": "skill", "rarity": "event"}), + mengsan_ic_entrench: Object.freeze({"cardType": "skill", "rarity": "event"}), + mengsan_ic_break: Object.freeze({"cardType": "attack", "rarity": "ancient"}), + mengsan_ic_corruption: Object.freeze({"cardType": "power", "rarity": "ancient"}), + mengsan_ic_midnight: Object.freeze({"cardType": "attack", "rarity": "rare"}), + mengsan_ic_blaze: Object.freeze({"cardType": "skill", "rarity": "uncommon"}), + mengsan_ic_rally: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_grapple: Object.freeze({"cardType": "attack", "rarity": "uncommon"}), + mengsan_ic_boulder: Object.freeze({"cardType": "attack", "rarity": "token"}), +}); diff --git a/mengsan/cards/packs/rewards.js b/mengsan/cards/packs/rewards.js new file mode 100644 index 0000000..21c04a6 --- /dev/null +++ b/mengsan/cards/packs/rewards.js @@ -0,0 +1,4 @@ +import { liubeiRewards, liubeiRewardIds } from "./liubei/rewards.js"; +import { colorlessRewards, colorlessRewardIds } from "./colorless/rewards.js"; +export const cardPackRewards = Object.freeze({...liubeiRewards,...colorlessRewards}); +export const cardPackRewardIds = Object.freeze([...liubeiRewardIds,...colorlessRewardIds]); diff --git a/mengsan/cards/packs/shared/data.js b/mengsan/cards/packs/shared/data.js new file mode 100644 index 0000000..f10ec2f --- /dev/null +++ b/mengsan/cards/packs/shared/data.js @@ -0,0 +1,6 @@ +import { colorlessCards } from "../colorless/data.js"; +import { eventCards } from "../event/data.js"; +import { statusCards } from "../status/data.js"; +import { curseCards } from "../curse/data.js"; +export const sharedCards = Object.freeze([...colorlessCards,...eventCards,...statusCards,...curseCards]); +export const sharedByName = Object.freeze(Object.fromEntries(sharedCards.map(c=>[c.name,c]))); diff --git a/mengsan/cards/packs/shared/runtime.js b/mengsan/cards/packs/shared/runtime.js new file mode 100644 index 0000000..970fd48 --- /dev/null +++ b/mengsan/cards/packs/shared/runtime.js @@ -0,0 +1,594 @@ +import { sharedCards, sharedByName } from "./data.js"; +import { cardDefinitions } from "../../card-definitions.js"; +import { witherDamage } from "../../wither-card.js"; +import { hasCardAffix } from "../../intrinsic-affixes.js"; +import { createCardData } from "../../card-data.js"; +import { cardUpgradeLevel, cardUpgradeRule, canUpgradeCard } from "../../upgrades.js"; +import { generationPool } from "../../card-pools.js"; +import { cardPackCosts } from "../data.js"; +import { ironcladStrengthPenalty } from "../../ironclad-hooks.js"; +import { installSharedHookResolver } from "../../shared-hooks.js"; +import { isAttackCard } from "../../../monsters/vine-tangled.js"; +import { applyMengsanDebuff, consumeArtifact } from "../../../monsters/artifact-status.js"; +import { isStunned } from "../../../battle/stun-intent.js"; +import { canPayCard, isActiveCardUse, authorizeFreeCardUse, takeXCardUse, cardCost } from "../../../battle/combat-rules.js"; + +const BRIDGE="mengsan_shared_bridge_shuying", RULES="mengsan_shared_rules_shuying", END="mengsan_shared_end_shuying", LIMIT="mengsan_shared_limit_shuying", AFTER="mengsan_shared_after_shuying"; +const safe=n=>Number.isSafeInteger(n) && n>=0; +function belongs(event,root) { + const seen=new Set(); + for(let e=event;e&&!seen.has(e);e=e.parent){if(e===root)return true;seen.add(e);} + return false; +} +function nearestUse(event) { + const seen=new Set(); + for(let e=event;e&&!seen.has(e);e=e.parent){if(e.name==="useCard")return e;seen.add(e);} + return null; +} +const physical=card=>card?.cards?.length===1 && card.cards[0]?.name===card.name ? card.cards[0] : card; +const rule=card=>{const c=sharedByName[card?.name];return c ? cardUpgradeLevel(card) && c.upgraded ? c.upgraded : c.base : null;}; + +export function createSharedCardRuntime({game,get,lib,status,getBattle,battleEnergy,temporaryStrength,random}) { + const states=new WeakMap(), uses=new WeakMap(), replays=new WeakMap(), costCaps=new WeakMap(), ballBonuses=new WeakMap(); + const seenUses=new WeakSet(), seenDraws=new WeakSet(), seenEnds=new WeakSet(), kills=new WeakMap(); + const tokenOf=player=>{ + const b=getBattle(); + return b?.session?.active && b.root && b.players?.has(player) && player?.isAlive?.() + ? {battle:b,session:b.session,root:b.root,run:status.mengsanRun_shuying} : null; + }; + const valid=(player,t)=>Boolean(t && getBattle()===t.battle && t.session.active && + t.battle.session===t.session && t.battle.root===t.root && t.battle.players.has(player) && player?.isAlive?.()); + const pileOf=s=>s.player===game.me ? s.token.battle.personalPiles : s.token.battle.monsterPiles?.get(s.player); + const stateOf=player=>{const s=states.get(player);return s&&valid(player,s.token)?s:null;}; + const add=(o,k,n)=>{const next=(o[k]||0)+n;if(!safe(next))throw new RangeError("通用牌数值越界");o[k]=next;}; + const friends=s=>[...s.token.battle.players].filter(p=>valid(p,s.token) && (p===s.player || s.player.isFriendOf(p))); + const enemies=s=>[...s.token.battle.players].filter(p=>valid(p,s.token) && p!==s.player && s.player.isEnemyOf(p)); + const refresh=s=>{if(valid(s.player,s.token)){s.token.battle.handUI?.refresh();s.token.battle.pilesUI?.refresh();}}; + function ensure(player) { + const token=tokenOf(player);if(!token)return null; + let s=stateOf(player);if(s)return s; + s={player,token,powers:Object.create(null),turn:Object.create(null),freeTurn:new WeakSet(),freeBattle:new WeakSet(), + nextTurn:[],bombs:[],negativeStrength:0,autoCount:0,drawCount:0,returns:[],maulBonus:0,vigor:0,queuedAllyEnergy:0}; + states.set(player,s);player.addSkill(RULES); + token.session.ownResource(s,()=>{if(states.get(player)===s){states.delete(player);player.removeSkill(RULES);s.nextTurn.length=0;s.returns.length=0;}}); + return s; + } + const rand=(s,list)=>{ + if(!list.length)return null; + const v=random(s.token.run);if(!Number.isFinite(v)||v<0||v>=1)throw new RangeError("通用牌随机数无效"); + return list[Math.floor(v*list.length)]; + }; + function sample(s,list,n) { + const bag=list.slice(),out=[]; + while(bag.length && out.lengthc.owner===s.token.run?.player?.character).map(c=>c.deck); + const names=generationPool(cardDefinitions,onlyColorless?["colorless"]:[...new Set(ownDecks),"colorless"]); + return names.filter(name=>(!type || cardDefinitions[name].cardType===type) && lib.card[name]); + } + function generated(s,name,upgrade=0) { + const b=s.token.battle,n=(b.generatedCardSerial||0)+1;if(!safe(n))throw new RangeError("生成牌序号越界");b.generatedCardSerial=n; + return createCardData({id:`mengsan_generated_${s.token.session.id}_${n}`,name,suit:"spade",number:1,nature:null,affixes:[], + upgrade:Math.min(upgrade,cardUpgradeRule(name)?.maxLevel||0)},{enemy:true}); + } + async function generateHand(s,name,{upgrade=0,free=false}={}) { + if(!valid(s.player,s.token)||!name)return null; + const c=await pileOf(s)?.addToHand(generated(s,name,upgrade)); + if(c && valid(s.player,s.token) && free)s.freeTurn.add(c); + refresh(s);return c; + } + async function choose(s,list,prompt,{max=1,optional=false}={}) { + if(!valid(s.player,s.token)||!list.length)return []; + const count=Math.min(max,list.length); + const answer=await s.player.chooseButton({createDialog:[prompt,list],forced:!optional,selectButton:optional?[0,count]:[1,count], + ai:button=>typeof button.link==="string" ? 1 : get.value(button.link,s.player)}).forResult(); + if(!valid(s.player,s.token))return []; + return [...new Set(answer.links||[])].filter(c=>list.includes(c)).slice(0,count); + } + async function chooseGenerated(s,{type=null,upgrade=0}={}) { + const names=sample(s,pool(s,type),3); + // Native virtual-card buttons preserve Chinese title and the registered card image. + const buttons=names.map(name=>["", "", name]); + if(!buttons.length)return; + const result=await s.player.chooseButton({createDialog:["选择一张牌加入手牌",[buttons,"vcard"]],forced:true,selectButton:1, + ai:button=>get.value({name:button.link[2]},s.player)}).forResult(); + if(!valid(s.player,s.token))return; + const name=result.links?.[0]?.[2];if(names.includes(name))await generateHand(s,name,{upgrade,free:true}); + } + function strength(s,player,n) { + if(!valid(player,s.token)||!safe(n))return; + const next=(player.storage.mengsanStrength_shuying||0)+n;if(!safe(next))throw new RangeError("力量数值越界"); + player.storage.mengsanStrength_shuying=next;player.addSkill("mengsan_raider_strength_shuying");player.markSkill("mengsan_raider_strength_shuying"); + } + async function hit(s,target,n,card=null,attack=false) { + if(!safe(n))throw new RangeError("通用牌伤害无效"); + if(!n||!valid(s.player,s.token)||!valid(target,s.token)||!s.player.isEnemyOf(target))return 0; + const e=target.damage(n,s.player);if(card)e.card=card;if(attack)e.mengsanAttack_shuying=true; + const gold=rule(card)?.killGold;if(gold)kills.set(e,{s,target,gold,claimed:false}); + // Fisticuffs' detail page explicitly includes damage absorbed by the target's block. + await e;return e._cancelled?0:Math.max(0,e.num||0); + } + async function hitAll(s,n) {for(const t of enemies(s)){if(!valid(s.player,s.token))break;await hit(s,t,n);}} + function origin(event) {const use=nearestUse(event);return use?.mengsanSharedOriginal_shuying || use?.mengsanIcRepeatOriginal_shuying || use?.cards?.find(c=>c.name===use.card?.name) || physical(event.card);} + function ballBonus(s,card) { + const c=physical(card),entry=ballBonuses.get(c),data=c?.storage?.mengsanCard_shuying||c; + return entry?.session===s.token.session ? entry.amount : data?.ballSession===s.token.session.id&&safe(data.ballBonus) ? data.ballBonus : 0; + } + function snapshot(use) { + const original=origin(use),data=original?.storage?.mengsanCard_shuying || {}; + return {original,card:{name:use.card.name,suit:use.card.suit,number:use.card.number,nature:use.card.nature}, + data:{...data,name:use.card.name,upgrade:cardUpgradeLevel(use.card),affixes:[...(data.affixes||[])]},targets:[...(use.targets||[])]}; + } + async function autoPlay(s,card,{echo=null,targets=null}={}) { + if(!valid(s.player,s.token))return false; + if(++s.autoCount>1024)throw new Error("通用牌自动结算超过安全上限"); + const pile=pileOf(s),info=lib.card[card?.name]; + const movable=!echo && (pile?.drawCards().includes(card)||pile?.discardCards().includes(card)); + const skip=()=>{if(movable&&pile.beginAutoPlay(card))pile.finishAutoPlay(card);return false;}; + if(!info || info.enable===false || hasCardAffix(card,"unplayable"))return skip(); + let selected=info.notarget?[]:targets?.filter(t=>valid(t,s.token) && + (typeof info.filterTarget!=="function" || info.filterTarget(card,s.player,t))); + if(!selected){ + const candidates=[...s.token.battle.players].filter(t=>valid(t,s.token) && + (typeof info.filterTarget!=="function" || info.filterTarget(card,s.player,t))); + selected=info.toself?[s.player]:info.selectTarget===-1?candidates:[rand(s,candidates)].filter(Boolean); + } + if(!selected.length&&!info.notarget)return skip(); + if(movable&&!pile.beginAutoPlay(card))return false; + let release=()=>{}; + try { + const played=echo?{...echo.card,storage:{mengsanCard_shuying:{...echo.data,affixes:echo.data.affixes.slice()}}}:card; + const use=s.player.useCard({card:played,cards:echo?[]:[card],targets:selected,addCount:false}); + use.mengsanSharedAuto_shuying=true;use.addCount=false; + if(echo){use.mengsanSharedEcho_shuying=true;use.mengsanSharedOriginal_shuying=echo.original;} + release=authorizeFreeCardUse(s.player,use.card,use,()=>valid(s.player,s.token)); + use.oncard=()=>{if(!valid(s.player,s.token)||!lib.filter.cardEnabled(use.card,s.player)||selected.some(t=>!s.player.canUse(use.card,t,false)))use.cancel();}; + await use;return !use._cancelled; + } finally {release();if(movable)pile.finishAutoPlay(card);} + } + function debitGold(s,n) { + if(s.player!==game.me || !safe(s.token.run?.player?.gold))return; + s.token.run.player.gold=Math.max(0,s.token.run.player.gold-n); + } + async function afterDraw(s,cards) { + if(!valid(s.player,s.token))return; + for(const c of cards||[])if(sharedByName[c.name]?.id==="void"){ + s.player.storage.mengsanEnergy_shuying=Math.max(0,(s.player.storage.mengsanEnergy_shuying||0)-1); + } + if(s.powers.automation){ + const n=s.drawCount+(cards?.length||0),gained=Math.floor(n/10);s.drawCount=n%10; + if(gained)battleEnergy.grant(s.player,gained*s.powers.automation,s.token.battle); + } + refresh(s); + } + function cost(s,card,amount) { + const c=physical(card),cap=costCaps.get(c); + if(cap?.session===s.token.session && (cap.permanent || cap.turn===s.turn))amount=Math.min(amount,1); + if(cardDefinitions[card?.name]?.cardType==="power")amount=Math.max(0,amount-(s.powers.powerDiscount||0)); + return amount; + } + function cardBlock(s,n,event) { + if(!(n>0))return n; + // Stop at an explicit skill boundary so powers/relics cannot borrow an outer card's source. + const seen=new Set(); + for(let e=event;e&&!seen.has(e);e=e.parent){ + seen.add(e);if(e.skill || lib.skill[e.name])return n; + if(e.name==="useCard" && e.player===s.player){ + if(s.powers.noCardBlock>0)return 0; + const title=lib.translate[e.card?.name]||""; + return n+(s.powers.dexterity||0)+(title.includes("防御")?(s.powers.defendBonus||0):0); + } + } + return n; + } + installSharedHookResolver(player=>{ + const s=ensure(player);if(!s)return null; + return {isFree:card=>s.freeTurn.has(physical(card))||s.freeBattle.has(physical(card)),cost:(card,n)=>cost(s,card,n), + strengthPenalty:()=>s.negativeStrength,blockAmount:(n,e)=>cardBlock(s,n,e),afterDraw:cards=>afterDraw(s,cards), + snapshotCard:card=>sharedByName[card.name]?.id==="the_ball"?{ballBonus:ballBonus(s,card),ballSession:s.token.session.id}:{}, + copyCard(source,target){ + if(sharedByName[source.name]?.id==="the_ball"&&target.name===source.name)ballBonuses.set(target,{session:s.token.session,amount:ballBonus(s,source)}); + if(s.freeBattle.has(source))s.freeBattle.add(target); + const cap=costCaps.get(source);if(cap?.session===s.token.session&&cap.permanent)costCaps.set(target,{...cap}); + }}; + }); + + async function play(spec,event,player) { + const s=ensure(player);if(!s || !belongs(event,s.token.root))return; + const r=rule(event.card),id=spec.id,target=event.target,pile=pileOf(s),original=origin(event); + if(!r)return; + if(r.damage!==undefined || ["mind_blast","gold_axe","volley"].includes(id)){ + let amount=r.damage||0; + if(id==="mind_blast")amount=pile?.drawCards().length||0; + if(id==="gold_axe")amount=s.token.battle.sharedCardUses||0; + if(id==="gang_up")amount+=r.perAllyAttack*(s.token.battle.sharedAttackRecords||[]).filter(x=>x.turn===status.currentPhase&&x.source!==player&&player.isFriendOf(x.source)&&x.targets.includes(target)).length; + if(id==="rend"){ + const bad=["mengsanVulnerable_shuying","mengsanWeak_shuying","mengsanFrail_shuying","mengsanTangled_shuying","mengsanShrink_shuying","mengsanConstrict_shuying"]; + let kinds=bad.filter(k=>target?.storage?.[k]>0).length+(ironcladStrengthPenalty(target)>0?1:0)+(isStunned(target)?1:0); + if(s.token.battle.sharedKnockdowns?.some(mark=>mark.target===target&&valid(mark.owner,s.token)))kinds++; + if([...s.token.battle.players].some(p=>stateOf(p)?.turn.choking?.has(target)))kinds++; + amount+=r.perDebuff*kinds; + } + if(id==="maul")amount+=s.maulBonus; + if(id==="the_ball")amount+=ballBonus(s,original); + let count=r.hits||1;if(id==="volley")count=takeXCardUse(player,event,s.token.battle)??0; + let actual=0; + for(let i=0;it!==target))await hit(s,t,actual); + if(id==="fisticuffs"&&actual>0&&valid(player,s.token))await player.changeHujia(actual); + if(id==="maul")add(s,"maulBonus",r.maulGrowth); + if(id==="the_ball"&&valid(player,s.token)){ + const next=ballBonus(s,original)+r.ballGrowth;if(!safe(next))throw new RangeError("魔球伤害数值越界"); + ballBonuses.set(original,{session:s.token.session,amount:next}); + const use=nearestUse(event),entry=uses.get(use); + if(entry&&!use.mengsanSharedEcho_shuying&&!use.mengsanIcEcho_shuying)entry.ballHandoff=true; + } + } + if(!valid(player,s.token))return; + if(r.block)await player.changeHujia(r.block); + if(!valid(player,s.token))return; + if(r.energy)battleEnergy.grant(player,r.energy,s.token.battle); + if(r.draw)await player.draw(r.draw); + if(!valid(player,s.token))return; + for(const t of r.all?enemies(s):[target])if(valid(t,s.token)){ + if(r.weak)applyMengsanDebuff(t,"weak",r.weak,player); + if(r.vulnerable)applyMengsanDebuff(t,"vulnerable",r.vulnerable,player); + } + if(r.strength)strength(s,player,r.strength); + if(r.dexterity)add(s.powers,"dexterity",r.dexterity); + if(r.temporaryStrength)temporaryStrength.grant(player,r.temporaryStrength,event,s.token.battle); + if(r.negativeStrength && valid(target,s.token) && !consumeArtifact(target))add(ensure(target),"negativeStrength",r.negativeStrength); + if(r.allyBlock && valid(target,s.token))await target.changeHujia(r.allyBlock); + if(r.allyEnergy && valid(target,s.token)){ + // Each actor restores energy on its own turn; keep an off-turn gift until that restoration finishes. + if(status.currentPhase===target)battleEnergy.grant(target,r.allyEnergy,s.token.battle); + else add(ensure(target),"queuedAllyEnergy",r.allyEnergy); + } + if(r.teamStrength)for(const p of friends(s).filter(p=>p!==player))temporaryStrength.grant(p,r.teamStrength,event,s.token.battle,{ownTurn:true}); + if(r.teamDraw)for(const p of friends(s)){if(!valid(player,s.token))return;await p.draw(r.teamDraw);} + if(r.teamBlock)for(const p of friends(s)){if(!valid(player,s.token))return;await p.changeHujia(r.teamBlock);} + if(id==="impatience" && !player.getCards("h").some(isAttackCard))await player.draw(r.conditionalDraw); + if(id==="restlessness" && !player.getCards("h").length){await player.draw(r.emptyDraw);if(valid(player,s.token))battleEnergy.grant(player,r.emptyEnergy,s.token.battle);} + if(r.chooseExhaust){ + const selected=await choose(s,player.getCards("h"),"净化:选择要消耗的手牌",{max:r.chooseExhaust,optional:true}); + for(const c of selected){if(!valid(player,s.token))return;await pile?.exhaustFromHand(c);} + } + if(id==="thinking_ahead"){ + const [c]=await choose(s,player.getCards("h"),"深谋远虑:将一张手牌放回堆顶"); + if(c)await pile?.moveHandToTop(c); + } + if(id==="seeker_strike" || ["secret_technique","secret_weapon","wish"].includes(id)){ + let list=pile?.drawCards()||[]; + if(id==="seeker_strike")list=sample(s,list,3); + if(id==="secret_technique")list=list.filter(c=>cardDefinitions[c.name]?.cardType==="skill"); + if(id==="secret_weapon")list=list.filter(isAttackCard); + const [c]=await choose(s,list,"选择抽牌堆中的一张牌加入手牌");if(c)await pile?.gainFromDraw(c); + } + if(r.chooseDiscard){ + const selected=await choose(s,pile?.discardCards()||[],"涅奥之怒:取回至多指定数量的牌",{max:r.chooseDiscard,optional:true}); + for(const c of selected){if(!valid(player,s.token))return;await pile?.gainToHand(c);} + } + if(id==="anointed")for(const c of pile?.drawCards().filter(c=>cardDefinitions[c.name]?.rarity==="rare")||[]){if(!valid(player,s.token))return;await pile.gainFromDraw(c);} + if(id==="scrawl"){ + const limit=player.getHandcardLimit(),count=Math.max(0,limit-player.countCards("h")); + if(safe(count)&&count)await player.draw(count); + } + if(id==="mimic"&&valid(target,s.token)&&target.hujia>0)await player.changeHujia(target.hujia); + if(r.generate)for(let i=0;i{ + // Jackpot+ upgrades the result, but its pool still requires a zero-cost base card. + return cardPackCosts[name]===0; + }); + for(let i=0;i!replays.has(c)));if(c)replays.set(c,{session:s.token.session,count:r.replay});} + if(id==="thrumming_hatchet"||id==="bolas")s.returns.push(original); + if(id==="prolong")s.nextTurn.push({block:Math.max(0,player.hujia||0)}); + if(r.futureBlock)s.nextTurn.push({block:r.futureBlock,remaining:r.futureTurns}); + if(r.nextDraw||r.nextEnergy)s.nextTurn.push({draw:r.nextDraw,energy:r.nextEnergy}); + if(id==="panic_button"&&!consumeArtifact(player))s.powers.noCardBlock=2; + if(id==="equilibrium"||id==="salvo")s.turn.retain=true; + if(r.bombDamage)s.bombs.push({turns:3,damage:r.bombDamage}); + if(r.intangible)add(s.powers,"intangible",r.intangible); + if(id==="the_gambit")s.powers.gambit=true; + if(id==="enlightenment")for(const c of player.getCards("h"))costCaps.set(c,{session:s.token.session,permanent:cardUpgradeLevel(event.card)>0,turn:s.turn}); + if(id==="brightest_flame"){ + // The detail page applies full-health HP loss before reducing the maximum. + if(player.hp===player.maxHp)await player.loseHp(1); + if(!valid(player,s.token))return; + await player.loseMaxHp(1); + if(player===game.me && safe(player.maxHp))s.token.run.player.maxHp=player.maxHp; + } + if(id==="apotheosis"){ + for(const c of pile?.battleCards(event.cards||[])||[]){ + const d=c.storage?.mengsanCard_shuying;if(d&&canUpgradeCard(d))d.upgrade=(d.upgrade||0)+1; + } + // Exhausted cards keep real nodes; refresh their serialized viewer data after upgrading them. + pile?.syncExhaustSnapshots(); + } + if(id==="deprecated_card"&&player===game.me){const cid=original?.storage?.mengsanCard_shuying?.id;s.token.run.player.deck=s.token.run.player.deck.filter(c=>c.id!==cid);} + if(id==="whistle"&&valid(target,s.token))game.mengsanStunEnemy_shuying(target); + if(id==="tag_team"&&valid(target,s.token)){s.token.battle.sharedTags||=[];s.token.battle.sharedTags.push({owner:player,target});} + if(r.allyDamageMultiplier&&valid(target,s.token)&&!consumeArtifact(target)){s.token.battle.sharedKnockdowns||=[];s.token.battle.sharedKnockdowns.push({owner:player,target,multiplier:r.allyDamageMultiplier});} + if(id==="intercept"&&valid(target,s.token))s.turn.intercept=target; + if(r.choking&&valid(target,s.token)&&!consumeArtifact(target)){s.turn.choking||=new Map();s.turn.choking.set(target,(s.turn.choking.get(target)||0)+r.choking);} + if(spec.cardType==="power"){ + for(const k of ["panache","defendBonus","turnVigor","automation","powerDiscount","plating"])if(r[k])add(s.powers,k,r[k]); + if(r.boulder)add(s.powers,"boulder",r.boulder); + for(const k of ["nostalgia","entropy","beacon_of_hope","mayhem","calamity"])if(id===k)add(s.powers,k,1); + if(r.battleUpgrade && player===game.me){const run=s.token.run;run.sharedBattleCardEffects||={upgrades:0};add(run.sharedBattleCardEffects,"upgrades",r.battleUpgrade);} + } + refresh(s); + } + + async function onUse(use) { + const s=ensure(use.player);if(!s || seenUses.has(use))return;seenUses.add(use); + const b=s.token.battle,entry={s,snapshot:snapshot(use),vigor:0,echoes:[]};uses.set(use,entry); + b.sharedPendingUses||=new Set();b.sharedPendingUses.add(use); + // Death can omit useCardAfter/Cancelled while the native use event still + // drains its after queue. Release only this use's entry after that unwind; + // onUseAfter is idempotent and ignores effects owned by a dead player. + use.insertAfter(async () => { + await onUseAfter(use); + }, { forceDie: true }); + if(game.me.isFriendOf(s.player))b.sharedCardUses=(b.sharedCardUses||0)+1; + if(isAttackCard(use.card)){ + b.sharedAttackRecords||=[];entry.attackRecord={turn:status.currentPhase,source:s.player,targets:(use.targets||[]).filter(t=>valid(t,s.token)&&s.player.isEnemyOf(t))};b.sharedAttackRecords.push(entry.attackRecord); + entry.vigor=s.vigor;s.vigor=0; + if(s.powers.calamity)for(let i=0;i!consumed.includes(t)); + } + } + if(s.powers.nostalgia && (s.turn.nostalgia||0)p!==s.player).map(ensure) + .filter(other=>pileOf(other)?.canReceiveTransfer(card,s.token.session)); + const recipient=rand(s,recipients); + if(recipient){ + const turnFree=s.freeTurn.has(card),battleFree=s.freeBattle.has(card); + if(await source?.transferTo(card,pileOf(recipient))){ + if(turnFree)recipient.freeTurn.add(card);if(battleFree)recipient.freeBattle.add(card); + refresh(recipient); + } + } + } + } + } finally { + b.sharedPendingUses?.delete(use); + if(getBattle()===b && b.session.active && !b.sharedPendingUses?.size){ + if(b.sharedDeferredVictory && game.me?.isAlive()){b.sharedDeferredVictory=false;game.mengsanFinishBattle_shuying();} + else game.checkResult(); + } + } + } + async function turnStart(player) { + const s=ensure(player);if(!s)return; + s.turn=Object.create(null);s.freeTurn=new WeakSet();s.autoCount=0; + if(s.queuedAllyEnergy){battleEnergy.grant(player,s.queuedAllyEnergy,s.token.battle);s.queuedAllyEnergy=0;} + if(s.powers.plating)s.powers.plating=Math.max(0,s.powers.plating-1); + if(s.powers.intangible)s.powers.intangible=Math.max(0,s.powers.intangible-1); + add(s,"vigor",s.powers.turnVigor||0); + s.token.battle.sharedKnockdowns=(s.token.battle.sharedKnockdowns||[]).filter(e=>e.owner!==player); + const next=s.nextTurn.splice(0); + for(const e of next){ + if(!valid(player,s.token))return; + if(e.block)await player.changeHujia(e.block); + if(!valid(player,s.token))return; + if(e.draw)await player.draw(e.draw); + if(!valid(player,s.token))return; + if(e.energy)battleEnergy.grant(player,e.energy,s.token.battle); + if(e.remaining>1)s.nextTurn.push({...e,remaining:e.remaining-1}); + } + for(const c of s.returns.splice(0)){if(!valid(player,s.token))return;await pileOf(s)?.returnToHand(c);} + if(s.powers.boulder){ + if(!valid(player,s.token))return; + await hitAll(s,s.powers.boulder);add(s.powers,"boulder",5); + } + if(s.powers.entropy && pileOf(s))for(let i=0;i!hasCardAffix(c,"eternal")),"熵:选择变化一张手牌"); + if(c && player.getCards("h").includes(c)){ + const name=rand(s,pool(s).filter(name=>name!==c.name)); + if(name)pileOf(s).transformHand(c,generated(s,name)); + } + } + refresh(s); + } + async function afterDrawPhase(player) { + const s=stateOf(player);if(!s?.powers.mayhem)return; + for(let i=0;isharedByName[c.name]?.id==="enthralled"))return true; + return held.some(c=>sharedByName[c.name]?.id==="normality") && (s.turn.plays||0)>=3; + } + const cards={},translate={}; + for(const spec of sharedCards){ + const ally=["believe_in_you","lift","mimic","intercept"].includes(spec.id); + const enemy=spec.cardType==="attack"&&!spec.base.all&&spec.id!=="volley" || spec.id==="dark_shackles"; + cards[spec.name]={type:spec.cardType==="status"?"status":"basic",fullimage:true, + mengsanShared_shuying:true, + image:`ext:术樱包/mengsan/assets/cards/${spec.name}.png`,usable:Infinity, + mengsanCost_shuying:spec.cost,mengsanXCost_shuying:spec.cost==="X", + mengsanInnate_shuying:card=>Boolean(rule(card)?.innate),mengsanExhaust_shuying:card=>Boolean(rule(card)?.exhaust), + mengsanPower_shuying:spec.cardType==="power",selectTarget:enemy||ally?1:-1,toself:!enemy&&!ally, + enable:spec.base.unplayable ? false : function(card,player,event){return Boolean(tokenOf(player)&&!hasCardAffix(card,"unplayable")&&!limited(player,card,event||status.event)&& + (!isActiveCardUse(event||status.event,player)||canPayCard(player,card)));}, + filterTarget(card,player,target){return Boolean(target?.isAlive?.() && (enemy?target!==player&&player.isEnemyOf(target):ally?target!==player&&player.isFriendOf(target):target===player));}, + content:async(event,trigger,player)=>play(spec,event,player), + cardPrompt(card){const r=rule(card||{name:spec.name});let description=r?.description||spec.description; + if(spec.id==="the_ball"){const s=stateOf(game.me);if(s)description=description.replace("造成10点伤害",`造成${10+ballBonus(s,card)}点伤害`);} + if(spec.id==="wither")description=description.replace("受到3点伤害",`受到${witherDamage(card)}点伤害`); + return `梦三:${spec.cost==null?"不能被打出":`${spec.cost==="X"?"X":cardCost(card||{name:spec.name})}费`}。${description}`;}, + ai:{order:spec.cardType==="power"?7:6,value:spec.cardType==="curse"?0:5,useful:spec.cardType==="curse"?0:5, + result:{target:enemy?-1:1},tag:spec.cardType==="attack"?{damage:1}:{}}, + }; + translate[spec.name]=spec.title;translate[`${spec.name}_info`]=cards[spec.name].cardPrompt({name:spec.name}); + } + const skills={ + [RULES]:{charlotte:true,popup:false, + mod:{ + cardEnabled(card,player){if(limited(player,card,status.event))return false;}, + cardEnabled2(card,player){if(limited(player,card,status.event))return false;}, + ignoredHandcard(card,player){if(stateOf(player)?.turn.retain)return true;}, + }, + }, + [END]:{trigger:{global:"phaseAfter"},forced:true,silent:true,popup:false,priority:-300, + filter(event){return Boolean(stateOf(event.player)?.pendingDebuffs);}, + async content(event,trigger){const s=stateOf(trigger.player);if(!s)return;const pending=s.pendingDebuffs;delete s.pendingDebuffs; + for(const kind of ["frail","weak"])if(pending[kind])applyMengsanDebuff(s.player,kind,pending[kind]);refresh(s);}, + }, + [LIMIT]:{trigger:{global:"useCard0"},forced:true,silent:true,popup:false,firstDo:true,priority:12000, + filter(event){const b=getBattle();return Boolean(b?.session.active && belongs(event,b.root) && limited(event.player,event.card,event));}, + async content(event,trigger){trigger.cancel();}, + }, + [AFTER]:{trigger:{global:["useCardAfter","useCardCancelled"]},forced:true,silent:true,popup:false,priority:-500, + filter(event){return uses.has(event);},async content(event,trigger){await onUseAfter(trigger);}, + }, + [BRIDGE]:{group:[END,LIMIT,AFTER],trigger:{global:["useCard1","phaseBefore","phaseDrawAfter","phaseDiscardBegin","phaseJieshuBegin","phaseAfter","drawAfter","damageBegin1","damageBegin2","damageBegin4","loseHpBegin","damage","changeHujiaAfter"]}, + forced:true,silent:true,popup:false,priority:300, + filter(event){const b=getBattle();return Boolean(b?.session.active&&belongs(event,b.root));}, + async content(event,trigger){ + const name=event.triggername,b=getBattle();if(!b?.session.active)return; + if(name==="useCard0"){if(limited(trigger.player,trigger.card,trigger))trigger.cancel();return;} + if(name==="useCard1"){await onUse(trigger);return;} + if(name==="phaseBefore"){await turnStart(trigger.player);return;} + if(name==="phaseDrawAfter"){await afterDrawPhase(trigger.player);return;} + if(name==="phaseDiscardBegin"){await handEnd(trigger);return;} + if(name==="phaseJieshuBegin"){await turnEnd(trigger.player);return;} + if(name==="phaseAfter"){clearTurn(trigger.player);return;} + if(name==="drawAfter"){ + if(!trigger.mengsanIcDrawHandled_shuying&&!seenDraws.has(trigger)){seenDraws.add(trigger);const s=ensure(trigger.player);if(s)await afterDraw(s,trigger.result?.cards||trigger.cards||[]);}return; + } + if(name==="damageBegin1"){ + const use=nearestUse(trigger),entry=uses.get(use); + if(entry?.attackRecord && entry.s.player===trigger.source && valid(trigger.player,entry.s.token) && trigger.source.isEnemyOf(trigger.player) && + !entry.attackRecord.targets.includes(trigger.player))entry.attackRecord.targets.push(trigger.player); + if(entry && entry.s.player===trigger.source && (trigger.mengsanAttack_shuying||isAttackCard(trigger.card)))trigger.num+=entry.vigor; + if(trigger.source)for(const mark of b.sharedKnockdowns||[])if(mark.target===trigger.player&&valid(mark.owner,entry?.s.token||tokenOf(mark.owner))&&mark.owner!==trigger.source&&mark.owner.isFriendOf(trigger.source))trigger.num*=mark.multiplier; + return; + } + if(name==="damageBegin4"||name==="loseHpBegin"){ + const s=stateOf(trigger.player);if(s?.powers.intangible&&trigger.num>1)trigger.num=1;return; + } + if(name==="damageBegin2" && (trigger.mengsanAttack_shuying||isAttackCard(trigger.card))){ + // Wiki's interception is attack damage suppression + doubled holder damage, not native retargeting. + let multiplier=1; + for(const p of b.players){const s=stateOf(p);if(!s?.turn.intercept || !valid(p,s.token) || !p.isEnemyOf(trigger.source))continue; + if(trigger.player===s.turn.intercept)multiplier=0; + if(trigger.player===p)multiplier*=2; + } + trigger.num=Math.floor(trigger.num*multiplier);return; + } + if(name==="damage"){ + const s=stateOf(trigger.player); + // This native signal is after block/HP change and before ordinary dying rescue. + // Clear before forcing death so immunity cannot cause a second kill after rescue. + if(s?.powers.gambit && trigger.num>(trigger.hujia||0) && (trigger.mengsanAttack_shuying||isAttackCard(trigger.card))){s.powers.gambit=false;await trigger.player.die(trigger.source);}return; + } + if(name==="changeHujiaAfter"){ + const s=stateOf(trigger.player); + if(s?.powers.beacon_of_hope && status.currentPhase===s.player && trigger.num>0 && !trigger.mengsanSharedBeacon_shuying){ + const n=Math.floor(trigger.num/2)*s.powers.beacon_of_hope; + if(n)for(const p of friends(s).filter(p=>p!==s.player)){if(!valid(s.player,s.token))return;const e=p.changeHujia(n);e.mengsanSharedBeacon_shuying=true;await e;} + }return; + } + }, + }, + }; + return {cards,translate,skills,names:sharedCards.map(c=>c.name), + async onDeath(event){ + const seen=new Set();for(let e=event;e&&!seen.has(e);e=e.parent){ + seen.add(e);const entry=kills.get(e);if(!entry||entry.claimed||entry.target!==event.player)continue; + entry.claimed=true;const {s,gold}=entry; + if(valid(s.player,s.token)&&s.player===game.me&&entry.target.isDead()){ + const run=s.token.run;add(run.player,"gold",gold);add(run.statistics,"goldEarned",gold); + }break; + } + }, + }; +} diff --git a/mengsan/cards/packs/status/data.js b/mengsan/cards/packs/status/data.js new file mode 100644 index 0000000..78d8687 --- /dev/null +++ b/mengsan/cards/packs/status/data.js @@ -0,0 +1,179 @@ +// Wiki Data:Card.tabx 保存快照;运行时效果由共用模块实现。 +export const statusCards = Object.freeze([ + { + "id": "toxic", + "name": "mengsan_status_toxic", + "title": "毒素", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": 1, + "description": "在你的回合结束时,如果这张牌在你的手牌中,你受到5点伤害。消耗。", + "base": { + "exhaust": true, + "endDamage": 5 + } + }, + { + "id": "beckon", + "name": "mengsan_status_beckon", + "title": "呼唤", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": 1, + "description": "在你的回合结束时,如果这张牌在你的手牌中,你失去6点生命。", + "base": { + "endLoseHp": 6 + } + }, + { + "id": "slimed", + "name": "mengsan_slimed_shuying", + "title": "黏液", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": 1, + "description": "抽1张牌。消耗。", + "base": { + "exhaust": true, + "draw": 1 + } + }, + { + "id": "debris", + "name": "mengsan_status_debris", + "title": "碎屑", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": 1, + "description": "消耗。", + "base": { + "exhaust": true + } + }, + { + "id": "wither", + "name": "mengsan_status_wither", + "title": "凋萎", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,你受到3点伤害。", + "base": { + "unplayable": true, + "endDamage": 3 + } + }, + { + "id": "infection", + "name": "mengsan_infection_shuying", + "title": "感染", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,你受到3点伤害。", + "base": { + "unplayable": true, + "endDamage": 3 + } + }, + { + "id": "soot", + "name": "mengsan_status_soot", + "title": "煤灰", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。", + "base": { + "unplayable": true + } + }, + { + "id": "wound", + "name": "mengsan_wound_shuying", + "title": "伤口", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。", + "base": { + "unplayable": true + } + }, + { + "id": "void", + "name": "mengsan_status_void", + "title": "虚空", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。虚无。每当你抽到这张牌时,失去1点能量。", + "base": { + "ethereal": true, + "unplayable": true + } + }, + { + "id": "dazed", + "name": "mengsan_dazed_shuying", + "title": "晕眩", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。虚无。", + "base": { + "ethereal": true, + "unplayable": true + } + }, + { + "id": "burn", + "name": "mengsan_status_burn", + "title": "灼伤", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": null, + "description": "不能被打出。在你的回合结束时,如果这张牌在你的手牌中,你受到2点伤害。", + "base": { + "unplayable": true, + "endDamage": 2 + } + }, + { + "id": "deprecated_card", + "name": "mengsan_status_deprecated_card", + "title": "弃用卡牌", + "pack": "status", + "category": "utility", + "cardType": "status", + "rarity": "status", + "cost": 0, + "description": "这张牌在近期的一次更新中被移除。抽1张牌。将这张牌从你的牌组中移除。消耗。", + "base": { + "exhaust": true, + "draw": 1 + } + } +]); diff --git a/mengsan/cards/personal-piles.js b/mengsan/cards/personal-piles.js index 7aec8c0..7544831 100644 --- a/mengsan/cards/personal-piles.js +++ b/mengsan/cards/personal-piles.js @@ -1,6 +1,11 @@ // Mode-owned piles use real Card nodes so native searches/gain/insert operations agree. // DIY must access ui piles inside the acting player's engine event, never cache global nodes. import { AFFIX_INFO } from "./affixes.js"; +import { snapshotRampage } from "./rampage.js"; +import { cardDefinitions } from "./card-definitions.js"; +import { hasCardAffix } from "./intrinsic-affixes.js"; +import { sharedHooks } from "./shared-hooks.js"; +import { queueIroncladExhaust, flushIroncladExhaust, ironcladSkillExhausts, ironcladHooks } from "./ironclad-hooks.js"; export function installPersonalPiles(session, owner, battle, run, resources, env) { @@ -15,18 +20,31 @@ export function installPersonalPiles(session, owner, battle, run, resources, env draw.id = "cardPile"; discard.id = "discardPile"; const owned = new Set(), topPlaying = new Set(), restores = []; const strict = env.strict !== false; - let released = false, forcedOwner = false; + let released = false, forcedOwner = false, forcedPublic = false, exhaustTotal = 0; + function recordExhaust(card) { + if (card.storage?.mengsanExhausted_shuying) return false; + card.storage.mengsanExhausted_shuying = true; + battle.exhaustPile.push(snapshot(card)); exhaustTotal++; + queueIroncladExhaust(owner, card); refresh(); return true; + } const personal = () => { - if (released) return false; + if (released || forcedPublic) return false; if (forcedOwner) return true; const seen = new Set(); for (let event = _status.event; event && !seen.has(event); event = event.parent) { seen.add(event); + // 判定及其公共牌清理沿用原生牌堆;判定触发的独立摸牌/弃牌事件 + // 会先遇到自己的 player,仍按个人牌堆处理。 + if (event.name === "judge" || event.name === "judgeCallback" || + event.name === "orderingDiscard" && event.relatedEvent?.name === "judge") return false; if (event.player) return event.player === owner; } return false; // Never infer ownership from whose turn it is. }; const refresh = () => { if (!released) env.refresh(); }; + const canReceiveTransfer = (card, sourceSession) => !released && session.active && sourceSession===session && + owner.isAlive() && !owned.has(card) && typeof card?.storage?.mengsanCard_shuying?.id==="string" && + ![...owned].some(c=>c.storage?.mengsanCard_shuying?.id===card.storage.mengsanCard_shuying.id); function adopt(card, data) { if (owned.has(card)) return; owned.add(card); resources.card(card); @@ -34,6 +52,9 @@ export function installPersonalPiles(session, owner, battle, run, resources, env card.storage.mengsanOwnerId_shuying = owner.playerid; if (data) { card.storage.mengsanCard_shuying = JSON.parse(JSON.stringify(data)); + const classification = cardDefinitions[card.name] || env.lib?.card?.[card.name]; + card.storage.mengsanCard_shuying.cardType = classification?.cardType ?? null; + card.storage.mengsanCard_shuying.rarity = classification?.rarity ?? "common"; // Retired affixes from old saves must not acquire new battle behaviour. card.storage.mengsanCard_shuying.affixes = (card.storage.mengsanCard_shuying.affixes || []).filter(key => key !== "annihilate"); } @@ -44,29 +65,33 @@ export function installPersonalPiles(session, owner, battle, run, resources, env if (released || !["discardPile", "equip", "judge"].includes(position)) return false; if (position === "discardPile" && topPlaying.has(current)) { if (!current.storage.mengsanExhausted_shuying) { - current.storage.mengsanExhausted_shuying = true; - battle.exhaustPile.push(snapshot(current)); - refresh(); + recordExhaust(current); } return true; } - const exhaust = env.lib?.card?.[current.name]?.mengsanExhaust_shuying === true || - current.storage.mengsanCard_shuying.affixes.includes("exhaust"); + // 能力牌退出战斗牌堆,不进入消耗牌堆,也不触发消耗收益。 + if (env.lib?.card?.[current.name]?.mengsanPower_shuying && current.storage.mengsanConsumed_shuying) { + current.storage.mengsanPowerRemoved_shuying = true; + return true; + } + const exhaustRule = env.lib?.card?.[current.name]?.mengsanExhaust_shuying; + const exhaust = (typeof exhaustRule === "function" ? exhaustRule(current) : exhaustRule === true) || + current.storage.mengsanCard_shuying.affixes.includes("exhaust") || + ironcladSkillExhausts(owner,current); if (exhaust && current.storage.mengsanConsumed_shuying) { if (current.storage.mengsanExhausted_shuying) return true; - current.storage.mengsanExhausted_shuying = true; - battle.exhaustPile.push(snapshot(current)); - refresh(); + recordExhaust(current); return true; } return false; // Actual discarded cards remain available to native skills. }; + // Only explicit affixes are native gaintags; intrinsic keywords can change on upgrade. for (const key of card.storage.mengsanCard_shuying.affixes) card.addGaintag(AFFIX_INFO[key]?.name || key); // 强化仅用手牌 +X 角标显示;数值仍保存在 mengsanCard_shuying.upgrade。 } function snapshot(card) { const data = card.storage?.mengsanCard_shuying || {}; - return { ...data, name: card.name, suit: card.suit, number: card.number, + return { ...data, ...snapshotRampage(card), ...ironcladHooks(owner)?.snapshotCard(card), ...sharedHooks(owner)?.snapshotCard?.(card), name: card.name, suit: card.suit, number: card.number, nature: card.nature || null, affixes: data.affixes?.slice() || [], upgrade: data.upgrade || 0 }; } for (const data of battle.drawPile) { @@ -121,15 +146,26 @@ export function installPersonalPiles(session, owner, battle, run, resources, env } refresh(); return cards; } - function wrap(object, key, replacement) { + function wrap(object, key, replacement, shouldReplace = personal) { const descriptor = Object.getOwnPropertyDescriptor(object, key), original = object[key]; - const wrapped = function (...args) { return personal() ? replacement(...args) : original.apply(this, args); }; + const wrapped = function (...args) { return shouldReplace() ? replacement.apply(this, args) : original.apply(this, args); }; object[key] = wrapped; restores.push(() => { if (object[key] === wrapped) { if (descriptor) Object.defineProperty(object, key, descriptor); else delete object[key]; } }); } wrap(get, "cards", (number, putBack) => take(typeof number === "number" ? number : 1, putBack)); wrap(get, "bottomCards", (number, putBack) => take(typeof number === "number" ? number : 1, putBack, true)); wrap(game, "washCard", wash); + // 原生 player.judge 在事件创建时就读取默认弃牌位置,此时尚未进入 judge。 + // 仅在同步创建期间读取公共节点,不把此状态延续到异步判定或子技能。 + // 敌人或助战也可能在玩家事件中创建判定,须统一默认位置。 + const judgeHost = env.lib?.element?.Player?.prototype || owner; + const nativeJudge = judgeHost.judge; + wrap(judgeHost, "judge", function (...args) { + const before = forcedPublic; + forcedPublic = true; + try { return nativeJudge.apply(this, args); } + finally { forcedPublic = before; } + }, () => !released); session.ownResource(draw, () => { if (released) return; observer.disconnect(); released = true; @@ -139,6 +175,42 @@ export function installPersonalPiles(session, owner, battle, run, resources, env }); return { take, + canReceiveTransfer, + receiveTransfer(card,sourceSession) { + if(!canReceiveTransfer(card,sourceSession))return null; + const originalData=card.storage.mengsanCard_shuying; + adopt(card,originalData); + return { + gain:()=>owner.gain(card,"gain2"), + received:()=>!released && session.active && owner.isAlive() && owned.has(card) && owner.getCards("h").includes(card), + rollback:()=>{owned.delete(card);refresh();}, + }; + }, + async transferTo(card,recipient) { + if(released || !session.active || !owner.isAlive() || !owned.has(card) || + card.storage?.mengsanExhausted_shuying || card.storage?.mengsanPowerRemoved_shuying || + !recipient?.canReceiveTransfer?.(card,session))return false; + const previous={data:card.storage.mengsanCard_shuying,ownerId:card.storage.mengsanOwnerId_shuying, + destroyed:card.destroyed,parent:card.parentNode,next:card.nextSibling}; + owned.delete(card); + let receipt,committed=false; + try { + receipt=recipient.receiveTransfer(card,session); + if(!receipt)return false; + await receipt.gain(); + if(receipt.received()){committed=true;refresh();return true;} + return false; + } finally { + if(!committed)receipt?.rollback(); + if(!committed && !released && session.active){ + owned.add(card);card.storage.mengsanCard_shuying=previous.data; + card.storage.mengsanOwnerId_shuying=previous.ownerId;card.destroyed=previous.destroyed; + if(previous.parent)previous.parent.insertBefore(card,previous.next?.parentNode===previous.parent?previous.next:null); + else discard.appendChild(card); + refresh(); + } + } + }, // 取出当前堆顶,不算抽牌、不洗牌,也不触发不足牌堆失败。 beginTopPlay() { if (released || !session.active || !owner.isAlive() || !draw.firstChild) return null; @@ -151,7 +223,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env if (!topPlaying.has(card)) return null; try { if (released || !session.active) return null; - if (card.storage?.mengsanExhausted_shuying) { card.remove(); return card; } + if (card.storage?.mengsanExhausted_shuying) { card.remove(); await flushIroncladExhaust(owner); return card; } if (owner.getCards("hej").includes(card)) { const loss = owner.lose(card, exhausted); loss.type = "mengsanExhaust"; await loss; @@ -159,8 +231,7 @@ export function installPersonalPiles(session, owner, battle, run, resources, env } // 被使用后在排序区/弃牌堆,或无法使用而仍在暂存区,均消耗原牌一次。 if (!owned.has(card)) return null; - card.storage.mengsanExhausted_shuying = true; - battle.exhaustPile.push(snapshot(card)); card.remove(); refresh(); + recordExhaust(card); card.remove(); refresh(); await flushIroncladExhaust(owner); return card; } finally { topPlaying.delete(card); } }, @@ -175,13 +246,88 @@ export function installPersonalPiles(session, owner, battle, run, resources, env // A cancelled/redirection event must not falsely consume its original. if (released || !session.active || card.parentNode !== exhausted) return null; if (!card.storage.mengsanExhausted_shuying) { - card.storage.mengsanExhausted_shuying = true; - battle.exhaustPile.push(snapshot(card)); + recordExhaust(card); } card.remove(); refresh(); + await flushIroncladExhaust(owner); return card; }, + exhaustTotal() { return exhaustTotal; }, + exhaustedCards() { return [...owned].filter(card => card.storage?.mengsanExhausted_shuying); }, + syncExhaustSnapshots() { + if(released || !session.active)return; + const current=new Map([...owned].filter(card=>card.storage?.mengsanExhausted_shuying) + .map(card=>[card.storage.mengsanCard_shuying?.id,card])); + for(let index=0;index data.id === id); + if (index >= 0) battle.exhaustPile.splice(index,1); + delete card.storage.mengsanExhausted_shuying; + delete card._selfDestroyed; + } else if (card.parentNode !== draw && card.parentNode !== discard) return false; + delete card.storage.mengsanConsumed_shuying; + card.fix?.(); exhausted.appendChild(card); refresh(); return true; + }, + finishAutoPlay(card) { + if (released || !session.active || !owned.has(card)) return; + // 已由原生用牌处理过的牌保留真实去向;没有合法目标的牌进入弃牌堆。 + if (card.parentNode === exhausted && !card.storage?.mengsanExhausted_shuying && + !card.storage?.mengsanPowerRemoved_shuying) discard.appendChild(card); + refresh(); + }, + drawTop() { return released || !session.active ? null : draw.firstChild; }, + drawCards() { return released || !session.active ? [] : Array.from(draw.childNodes); }, + async gainFromDraw(card) { + if(released || !session.active || !owned.has(card) || card.parentNode!==draw)return false; + await owner.gain(card,"gain2");refresh();return owner.getCards("h").includes(card); + }, + async moveHandToTop(card) { + if(released || !session.active || !owned.has(card) || !owner.getCards("h").includes(card))return false; + await owner.lose(card,draw); + if(released || !session.active || card.parentNode!==draw)return false; + card.fix?.();draw.insertBefore(card,draw.firstChild);refresh();return true; + }, + addToDraw(data,index=0) { + if(!Number.isSafeInteger(index) || index<0)throw new RangeError("插入牌堆的位置无效"); + const card=this.addToDiscard(data);if(!card)return null; + draw.insertBefore(card,draw.childNodes[Math.min(index,draw.childNodes.length)] || null);refresh();return card; + }, + async gainToHand(card) { + if (released || !session.active || !owned.has(card) || card.storage?.mengsanExhausted_shuying || + card.parentNode !== discard) return false; + await owner.gain(card,"gain2"); refresh(); return owner.getCards("h").includes(card); + }, + async returnToHand(card) { + if(released || !session.active || !owner.isAlive() || !owned.has(card))return false; + if(owner.getCards("h").includes(card))return true; + if(card.storage?.mengsanExhausted_shuying){ + const id=card.storage.mengsanCard_shuying?.id; + const index=battle.exhaustPile.findIndex(data=>data.id===id); + if(index>=0)battle.exhaustPile.splice(index,1); + delete card.storage.mengsanExhausted_shuying;delete card._selfDestroyed; + } + delete card.storage.mengsanConsumed_shuying; + card.fix?.();await owner.gain(card,"gain2");refresh();return owner.getCards("h").includes(card); + }, + transformHand(card, data) { + if (released || !session.active || !owned.has(card) || !owner.getCards("h").includes(card) || + hasCardAffix(card,"eternal")) return false; + card.init([data.suit,data.number,data.name,data.nature]); + card.storage ||= {}; card.storage.mengsanCard_shuying = {...data,affixes:data.affixes.slice()}; + card.storage.mengsanOwnerId_shuying = owner.playerid; + refresh(); return true; + }, // Current physical combat cards only. Exhausted originals remain in owned; // never add the serialized exhaust snapshots a second time. battleCards(playing = []) { @@ -210,6 +356,11 @@ export function installPersonalPiles(session, owner, battle, run, resources, env refresh(); return card; }, + async addToHand(data) { + const card = this.addToDiscard(data); + if (!card) return null; + await owner.gain(card,"gain2"); refresh(); return card; + }, // Return references only to cards currently in this actor's discard node. discardCards() { if (released || !session.active) return []; @@ -242,14 +393,17 @@ export function installPersonalPiles(session, owner, battle, run, resources, env refresh(); }, trimOpeningHand(limit = 4) { - for (const card of owner.getCards("h").filter(card => !card.storage?.mengsanCard_shuying?.affixes?.includes("innate")).slice(limit)) { + for (const card of owner.getCards("h").filter(card => !card.storage?.mengsanCard_shuying?.affixes?.includes("innate") && + !env.lib?.card?.[card.name]?.mengsanInnate_shuying?.(card)).slice(limit)) { if (owned.has(card)) draw.insertBefore(card, draw.firstChild); else card.remove(); } refresh(); }, drawInnate() { - const cards = Array.from(draw.childNodes).filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("innate") && !owner.getCards("h").includes(card)); + const cards = Array.from(draw.childNodes).filter(card => + (card.storage?.mengsanCard_shuying?.affixes?.includes("innate") || + env.lib?.card?.[card.name]?.mengsanInnate_shuying?.(card)) && !owner.getCards("h").includes(card)); if (cards.length) owner.directgain(cards); refresh(); return cards; diff --git a/mengsan/cards/rampage.js b/mengsan/cards/rampage.js new file mode 100644 index 0000000..1f6bb72 --- /dev/null +++ b/mengsan/cards/rampage.js @@ -0,0 +1,104 @@ +// 累积仅属于真实牌对象和当前会话;不写Card.storage或永久牌组。 +const NAME = "mengsan_baozou"; +const growth = new WeakMap(); +const validEntry = (card, entry) => card?.name === NAME && entry?.session.active && + entry.battle.session === entry.session; +export function snapshotRampage(card) { + const entry = growth.get(card); + return validEntry(card, entry) ? { + mengsanRampageBonus_shuying: entry.bonus, + mengsanRampageSession_shuying: entry.session.id, + } : {}; +} +export function copyRampageGrowth(source,target,battle) { + if(source?.cards?.length===1)source=source.cards[0]; + const from=growth.get(source); + if(!validEntry(source,from) || from.battle!==battle || target?.name!==NAME)return; + const entry={battle,session:battle.session,bonus:from.bonus}; + growth.set(target,entry); + battle.session.ownResource(entry,()=>{if(growth.get(target)===entry)growth.delete(target);}); +} +function nearestUse(event) { + const seen = new Set(); + for (let current = event; current && !seen.has(current); current = current.parent) { + seen.add(current); + if (current.name === "useCard") return current; + } + return null; +} +function original(event) { + const use = nearestUse(event); + if(use?.card?.name===NAME && use.mengsanIcRepeatOriginal_shuying?.name===NAME)return use.mengsanIcRepeatOriginal_shuying; + return use?.card?.name === NAME && use.cards?.length === 1 && + use.cards[0]?.name === NAME ? use.cards[0] : null; +} +function cardOf(card) { + return card?.name === NAME && card.cards?.length === 1 && card.cards[0]?.name === NAME + ? card.cards[0] : card; +} +function belongsTo(event, root) { + const seen = new Set(); + for (let current = event; current && !seen.has(current); current = current.parent) { + if (current === root) return true; + seen.add(current); + } + return false; +} +export function createRampage(game, getActiveBattle) { + const used = new WeakSet(); + const getPile = (player, battle) => player === game.me + ? battle.personalPiles : battle.monsterPiles?.get(player); + function damage(card) { + const node = cardOf(card), battle = getActiveBattle(), entry = growth.get(node); + let bonus = validEntry(node, entry) && entry.battle === battle ? entry.bonus : 0; + // Only plain, current-session UI snapshots may expose serialized growth. + if (!node?.storage && battle?.session.active && + node?.mengsanRampageSession_shuying === battle.session.id && + Number.isSafeInteger(node.mengsanRampageBonus_shuying) && node.mengsanRampageBonus_shuying >= 0) + bonus = node.mengsanRampageBonus_shuying; + if (bonus > Number.MAX_SAFE_INTEGER - 9) throw new RangeError("暴走伤害越界"); + return 9 + bonus; + } + return { + card: cardOf, original, damage, + async apply(player, event, baseDamage, increase) { + if (baseDamage !== 9 || !Number.isSafeInteger(increase) || increase <= 0) + throw new RangeError("暴走攻击参数无效"); + const use = nearestUse(event), card = original(event); + const battle = getActiveBattle(), session = battle?.session, root = battle?.root; + if (!battle || !session?.active || !root || !card || use.player !== player || + event.card?.name !== NAME || used.has(use) || !belongsTo(use, root)) return; + const pile = getPile(player, battle), target = event.target; + const valid = () => getActiveBattle() === battle && battle.session === session && + session.active && battle.root === root && battle.players?.has(player) && + player.isAlive() && card.name === NAME && getPile(player, battle) === pile && + typeof pile?.battleCards === "function" && pile.battleCards(use.cards).includes(card); + if (!valid() || !battle.players.has(target) || !target?.isAlive?.() || + target === player || !player.isEnemyOf(target)) return; + const amount = damage(card), bonus = amount - baseDamage; + if (increase > Number.MAX_SAFE_INTEGER - baseDamage - bonus) + throw new RangeError("暴走累积伤害越界"); + used.add(use); + player.line(target, "green"); + const hit = target.damage(amount, player); + hit.mengsanAttack_shuying = true; + await hit; + if (!valid()) return; + let entry = growth.get(card); + if (!validEntry(card, entry) || entry.battle !== battle) { + entry = { battle, session, bonus: 0 }; + const record = entry; + session.ownResource(record, () => { + if (growth.get(card) === record) growth.delete(card); + }); + growth.set(card, entry); + } + // Damage hooks may initiate nested uses; retain their increments without overwrite. + if (increase > Number.MAX_SAFE_INTEGER - baseDamage - entry.bonus) + throw new RangeError("暴走累积伤害越界"); + entry.bonus += increase; + battle.handUI?.refresh(); + battle.pilesUI?.refresh(); + }, + }; +} diff --git a/mengsan/cards/rarity-data.js b/mengsan/cards/rarity-data.js new file mode 100644 index 0000000..a82925f --- /dev/null +++ b/mengsan/cards/rarity-data.js @@ -0,0 +1,7 @@ +// 卡面与机制读取同一注册表;未来职业在各自 metadata 中填写 rarity。 +import { cardDefinitions } from "./card-definitions.js"; +export const cardRarities = Object.freeze({ + ...Object.fromEntries(Object.values(cardDefinitions).map(card => [card.name, card.rarity])), + mengsan_dazed_shuying: "status", mengsan_slimed_shuying: "status", + mengsan_infection_shuying: "status", mengsan_wound_shuying: "status", +}); diff --git a/mengsan/cards/rarity.js b/mengsan/cards/rarity.js new file mode 100644 index 0000000..10be5b2 --- /dev/null +++ b/mengsan/cards/rarity.js @@ -0,0 +1,51 @@ +import { cardRarities } from "./rarity-data.js"; +import { cardRarityLabels } from "./classification-types.js"; + +// 使用固定可信色值;不把牌组数据或外部输入直接拼成 CSS。 +const palettes = Object.fromEntries([ + ["common", "普通", "#b2b5b6", "#383e42", "#14181b", "#dededd"], + ["uncommon", "罕见", "#66d6df", "#113d42", "#102a2e", "#b5eff1"], + ["rare", "稀有", "#ecc063", "#604116", "#332710", "#fff0b6"], + ["event", "事件", "#77ce8a", "#123c21", "#152718", "#c4f3c8"], + ["ancient", "先古之民", "#b45cc0", "#291a35", "#17131b", "#bfc7c5"], +].map(([key, label, accent, dark, ink, metal]) => [key, Object.freeze({key, label, accent, dark, ink, metal})])); +// 初始、衍生沿用现有中性配色;尚无专门视觉的状态/诅咒/任务保留原卡面。 +export const rarityTypes = Object.freeze({ ...palettes, + basic: Object.freeze({...palettes.common, key:"basic", label:cardRarityLabels.basic}), + token: Object.freeze({...palettes.common, key:"token", label:cardRarityLabels.token}), + ...Object.fromEntries(["status","curse","quest"].map(key => + [key, Object.freeze({key, label:cardRarityLabels[key], visualKey:"pending"})])), +}); +const variables = ["accent", "dark", "ink", "metal"]; +export function createRarityResolver(assignments) { + if (!assignments || typeof assignments !== "object" || Array.isArray(assignments)) + throw new TypeError("梦三稀有度配置须为对象"); + const values = Object.create(null); + for (const [name, value] of Object.entries(assignments)) { + if (!/^[a-z][a-z0-9_]*$/.test(name) || + !(value == null || typeof value === "string" && Object.hasOwn(rarityTypes, value))) + throw new TypeError(`梦三稀有度配置无效:${name}`); + values[name] = value; + } + return card => { + const name = typeof card === "string" ? card : card?.name; + const key = typeof name === "string" && Object.hasOwn(values, name) ? values[name] : null; + // 未登记的原生牌、未填写或 null 均按普通显示,显式等级仍优先。 + return key ? rarityTypes[key] : rarityTypes.common; + }; +} +export const getCardRarity = createRarityResolver(cardRarities); +// Reused by real hand nodes and read-only previews; never write card.storage or game data. +export function applyCardRarity(node, card) { + const info = getCardRarity(card); + const visualKey = info.visualKey || info.key; + if (node.dataset.mengsanRarity !== visualKey) node.dataset.mengsanRarity = visualKey; + if (node.dataset.mengsanRarityLabel !== info.label) node.dataset.mengsanRarityLabel = info.label; + for (const key of variables) { + const property = `--ms-rarity-${key}`; + if (visualKey === "pending") { + if (node.style.getPropertyValue(property)) node.style.removeProperty(property); + } else if (node.style.getPropertyValue(property) !== info[key]) node.style.setProperty(property, info[key]); + } + return info; +} diff --git a/mengsan/cards/retired-cards.js b/mengsan/cards/retired-cards.js new file mode 100644 index 0000000..11beaf7 --- /dev/null +++ b/mengsan/cards/retired-cards.js @@ -0,0 +1,31 @@ +// 仅迁移读取/写入时的存档副本,不重新注册已移除的独立牌。 +const cardNames = Object.freeze({mengsan_ic_strike: "sha", mengsan_ic_bash: "mengsan_zhongsha"}); +const rewardIds = Object.freeze({ + "shared.reward.card.mengsan_ic_strike": "card_sha", + "shared.reward.card.mengsan_ic_bash": "shared.reward.card.zhongsha", +}); + +// 旧奖励候选保留ID,避免与同批已有杀/重杀候选发生ID冲突。 +export function resolveRetiredCardReward(id) { + return Object.hasOwn(rewardIds, id) ? rewardIds[id] : id; +} + +export function migrateRetiredCards(run) { + if (!run || typeof run !== "object") return run; + // 同时覆盖永久牌组、待结算快照、已选结果与重试快照,保留牌实例ID及其余状态。 + const pending = [run], seen = new Set(); + while (pending.length) { + const value = pending.pop(); + if (seen.has(value)) continue; + seen.add(value); + if (Object.hasOwn(cardNames, value.name)) value.name = cardNames[value.name]; + const cardName = Object.hasOwn(cardNames, value.card?.name) ? cardNames[value.card.name] : value.card?.name; + if (cardName === "sha" && value.name === "获得一张【打击】") value.name = "获得一张【杀】"; + if (cardName === "mengsan_zhongsha" && value.name === "获得一张【痛击】") value.name = "获得一张【重杀】"; + for (const key of Object.keys(value)) { + const item = value[key]; + if (item && typeof item === "object") pending.push(item); + } + } + return run; +} diff --git a/mengsan/cards/shared-battle-end.js b/mengsan/cards/shared-battle-end.js new file mode 100644 index 0000000..454f51a --- /dev/null +++ b/mengsan/cards/shared-battle-end.js @@ -0,0 +1,16 @@ +import { upgradeRandomCard } from "./upgrades.js"; +import { nextRandom } from "../progression/state.js"; +// Called inside the existing receipt-guarded settlement transaction, once per battle. +export function applySharedBattleEndCards(run,outcome) { + const effects=run.sharedBattleCardEffects;delete run.sharedBattleCardEffects; + if(outcome!=="victory")return; + run.player.deck=run.player.deck.filter(card=>{ + if(card.name!=="mengsan_curse_guilty")return true; + const count=card.guiltyBattles??0; + if(!Number.isSafeInteger(count)||count<0||count>4)throw new RangeError("愧疚战斗计数无效"); + card.guiltyBattles=count+1;return card.guiltyBattles<5; + }); + const count=effects?.upgrades??0; + if(!Number.isSafeInteger(count)||count<0||count>1024)throw new RangeError("战后卡牌强化次数无效"); + for(let n=0;nnextRandom(run)))break; +} diff --git a/mengsan/cards/shared-hooks.js b/mengsan/cards/shared-hooks.js new file mode 100644 index 0000000..05705ed --- /dev/null +++ b/mengsan/cards/shared-hooks.js @@ -0,0 +1,13 @@ +// Pure bridge: the controller validates the current battle/session on every lookup. +let resolver = () => null; +export function installSharedHookResolver(next) { + if (typeof next !== "function") throw new TypeError("通用牌钩子无效"); + resolver = next; +} +export const sharedHooks = player => player ? resolver(player) : null; +export const sharedCardIsFree = (player,card) => sharedHooks(player)?.isFree(card) || false; +export const sharedCardCost = (player,card,cost) => sharedHooks(player)?.cost(card,cost) ?? cost; +export const sharedStrengthPenalty = player => sharedHooks(player)?.strengthPenalty() || 0; +export const beforeSharedBlock = (player,num,event) => sharedHooks(player)?.blockAmount(num,event) ?? num; +export const afterSharedDraw = async (player,cards) => sharedHooks(player)?.afterDraw(cards); + diff --git a/mengsan/cards/spite.js b/mengsan/cards/spite.js new file mode 100644 index 0000000..54f4857 --- /dev/null +++ b/mengsan/cards/spite.js @@ -0,0 +1,34 @@ +// 只读引擎当前完整回合的实际changeHp历史,不写角色/永久牌组状态。 +const belongsTo = (event, root) => { + const seen = new Set(); + for (let current = event; current && !seen.has(current); current = current.parent) { + if (current === root) return true; + seen.add(current); + } + return false; +}; +export function createSpite(game, getActiveBattle) { + return async function spite(player, event, damage, conditionalHits) { + if (!Number.isSafeInteger(damage) || damage <= 0 || + !Number.isSafeInteger(conditionalHits) || conditionalHits <= 0) + throw new RangeError("怨恨攻击参数无效"); + const battle = getActiveBattle(), session = battle?.session, root = battle?.root; + const target = event?.target; + const valid = () => Boolean(battle && root && getActiveBattle() === battle && + battle.session === session && session?.active && battle.root === root && + battle.players?.has(player) && battle.players.has(target) && + player?.isAlive?.() && target?.isAlive?.() && target !== player && player.isEnemyOf(target)); + if (!valid() || !belongsTo(event, root)) return; + const history = game.getGlobalHistory("changeHp"); + const lost = Array.isArray(history) && history.some(change => change.player === player && + Number.isFinite(change.changedHp) && change.changedHp < 0 && belongsTo(change, root)); + const count = lost ? conditionalHits : 1; + for (let index = 0; index < count; index++) { + if (!valid() || game.getGlobalHistory("changeHp") !== history) break; + player.line(target, "green"); + const hit = target.damage(damage, player); + hit.mengsanAttack_shuying = true; + await hit; + } + }; +} diff --git a/mengsan/cards/starting-deck.js b/mengsan/cards/starting-deck.js index cbf23fb..e6e17ff 100644 --- a/mengsan/cards/starting-deck.js +++ b/mengsan/cards/starting-deck.js @@ -14,7 +14,7 @@ export const defaultDeck = Object.freeze([ export const characterDecks = { mengsan_liubei_shuying: [ ["spade", 7, "sha"], ["heart", 10, "sha"], ["club", 4, "sha"], ["diamond", 6, "sha"], - ["heart", 2, "shan"], ["diamond", 7, "shan"], ["club", 2, "shan"], + ["heart", 2, "mengsan_fangyu"], ["diamond", 7, "mengsan_fangyu"], ["club", 2, "shan"], ["heart", 3, "tao"], ["heart", 7, "wuzhong"], ["spade", 1, "juedou"], ["spade", 2, "bagua"], ["club", 6, "sha"], ], diff --git a/mengsan/cards/strike-counter.js b/mengsan/cards/strike-counter.js index ab0fde7..e7c48b9 100644 --- a/mengsan/cards/strike-counter.js +++ b/mengsan/cards/strike-counter.js @@ -1,3 +1,8 @@ +// 杀对应基础打击;重杀对应痛击,不属于打击牌。 +export function isStrikeCard(card, lib) { + return card?.name === "sha" || String(lib.translate[card?.name] || "").includes("打击"); +} + // Count current actor-owned combat cards; permanent save data is deliberately absent. export function createStrikeCounter(game, getActiveBattle, lib) { return (player, event) => { @@ -19,8 +24,7 @@ export function createStrikeCounter(game, getActiveBattle, lib) { } let count = 0; for (const card of piles.battleCards(playing)) { - const name = lib.translate[card.name]; - if (typeof name === "string" && name.includes("打击")) count++; + if (isStrikeCard(card, lib)) count++; } return count; }; diff --git a/mengsan/cards/temporary-strength.js b/mengsan/cards/temporary-strength.js index d77155e..39eed41 100644 --- a/mengsan/cards/temporary-strength.js +++ b/mengsan/cards/temporary-strength.js @@ -22,7 +22,17 @@ export function temporaryStrengthAmount(player, battle) { export function temporaryStrengthExpires(player, phase) { const entry = records.get(player); - return Boolean(entry && phase.name === "phase" && (!entry.turn || entry.turn === phase)); + return Boolean(entry && phase.name === "phase" && entry.segments.some(part=> + part.ownTurn ? phase.player===player : !part.turn || part.turn===phase)); +} + +export function expireTemporaryStrength(player,phase) { + const entry=records.get(player);if(!entry || phase.name!=="phase")return false; + const remaining=entry.segments.filter(part=>part.ownTurn ? phase.player!==player : part.turn && part.turn!==phase); + if(remaining.length===entry.segments.length)return false; + if(!remaining.length)return clearTemporaryStrength(player); + entry.segments=remaining;entry.amount=remaining.reduce((n,part)=>n+part.amount,0); + player.storage.mengsanSetupStrength_shuying=entry.amount;player.markSkill(SKILL);return true; } export function createTemporaryStrength(game, getActiveBattle) { @@ -32,30 +42,33 @@ export function createTemporaryStrength(game, getActiveBattle) { const battle = getActiveBattle(); return battle?.session.active ? battle : null; }, - grant(player, amount, event, battle) { + grant(player, amount, event, battle, {ownTurn=false}={}) { if (!battle?.session.active || getActiveBattle() !== battle || !player.isAlive()) return false; if (!Number.isSafeInteger(amount) || amount <= 0) throw new RangeError("临时力量必须是正安全整数"); const ancestor = event.getParent?.("phase"); const turn = ancestor?.name === "phase" ? ancestor : null; let entry = records.get(player); - if (entry && (entry.battle !== battle || entry.turn !== turn)) { + if (entry && entry.battle !== battle) { clearTemporaryStrength(player); entry = null; } if (amount > Number.MAX_SAFE_INTEGER - (entry?.amount || 0)) throw new RangeError("临时力量层数越界"); if (!entry) { - entry = { battle, turn, amount: 0 }; + entry = { battle, segments:[], amount: 0 }; // Register cleanup before publishing state. A stale release cannot clear a later battle. battle.session.ownResource(entry, () => { if (records.get(player) === entry) forgetTemporaryStrength(player); }); records.set(player, entry); } + // Normal card grants keep their original phase expiry; ally gifts last through the recipient's turn. + const part=entry.segments.find(part=>part.ownTurn===ownTurn&&(ownTurn||part.turn===turn)); + if(part)part.amount+=amount;else entry.segments.push({turn:ownTurn?null:turn,ownTurn,amount}); entry.amount += amount; player.storage.mengsanSetupStrength_shuying = entry.amount; player.addSkill(SKILL); player.markSkill(SKILL); - game.log(player, `在本回合内获得${amount}点力量`); + game.log(player, ownTurn?`获得${amount}点力量,持续到自己的行动回合结束`:`在本回合内获得${amount}点力量`); return true; }, }; diff --git a/mengsan/cards/upgrades.js b/mengsan/cards/upgrades.js index c35f5dc..5e496c1 100644 --- a/mengsan/cards/upgrades.js +++ b/mengsan/cards/upgrades.js @@ -1,21 +1,6 @@ +import { cardPackUpgradeRules } from "./packs/data.js"; const upgrades = Object.freeze({ - mengsan_songjianwushi: Object.freeze({ maxLevel: 1, baseBlock: 8, block: 11, draw: 1 }), - mengsan_pomie: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0 }), - mengsan_jianyi: Object.freeze({ maxLevel: 1, baseBlock: 7, block: 9, chooseExhaust: true }), - mengsan_fangxue: Object.freeze({ maxLevel: 1, loseHp: 3, baseEnergy: 2, energy: 3 }), - mengsan_yujin: Object.freeze({ maxLevel: 1, baseDamage: 18, damage: 24, exhaust: 1 }), - mengsan_wanmeidaji: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 6, basePerStrike: 2, perStrike: 3 }), - mengsan_yubeidaji: Object.freeze({ maxLevel: 1, baseDamage: 7, damage: 9, baseStrength: 2, strength: 3 }), - mengsan_tupo: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 13, loseHp: 1 }), - mengsan_touchui: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 12 }), - mengsan_tiezhanbo: Object.freeze({ maxLevel: 1, baseBlock: 5, block: 7, baseDamage: 5, damage: 7 }), - mengsan_shuangchongdaji: Object.freeze({ maxLevel: 1, baseDamage: 5, damage: 7, hits: 2 }), - mengsan_shandianpili: Object.freeze({ maxLevel: 1, baseDamage: 4, damage: 7, vulnerable: 1 }), - mengsan_rongrongzhiquan: Object.freeze({ maxLevel: 1, baseDamage: 10, damage: 14, exhaust: true }), - mengsan_quanshenzhuangji: Object.freeze({ maxLevel: 1, baseCost: 1, cost: 0, damageFromBlock: true }), - mengsan_jianbingdaji: Object.freeze({ maxLevel: 1, baseDamage: 9, damage: 10, baseDraw: 1, draw: 2 }), - mengsan_feijianhuixuanbiao: Object.freeze({ maxLevel: 1, damage: 3, baseHits: 3, hits: 4 }), - mengsan_fennu: Object.freeze({ maxLevel: 1, baseDamage: 6, damage: 8 }), + ...cardPackUpgradeRules, sha: Object.freeze({ maxLevel: 1, damage: 9 }), mengsan_zhongsha: Object.freeze({ maxLevel: 1, damage: 10, vulnerable: 3 }), @@ -24,8 +9,14 @@ const upgrades = Object.freeze({ export const cardUpgradeRule = name => Object.hasOwn(upgrades, name) ? upgrades[name] : null; +function upgradeData(card) { + if(card?.cards?.length===1 && card.cards[0]?.name===card.name && card.cards[0].storage?.mengsanCard_shuying)card=card.cards[0]; + const data=card?.storage?.mengsanCard_shuying || card; + return data && data.name===card?.name ? data : null; +} + export function cardUpgradeLevel(card) { - const data = card?.storage?.mengsanCard_shuying || card; + const data = upgradeData(card); if (!data || data.name !== card?.name) return 0; const limit = cardUpgradeRule(card.name)?.maxLevel || 0; return Number.isInteger(data.upgrade) && data.upgrade > 0 @@ -34,9 +25,10 @@ export function cardUpgradeLevel(card) { export function canUpgradeCard(card) { const rule = cardUpgradeRule(card?.name); - const level = card?.upgrade ?? 0; - return Boolean(rule && Number.isInteger(level) && level >= 0 && - level < rule.maxLevel && !card.affixes?.includes("eternal")); + const data = upgradeData(card), level = data?.upgrade ?? 0; + // 永恒只禁止移除和变化;是否可强化由强化规则及真实等级决定。 + return Boolean(data && rule && Number.isInteger(level) && level >= 0 && + level < rule.maxLevel); } export const hasUpgradeableCard = deck => @@ -50,6 +42,7 @@ export function upgradeRandomCard(deck, random) { throw new RangeError("梦三强化随机数必须位于[0, 1)"); } const card = eligible[Math.floor(value * eligible.length)]; - card.upgrade = (card.upgrade || 0) + 1; + const data=upgradeData(card); + data.upgrade = (data.upgrade || 0) + 1; return card; } diff --git a/mengsan/cards/whirlwind.js b/mengsan/cards/whirlwind.js new file mode 100644 index 0000000..3c021d9 --- /dev/null +++ b/mengsan/cards/whirlwind.js @@ -0,0 +1,28 @@ +import { takeXCardUse } from "../battle/combat-rules.js"; +// 固定原场次与会话;一轮一个存活敌人快照,下一轮包含新生成敌人。 +export function createWhirlwind(game, getActiveBattle) { + const valid = (player, battle, session) => Boolean(battle && getActiveBattle() === battle && + battle.session === session && session?.active && battle.players?.has(player) && player?.isAlive?.()); + const eligible = (player, battle, target) => target !== player && battle.players.has(target) && + target.isAlive() && player.isEnemyOf(target); + return async function whirlwind(player, event, damage) { + if (!Number.isSafeInteger(damage) || damage <= 0) throw new TypeError("旋风斩伤害无效"); + const battle = getActiveBattle(), session = battle?.session; + if (!valid(player, battle, session)) return; + const count = takeXCardUse(player, event, battle); + if (count === null) return; + for (let round = 0; round < count; round++) { + if (!valid(player, battle, session)) break; + const targets = game.filterPlayer(target => eligible(player, battle, target)); + if (!targets.length) break; + for (const target of targets) { + if (!valid(player, battle, session)) return; + if (!eligible(player, battle, target)) continue; + player.line(target, "green"); + const hit = target.damage(damage, player); + hit.mengsanAttack_shuying = true; + await hit; + } + } + }; +} diff --git a/mengsan/cards/wither-card.js b/mengsan/cards/wither-card.js new file mode 100644 index 0000000..9cebd47 --- /dev/null +++ b/mengsan/cards/wither-card.js @@ -0,0 +1,15 @@ +// Enemy-only growth; ordinary player upgrades never use this field. +export const WITHER_CARD="mengsan_status_wither"; +export const MAX_WITHER_LEVEL=Math.floor((Number.MAX_SAFE_INTEGER-3)/3); +export function witherDamage(card) { + const data=card?.storage?.mengsanCard_shuying||card,level=data?.witherLevel??0; + if(!Number.isSafeInteger(level)||level<0||level>MAX_WITHER_LEVEL)throw new RangeError("凋萎层数无效"); + return 3+3*level; +} +export function growEnemyWither(card,amount=1) { + const data=card?.storage?.mengsanCard_shuying||card; + if(data?.name!==WITHER_CARD||!Number.isSafeInteger(amount)||amount<1)throw new TypeError("凋萎增长参数无效"); + const next=(witherDamage(data)-3)/3+amount; + if(!Number.isSafeInteger(next)||next>MAX_WITHER_LEVEL)throw new RangeError("凋萎层数越界"); + data.witherLevel=next;return next; +} diff --git a/mengsan/cards/wound-card.js b/mengsan/cards/wound-card.js new file mode 100644 index 0000000..08196be --- /dev/null +++ b/mengsan/cards/wound-card.js @@ -0,0 +1,37 @@ +import { getIntentHostiles } from "../battle/intent-effects.js"; +import { isVantom } from "../monsters/vantom-intent.js"; + +export const WOUND_NAME = "mengsan_wound_shuying"; +export function createWoundData(id) { + if (typeof id !== "string" || !id) throw new TypeError("伤口牌缺少ID"); + return { id, name: WOUND_NAME, suit: "spade", number: 1, + nature: null, affixes: ["unplayable"], upgrade: 0 }; +} +export const woundDefinition = Object.freeze({ + type: "status", fullimage: true, + image: "ext:术樱包/mengsan/assets/cards/mengsan_wound_shuying.png", + enable: false, notarget: true, + ai: { value: -5, useful: -5, order: 0 }, + cardPrompt: () => "不能被打出。", +}); +export function applyWound(game, current, source, count) { + if (!current?.session.active || !source?.isAlive() || !isVantom(source)) return; + if (count !== 3) throw new RangeError("肢解须加入3张伤口"); + if (!current.woundCards) { + const entry = current.woundCards = { sequence: 0 }; + current.session.ownResource(entry, () => { if (current.woundCards === entry) delete current.woundCards; }); + } + for (const target of getIntentHostiles(game, source)) { + if (!current.session.active || !source.isAlive()) break; + const pile = target === game.me ? current.personalPiles : current.monsterPiles.get(target); + if (!pile) continue; + for (let i = 0; i < count; i++) { + if (!current.session.active || !source.isAlive()) break; + const sequence = current.woundCards.sequence + 1; + if (!Number.isSafeInteger(sequence)) throw new RangeError("伤口牌序号溢出"); + current.woundCards.sequence = sequence; + pile.addToDiscard(createWoundData(`wound_${source.playerid}_${sequence}`)); + } + game.log(target, `的个人弃牌堆加入${count}张【伤口】`); + } +} diff --git a/mengsan/config.js b/mengsan/config.js index dda6082..863c873 100644 --- a/mengsan/config.js +++ b/mengsan/config.js @@ -22,6 +22,7 @@ const mengsanConfig = { boss: "Boss", }, nodeContents: contentRegistry.nodeContents, + encounters: contentRegistry.encounters, rewardPools: contentRegistry.rewardPools, rewards: contentRegistry.rewards, }; diff --git a/mengsan/content/acts/act1/encounters.js b/mengsan/content/acts/act1/encounters.js new file mode 100644 index 0000000..ca9ae7a --- /dev/null +++ b/mengsan/content/acts/act1/encounters.js @@ -0,0 +1,66 @@ +import { createRaiderTrioBattlePlan } from "./raider-trio.js"; +import { createInkletBattlePlan } from "./inklet-group.js"; +import { createNibbitPairBattlePlan } from "./nibbit-pair.js"; +import { createKinBattlePlan } from "./kin-group.js"; + +const monsters = Object.freeze({ + crawler: "mengsan_fuzzy_wurm_crawler_shuying", nibbit: "mengsan_nibbit_shuying", + shrinker: "mengsan_shrinker_beetle_shuying", leafS: "mengsan_leaf_slime_s_shuying", + leafM: "mengsan_leaf_slime_m_shuying", twigS: "mengsan_twig_slime_s_shuying", + twigM: "mengsan_twig_slime_m_shuying", cubex: "mengsan_cubex_construct_shuying", + flyconid: "mengsan_flyconid_shuying", jaxfruit: "mengsan_snapping_jaxfruit_shuying", + fogmog: "mengsan_fogmog_shuying", mawler: "mengsan_mawler_shuying", + strangler: "mengsan_slithering_strangler_shuying", vine: "mengsan_vine_shambler_shuying", + byrdonis: "mengsan_byrdonis_shuying", effigy: "mengsan_bygone_effigy_shuying", + phrog: "mengsan_phrog_parasite_shuying", vantom: "mengsan_vantom_shuying", + beast: "mengsan_ceremonial_beast_shuying", +}); +const group = (id, name, variants, tier = "normal") => ({ + id: `act1.encounter.${id}`, name, tier, + variants: variants.map(members => members.map((member, index) => ({ + id: `enemy_${index + 1}`, character: monsters[member], camp: "enemy", + tier, hand: 4, inheritSkills: false, + }))), +}); +const single = (id, name, member, tier) => group(id, name, [[member]], tier); +const custom = (id, name, createBattlePlan, tier = "normal") => ({id: `act1.encounter.${id}`, name, tier, createBattlePlan}); + +// 一个条目是一种遭遇;条目中的 variants 等概率选一组,同组成员同时上场。 +const weak = [ + single("weak.crawler", "毛绒伏地虫", "crawler"), + single("weak.nibbit", "小啃兽", "nibbit"), + single("weak.shrinker", "缩小甲虫", "shrinker"), + group("weak.slimes", "史莱姆三只", [["leafS", "leafM", "twigS"], ["leafS", "twigM", "twigS"]]), +]; +const strong = [ + single("strong.cubex", "立柱构造体", "cubex"), + group("strong.slimeFlyconid", "中型史莱姆与飞蝇菌子", [["leafM", "flyconid"], ["twigM", "flyconid"]]), + group("strong.jaxfruitFlyconid", "闪光贾克斯果与飞蝇菌子", [["jaxfruit", "flyconid"]]), + single("strong.fogmog", "雾菇", "fogmog"), + custom("strong.inklets", "墨宝三只", createInkletBattlePlan), + single("strong.mawler", "蛮兽", "mawler"), + custom("strong.nibbits", "小啃兽双只", createNibbitPairBattlePlan), + group("strong.shrinkerCrawler", "缩小甲虫与毛绒伏地虫", [["shrinker", "crawler"]]), + custom("strong.raiders", "劫掠者团伙", createRaiderTrioBattlePlan), + group("strong.slimes", "史莱姆四只", [["leafM", "twigM", "leafS", "twigS"]]), + group("strong.stranglerParty", "蛇行扼杀者与同行怪物", [ + ["jaxfruit", "strangler"], ["leafM", "strangler"], ["twigM", "strangler"], + ["leafS", "twigS", "strangler"], + ]), + single("strong.vine", "藤蔓蹒跚者", "vine"), +]; +const elite = [ + single("elite.byrdonis", "多尼斯异鸟", "byrdonis", "elite"), + single("elite.effigy", "旧日雕像", "effigy", "elite"), + single("elite.phrog", "异蛙寄生虫", "phrog", "elite"), +]; +const boss = [ + single("boss.vantom", "墨影幻灵", "vantom", "boss"), + single("boss.beast", "仪式兽", "beast", "boss"), + custom("boss.kin", "同族神官与两名信徒", createKinBattlePlan, "boss"), +]; + +export const encounterPools = Object.freeze(Object.fromEntries( + Object.entries({weak, strong, elite, boss}).map(([key, entries]) => [key, Object.freeze(entries.map(entry => entry.id))]), +)); +export const encounters = Object.fromEntries([...weak, ...strong, ...elite, ...boss].map(entry => [entry.id, entry])); diff --git a/mengsan/content/acts/act1/index.js b/mengsan/content/acts/act1/index.js index 47792ad..8c1cd2f 100644 --- a/mengsan/content/acts/act1/index.js +++ b/mengsan/content/acts/act1/index.js @@ -2,9 +2,11 @@ import liubeiOpening, { openingRewards } from "./stories/liubei-opening.js"; import actMap from "./map.js"; import { rewardPools } from "./rewards.js"; import forestAmbush from "./stories/forest-ambush.js"; +import { encounters } from "./encounters.js"; export default { map: actMap, + encounters, nodeContents: { [forestAmbush.id]: forestAmbush, [liubeiOpening.id]: liubeiOpening, diff --git a/mengsan/content/acts/act1/kin-group.js b/mengsan/content/acts/act1/kin-group.js new file mode 100644 index 0000000..013fe15 --- /dev/null +++ b/mengsan/content/acts/act1/kin-group.js @@ -0,0 +1,10 @@ +import { KIN_PRIEST_CHARACTER, KIN_FOLLOWER_CHARACTER } from "../../../monsters/kin-intent.js"; + +// 一名神官、两名信徒;用角色默认/入场随机HP,不把超过99的神官HP写入关卡覆盖值。 +export function createKinBattlePlan() { + return { units: [ + { id: "kin_priest", character: KIN_PRIEST_CHARACTER, camp: "enemy", tier: "boss", hand: 4, inheritSkills: false }, + { id: "kin_follower_1", character: KIN_FOLLOWER_CHARACTER, camp: "enemy", tier: "normal", hand: 4, inheritSkills: false, kinOwner: "kin_priest", after: "kin_priest" }, + { id: "kin_follower_2", character: KIN_FOLLOWER_CHARACTER, camp: "enemy", tier: "normal", hand: 4, inheritSkills: false, kinOwner: "kin_priest", after: "kin_follower_1" }, + ], rules: [] }; +} diff --git a/mengsan/content/acts/act1/map.js b/mengsan/content/acts/act1/map.js index 7ad1f7b..bd1a122 100644 --- a/mengsan/content/acts/act1/map.js +++ b/mengsan/content/acts/act1/map.js @@ -1,6 +1,12 @@ +import { encounterPools } from "./encounters.js"; + const act1Map = { id: "act1", name: "第一关·初梦", + endsCurrentContent: true, + completionDialogue: [ + { type: "narrator", text: "战斗还未完结,等待后续更新。" }, + ], floorNodes: [ 1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, @@ -10,6 +16,7 @@ const act1Map = { 3, 3, 3, 3, 3, 3, 3, 1, ], nodeWeights: { battle: 50, elite: 15, event: 15, rest: 10, shop: 10 }, + encounterPools, requiredFloors: { 2: "battle", 10: "chest", 16: "rest" }, chestRewardPool: "act1.pool.chest", fixedNodes: [ @@ -27,8 +34,9 @@ const act1Map = { "mengsan_nibbit_shuying", "mengsan_nibbit_pair_shuying", "mengsan_cubex_construct_shuying", "mengsan_shrinker_beetle_shuying", "mengsan_twig_slime_m_shuying", "mengsan_twig_slime_s_shuying", "mengsan_leaf_slime_m_shuying", "mengsan_leaf_slime_s_shuying", "mengsan_slithering_strangler_shuying", "mengsan_snapping_jaxfruit_shuying", "mengsan_fuzzy_wurm_crawler_shuying", "mengsan_inklet_shuying", ], - eliteEnemies: ["re_xiahoudun", "re_zhangliao", "re_xuzhu", "mengsan_byrdonis_shuying", "mengsan_bygone_effigy_shuying", "mengsan_phrog_parasite_shuying"], - boss: "re_lvbu", + eliteEnemies: ["mengsan_byrdonis_shuying", "mengsan_bygone_effigy_shuying", "mengsan_phrog_parasite_shuying"], + boss: "mengsan_vantom_shuying", + bossEnemies: ["mengsan_vantom_shuying", "mengsan_kin_priest_shuying", "mengsan_ceremonial_beast_shuying"], baseGold: 20, }; diff --git a/mengsan/content/acts/act1/stories/forest-ambush.js b/mengsan/content/acts/act1/stories/forest-ambush.js index 32bfc1e..d36d9b5 100644 --- a/mengsan/content/acts/act1/stories/forest-ambush.js +++ b/mengsan/content/acts/act1/stories/forest-ambush.js @@ -59,6 +59,7 @@ const forestAmbush = { }, ], rewardPool: "act1.pool.story.scoutGift", + allowSharedCardRewards: true, rewardTitle: "斥候的谢礼(三选一)", flags: { "story.act1.helpedScout": true, @@ -97,6 +98,7 @@ const forestAmbush = { ], }, rewardPool: "act1.pool.story.ambushLoot", + allowSharedCardRewards: true, rewardTitle: "伏兵战利品(三选一)", description: "击败伏兵后,从随机出现的三个奖励中选择一个。", }, diff --git a/mengsan/content/encounters.js b/mengsan/content/encounters.js new file mode 100644 index 0000000..410c307 --- /dev/null +++ b/mengsan/content/encounters.js @@ -0,0 +1,18 @@ +import { validateBattlePlan } from "../battle/battle-director.js"; + +export function chooseEncounterEntry(list, random) { + if (!Array.isArray(list) || !list.length || typeof random !== "function") { + throw new Error("梦三遭遇池为空或缺少随机数接口"); + } + const value = random(); + if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("遭遇随机数必须在[0,1)内"); + return list[Math.floor(value * list.length)]; +} + +export function createEncounterPlan(definition, random) { + if (!definition) throw new Error("梦三遭遇定义不存在"); + const plan = typeof definition.createBattlePlan === "function" + ? definition.createBattlePlan(random) + : { units: JSON.parse(JSON.stringify(chooseEncounterEntry(definition.variants, random))), rules: [] }; + return validateBattlePlan(plan); +} diff --git a/mengsan/content/registry.js b/mengsan/content/registry.js index 0d7cf95..b667dd6 100644 --- a/mengsan/content/registry.js +++ b/mengsan/content/registry.js @@ -15,6 +15,7 @@ const packages = [shared, act1, act2, act3]; const nodeContents = {}; const rewardPools = {}; const rewards = {}; +const encounters = Object.create(null); const dialogueTypes = new Set(["character", "narrator", "sound", "choice"]); const validateDialogue = (dialogue, location) => { @@ -41,11 +42,24 @@ packages.forEach(content => { addEntries(nodeContents, content.nodeContents, "节点内容"); addEntries(rewardPools, content.rewardPools, "奖励池"); addEntries(rewards, content.rewards, "奖励"); + addEntries(encounters, content.encounters, "遭遇"); }); const acts = [act1.map, act2.map, act3.map]; if (new Set(acts.map(act => act.id)).size != acts.length) throw new Error("梦三大关ID存在重复"); acts.forEach(act => { + validateDialogue(act.completionDialogue, `${act.id} 通关`); + if (act.endsCurrentContent != null && typeof act.endsCurrentContent !== "boolean") { + throw new Error(`梦三章节结束配置无效:${act.id}`); + } + if (act.encounterPools) for (const key of ["weak", "strong", "elite", "boss"]) { + const pool = act.encounterPools[key]; + if (!Array.isArray(pool) || !pool.length || new Set(pool).size !== pool.length || + pool.some(id => !Object.hasOwn(encounters, id) || + encounters[id].tier !== (["weak", "strong"].includes(key) ? "normal" : key))) { + throw new Error(`梦三遭遇池无效:${act.id}/${key}`); + } + } (act.fixedNodes || []).forEach(node => { if (!nodeContents[node.contentId]) throw new Error(`梦三固定节点内容不存在:${node.contentId}`); if (!Number.isInteger(node.floor) || node.floor < 0 || node.floor >= act.floorNodes.length) { @@ -101,6 +115,14 @@ Object.values(rewards).forEach(reward => { if (battles !== -1 && (!Number.isInteger(battles) || battles < 1 || battles > 99)) throw new Error("梦三支援持续场数无效"); }); -const contentRegistry = { acts, nodeContents, rewardPools, rewards }; +for (const [id, encounter] of Object.entries(encounters)) { + if (encounter.id !== id || typeof encounter.name !== "string" || !encounter.name || + !["normal", "elite", "boss"].includes(encounter.tier)) throw new Error(`梦三遭遇定义无效:${id}`); + if (typeof encounter.createBattlePlan === "function") continue; + if (!Array.isArray(encounter.variants) || !encounter.variants.length) throw new Error(`梦三遭遇组合为空:${id}`); + for (const units of encounter.variants) validateBattlePlan({units, rules: []}); +} + +const contentRegistry = { acts, nodeContents, rewardPools, rewards, encounters }; export default contentRegistry; diff --git a/mengsan/content/scenario-characters.js b/mengsan/content/scenario-characters.js index 9a01007..d8ca536 100644 --- a/mengsan/content/scenario-characters.js +++ b/mengsan/content/scenario-characters.js @@ -1,9 +1,14 @@ -// 梦三私有武将:不改动原版界刘备,也不导入整套国战技能。 +// 梦三私有刘备:保留模式内 ID 以兼容已有征程,不导入原版技能。 export function createScenarioCharacters(Character) { return { mengsan_liubei_shuying: new Character({ sex: "male", group: "han", hp: 50, maxHp: 50, skills: ["mengsan_taoyuan_bond_shuying"], - img: "image/character/re_liubei.jpg" }), + img: "extension/术樱包/mengsan/assets/portraits/liubei-portrait.jpg" }), + // 羁绊助战使用梦三独立配置;战斗生命与技能由助战成长配置决定。 + mengsan_guanyu_shuying: new Character({ sex: "male", group: "han", hp: 40, maxHp: 40, + skills: [], img: "extension/术樱包/mengsan/assets/portraits/guanyu-portrait.jpg" }), + mengsan_zhangfei_shuying: new Character({ sex: "male", group: "han", hp: 40, maxHp: 40, + skills: [], img: "extension/术樱包/mengsan/assets/portraits/zhangfei-portrait.jpg" }), // 国战男性普通小兵使用原生 shibing1 立绘;实际战斗生命由怪物类或关卡配置决定。 mengsan_soldier_shuying: new Character({ sex: "male", group: "qun", hp: 4, maxHp: 4, skills: [], img: "image/character/shibing1.jpg" }), @@ -31,6 +36,13 @@ export function createScenarioCharacters(Character) { skills: [], img: "extension/术樱包/mengsan/assets/portraits/phrog-parasite-forest-portrait.png" }), mengsan_wriggler_shuying: new Character({ sex: "none", group: "qun", hp: 19, maxHp: 19, skills: [], img: "extension/术樱包/mengsan/assets/portraits/wriggler-forest-portrait.png" }), + mengsan_ceremonial_beast_shuying: new Character({ sex: "none", group: "qun", hp: 252, maxHp: 252, skills: [], img: "extension/术樱包/mengsan/assets/portraits/ceremonial-beast-forest-portrait.png" }), + mengsan_kin_priest_shuying: new Character({ sex: "none", group: "qun", hp: 190, maxHp: 190, + skills: [], img: "extension/术樱包/mengsan/assets/portraits/kin-priest-forest-portrait.png" }), + mengsan_kin_follower_shuying: new Character({ sex: "none", group: "qun", hp: 59, maxHp: 59, + skills: [], img: "extension/术樱包/mengsan/assets/portraits/kin-follower-forest-portrait.png" }), + mengsan_vantom_shuying: new Character({ sex: "none", group: "qun", hp: 173, maxHp: 173, + skills: [], img: "extension/术樱包/mengsan/assets/portraits/vantom-forest-portrait.png" }), mengsan_bygone_effigy_shuying: new Character({ sex: "none", group: "qun", hp: 127, maxHp: 127, skills: [], img: "extension/术樱包/mengsan/assets/portraits/bygone-effigy-forest-portrait.png" }), mengsan_byrdonis_shuying: new Character({ sex: "none", group: "qun", hp: 82, maxHp: 82, @@ -57,15 +69,17 @@ export function createScenarioCharacters(Character) { skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-crossbow-portrait.png" }), mengsan_raider_tracker_shuying: new Character({ sex: "none", group: "qun", hp: 23, maxHp: 23, skills: [], img: "extension/术樱包/mengsan/assets/portraits/raider-tracker-portrait.png" }), - mengsan_sushuang_zhangshiping_shuying: new Character({ sex: "male", group: "shu", hp: 4, maxHp: 4, + mengsan_sushuang_zhangshiping_shuying: new Character({ sex: "male", group: "han", hp: 4, maxHp: 4, skills: [], img: "extension/术樱包/mengsan/assets/portraits/sushuang-zhangshiping.png", names: "苏|双-张|世平" }), }; } export const scenarioTranslations = { - mengsan_liubei_shuying: "界刘备", + mengsan_liubei_shuying: "刘备", + mengsan_guanyu_shuying: "关羽", + mengsan_zhangfei_shuying: "张飞", mengsan_liubei_shuying_title: "50/50生命 · 先天桃园羁绊", mengsan_taoyuan_bond_shuying: "桃园羁绊", - mengsan_taoyuan_bond_shuying_info: "先天:本征程初始结识关羽、张飞,二人羁绊均为8级。可在行军菜单中指定一名助战角色;战斗结束,到场增加50%升级进度,未到场增加25%。", + mengsan_taoyuan_bond_shuying_info: "先天:本征程初始结识关羽、张飞,二人羁绊均为8级,默认选择关羽助战。可在行军菜单中更换助战角色;战斗结束,到场增加50%升级进度,未到场增加25%。", mengsan_soldier_shuying: "士兵", mengsan_flyconid_shuying: "飞蝇菌子", mengsan_fogmog_shuying: "雾菇", @@ -75,6 +89,10 @@ export const scenarioTranslations = { mengsan_cubex_construct_shuying: "立柱构造体", mengsan_byrdonis_shuying: "多尼斯异鸟", mengsan_bygone_effigy_shuying: "旧日雕像", + mengsan_vantom_shuying: "墨影幻灵", + mengsan_ceremonial_beast_shuying: "仪式兽", + mengsan_kin_priest_shuying: "同族神官", + mengsan_kin_follower_shuying: "同族信徒", mengsan_phrog_parasite_shuying: "异蛙寄生虫", mengsan_wriggler_shuying: "扭动虫", mengsan_nibbit_pair_shuying: "小啃兽双只", diff --git a/mengsan/content/shared/rewards.js b/mengsan/content/shared/rewards.js index 87703ab..4c46c01 100644 --- a/mengsan/content/shared/rewards.js +++ b/mengsan/content/shared/rewards.js @@ -1,7 +1,10 @@ import { relicRewards, relicRewardIds } from "../../relics/definitions.js"; +import { cardPackRewards, cardPackRewardIds } from "../../cards/packs/rewards.js"; +import { cardDefinitions } from "../../cards/card-definitions.js"; export const rewards = { ...relicRewards, + ...cardPackRewards, "shared.reward.support.scout": { name: "道具·斥候援令", description: "本次征程后续每场战斗,友方张辽携 4 张手牌参战;重复获得不叠加人数。", support: { category: "item", battles: -1, unit: { character: "re_zhangliao", camp: "ally", hand: 4 } }, @@ -11,6 +14,34 @@ export const rewards = { card: { name: "mengsan_zhongsha" }, name: "获得一张【重杀】", description: "2费:造成8点伤害,然后给予目标2层易伤。", }, + "shared.reward.card.baozou": { + card: { name: "mengsan_baozou" }, name: "获得一张【暴走】", + description: "1费:造成9点伤害,然后将这张牌本场战斗中的伤害增加5点。", + }, + "shared.reward.card.yuanhen": { + card: { name: "mengsan_yuanhen" }, name: "获得一张【怨恨】", + description: "0费:造成5点伤害;本回合失去过生命时改为攻击2次。", + }, + "shared.reward.card.xuanfengzhan": { + card: { name: "mengsan_xuanfengzhan" }, name: "获得一张【旋风斩】", + description: "X费:消耗剩余费用,对所有敌人各造成5点伤害X次。", + }, + "shared.reward.card.qiling": { + card: { name: "mengsan_qiling" }, name: "获得一张【欺凌】", + description: "0费:造成4点伤害,目标每有1层易伤额外增加2点伤害。", + }, + "shared.reward.card.xueqiang": { + card: { name: "mengsan_xueqiang" }, name: "获得一张【血墙】", + description: "2费:失去2点生命,然后获得16点格挡。", + }, + "shared.reward.card.zhanli": { + card: { name: "mengsan_zhanli" }, name: "获得一张【战栗】", + description: "1费:给予一名敌人3层易伤。消耗。", + }, + "shared.reward.card.wuzhuang": { + card: { name: "mengsan_wuzhuang" }, name: "获得一张【武装】", + description: "1费:获得5点格挡,然后强化你手牌中的一张牌(仅本场战斗)。", + }, "shared.reward.card.songjianwushi": { card: { name: "mengsan_songjianwushi" }, name: "获得一张【耸肩无视】", description: "1费:获得8点格挡,然后抽1张牌。", @@ -94,8 +125,8 @@ export const rewards = { "shared.reward.maxHp": { effectId: "max_hp", name: "极品强化·生机", description: "生命上限与当前生命各 +5" }, }; -export const rewardPools = { - "shared.pool.battle.normal": [ +const battleCardRewardIds = [ + ...cardPackRewardIds, "shared.reward.card.sha", "shared.reward.card.zhongsha", "shared.reward.card.fangyu", @@ -113,14 +144,22 @@ export const rewardPools = { "shared.reward.card.fangxue", "shared.reward.card.jianyi", "shared.reward.card.pomie", + "shared.reward.card.baozou", + "shared.reward.card.yuanhen", + "shared.reward.card.xuanfengzhan", + "shared.reward.card.qiling", + "shared.reward.card.xueqiang", + "shared.reward.card.zhanli", + "shared.reward.card.wuzhuang", "shared.reward.card.songjianwushi", "shared.reward.card.wanmeidaji", "shared.reward.card.yubeidaji", "shared.reward.card.tao", "shared.reward.card.wuzhong", - ], - "shared.pool.boss.premium": [ - ...relicRewardIds, - "shared.reward.skill.heroic", - "shared.reward.item.handCharm", - "shared.reward.maxHp", - ], +]; + +export const rewardPools = { + "shared.pool.battle.normal": [...battleCardRewardIds], + "shared.pool.battle.elite": [...battleCardRewardIds], + "shared.pool.elite.relics": [...relicRewardIds], + "shared.pool.boss.premium": battleCardRewardIds.filter(id => + cardDefinitions[rewards[id].card?.name]?.rarity === "rare"), }; diff --git a/mengsan/docs/act1-completion-and-rewards-20261004.md b/mengsan/docs/act1-completion-and-rewards-20261004.md new file mode 100644 index 0000000..43969f6 --- /dev/null +++ b/mengsan/docs/act1-completion-and-rewards-20261004.md @@ -0,0 +1,35 @@ +# 第一章结束与战斗奖励调整(2026-10-04) + +本说明补充现有章节、助战和奖励文档,以当前源码为准。 + +## 第一章结束 + +第一章 Boss 胜利后先完成奖励选择与结算,再播放旁白: + +> 战斗还未完结,等待后续更新。 + +随后结束本次征程并记录通关,不进入第二章的占位内容。第一章索引保留,供结束旁白读取;章节配置通过 `endsCurrentContent` 与 `completionDialogue` 控制这一行为。 + +## 刘备和先天助战 + +显示名改为“刘备”,立绘使用 `image/character/liubei.jpg`,模式私有 ID 保持不变以兼容存档和专属卡归属。 + +先天桃园羁绊仍结识8级关羽和张飞,默认选中关羽。旧存档已结识关羽但未选择助战时补上默认选择;已选其他助战保留。默认选择只初始化一次,之后主动取消或改选不会被重置。专属开场仍不判定助战到场。 + +## 战斗奖励 + +| 战斗 | 必得奖励 | 可选卡牌 | +| --- | --- | --- | +| 普通 | 原配置金币 | 当前角色专属卡牌,三选一或跳过 | +| 精英 | 原配置金币+随机一件尚未持有的遗物 | 当前角色专属卡牌,三选一或跳过;每项候选普通20%、罕见60%、稀有20% | +| Boss | 原配置金币 | 三个不同的当前角色专属稀有牌候选,选一张或跳过 | + +精英先按稀有度抽取,再在该等级内抽取不重复的卡牌,避免各等级牌数影响配置概率。若某等级候选耗尽,只在尚有候选的等级之间重新分配概率。Boss 专属稀有牌不足时拒绝降级,不混入遗物、属性或技能奖励。 + +精英遗物与卡牌候选一起写入结算检查点,跳过卡牌仍领取金币和遗物。遗物不重复获得;已持有全部遗物时,界面明确说明不再重复发放。 + +普通随机卡牌奖励默认只允许当前角色专属牌。特定剧情的固定卡牌与开场强化包保持原规则;斥候谢礼与伏兵战利品作为特定剧情,显式配置 `allowSharedCardRewards: true` 保留原有通用牌。其他特定奖励也可使用此标记,通过抽奖入口的 `allowSharedCards` 选项传递,不改变永久牌组本身的可获得资格。Boss 不受此标记影响,始终限定角色专属稀有牌。 + +旧存档已经生成的待领奖候选保持原样,不在恢复结算时重抽。后续新结算使用新规则。 + +本次检查针对实际游戏源码;语法、配置核对不等于实机验收。 diff --git a/mengsan/docs/act1-early-battles-fix.md b/mengsan/docs/act1-early-battles-fix.md new file mode 100644 index 0000000..62b97d0 --- /dev/null +++ b/mengsan/docs/act1-early-battles-fix.md @@ -0,0 +1,14 @@ +# 第一章早期精英限制修正(2026-10-04) + +本说明修正 `act1-encounters.md` 中“精英可能在前三场普通战斗完成前出现”的旧规则。 + +- 角色专属开场不占弱怪池名额。开场后前三场普通战斗仍从弱怪池抽取,胜利结算后计数。 +- 新地图开场后的前三层,随机抽到的精英改为普通战斗;事件、休息、商店仍可出现。 +- 未完成三场普通战斗时,当前可前往的随机精英节点也改为普通战斗,并使用弱怪池、普通阶级和普通战斗奖励。这些胜场计入三场名额。 +- 已有第一章地图在展示时修正前三层及当前可走的随机精英节点,保留完成记录和路线,无需重新开档。 +- 完成第三场后,后续普通战斗使用强怪池,尚未转换的后续精英节点仍使用精英池。 +- 固定内容、专属及剧情战斗、Boss、第二章和第三章保持原规则。失败或重试不增加胜场计数,已保存的重试遭遇保持原组合。 + +实现入口:`progression/encounter-progress.js` 的早期保护和计数常量,以及 `progression/state.js` 的地图生成、可选节点准备与遭遇抽取。 + +本次仅核对源码、语法及差异,未启动游戏;实机验收待用户执行。 diff --git a/mengsan/docs/act1-encounters.md b/mengsan/docs/act1-encounters.md new file mode 100644 index 0000000..c3eb8dc --- /dev/null +++ b/mengsan/docs/act1-encounters.md @@ -0,0 +1,68 @@ +# 第一章遭遇池(2026-10-03) + +本次仅用于第一章。第二、三章仍使用各自原有配置,后续单独设计。 + +## 普通战斗阶段 + +离开角色专属开场后,前3场普通战斗从弱怪池抽取,第4场起从强怪池抽取。 +按普通战斗胜利完成结算计数,事件、休息、精英、Boss、专属及剧情战斗不计。 +失败、重试和未完成战斗不增加计数;重试保留原遭遇组合,重复结算不重复计数。 +普通遭遇条目等概率抽取,条目内部的“或”方案也等概率抽取,允许不同场重复遭遇。 + +计数存于 `run.statistics.normalBattles`。旧第一章存档缺少此字段时, +从当前地图已完成且无固定内容的普通战斗节点恢复,不重建已走过的地图。 +旧存档已经走过的普通战斗计入阶段,不从更新后重新赠送3场弱怪。 + +## 弱怪池 + +| 遭遇 | 同场成员 | +| --- | --- | +| 毛绒伏地虫 | 1只毛绒伏地虫 | +| 小啃兽 | 1只小啃兽 | +| 缩小甲虫 | 1只缩小甲虫 | +| 史莱姆三只 | 树叶史莱姆(小)+随机树叶/树枝史莱姆(中)+树枝史莱姆(小) | + +## 强怪池 + +| 遭遇 | 同场成员 | +| --- | --- | +| 立柱构造体 | 1只立柱构造体 | +| 中型史莱姆与飞蝇菌子 | 随机树叶/树枝史莱姆(中)+飞蝇菌子 | +| 闪光贾克斯果与飞蝇菌子 | 闪光贾克斯果+飞蝇菌子 | +| 雾菇 | 1只雾菇,沿用其本场召唤规则 | +| 墨宝三只 | 3只墨宝,保留前/中/后站位 | +| 蛮兽 | 1只蛮兽 | +| 小啃兽双只 | 2只小啃兽 | +| 缩小甲虫与毛绒伏地虫 | 缩小甲虫+毛绒伏地虫 | +| 劫掠者团伙 | 从暴徒、刺客、斧手、弩手、追踪者中随机选3名,同场不重复 | +| 史莱姆四只 | 树叶(中)+树枝(中)+树叶(小)+树枝(小) | +| 蛇行扼杀者与同行怪物 | 下列4个方案随机取1个:贾克斯果+扼杀者;树叶(中)+扼杀者;树枝(中)+扼杀者;树叶(小)+树枝(小)+扼杀者 | +| 藤蔓蹒跚者 | 1只藤蔓蹒跚者 | + +## 精英与Boss + +精英池:多尼斯异鸟、旧日雕像、异蛙寄生虫,各占一个条目。 +异蛙保留寄生与生成扭动虫的机制。 + +Boss池:墨影幻灵、仪式兽、同族神官+2名同族信徒,各占一个条目。 +同族信徒分别绑定本场神官,保留爪牙与胜利判断规则。 + +随机类型楼层的节点权重仍为普通50、精英15、事件15、休息10、商店10, +合计100。固定开场、宝箱、休息与最终Boss楼层不参与这一概率抽取。 +精英可能在前三场普通战斗完成前出现,但不占弱池的三场名额。 + +地图普通战斗使用交叉兵器图标;精英使用紫色盾徽图标和“精英”文字; +Boss使用独立旗帜图标。随机出现不等于每张地图必定有精英。 + +## 配置入口 + +- `content/acts/act1/encounters.js`:4个弱遭遇、12个强遭遇、3个精英、3个Boss。 +- `content/acts/act1/map.js`:地图类型权重及遭遇池ID。 +- `content/registry.js`:跨章节遭遇注册与组合验证。 +- `content/encounters.js`:从遭遇及其内部方案中抽取,并创建独立战斗计划。 +- `progression/encounter-progress.js`:普通战斗计数与旧存档恢复。 +- `battle/battle-settlement.js`:胜利结算事务中推进计数。 + +怪物生命、意图、牌堆、分裂、召唤与特殊行为沿用各自已实现的规则。 +组合成员同时上场;不把普通组中的多个成员改成精英阶级。 +本次仅做源码语法、引用和内容文件核对,行为测试及实机验收由用户执行。 diff --git a/mengsan/docs/battle-config.md b/mengsan/docs/battle-config.md index 2dcf62a..c0ca392 100644 --- a/mengsan/docs/battle-config.md +++ b/mengsan/docs/battle-config.md @@ -9,6 +9,8 @@ - 本版单场累计最多 8 个席位,包含主角、初始单位、奖励支援、以后出现的援军。 ## 配置位置 +第一章弱/强/精英/Boss遭遇配置与阶段规则见 [act1-encounters.md](act1-encounters.md)。 + 节点内容或剧情选项的 `outcome.battle` 中增加 `battlePlan`。 Boss 也可以在大关配置的 `bossBattlePlan` 中定义。 `units` 中每一项是一名初始角色,因此数组长度就是初始非主角人数。 diff --git a/mengsan/docs/bonds.md b/mengsan/docs/bonds.md index 40a9841..a37cb27 100644 --- a/mengsan/docs/bonds.md +++ b/mengsan/docs/bonds.md @@ -9,6 +9,9 @@ 1 级为 0%,10 级为 100%。 - 战前在地图切换助战;开战只判定一次。成功后占玩家下家,AI 自动行动。 原有剧情与道具支援保留,原有 8 席校验和座次代码保留。 +- 第一章第一层的角色专属首战不触发羁绊助战,也不累计本场羁绊成长。 + 指定助战不等于必到:8级为77.78%,10级为100%;濒死时仍须先救助。 + 开战日志明确区分首战限制、濒死、配置缺失、概率未命中与实际到场。 - 完成战斗结算:指定 NPC 到场 +50%,未到场 +25%。 濒死、战斗配置未补齐也属于未到场。不要求 NPC 存活,不限定胜利。 退出或重载未完成战斗不增长;重复结算不重复增长。 @@ -29,8 +32,11 @@ 配置位置:`bonds/definitions.js` 的 `bondDefinitions`。 -关羽、张飞的 `portrait` 以及 10 个 `growth` 行目前均预留。 -成长数值与技能未填写完整前可以结识、选择与成长,但不能参战; +关羽、张飞的 10 个 `growth` 行已补齐可用基础配置:40生命、4张初始手牌、 +每回合摸2张、手牌上限5、每回合3费用、`skills:[]`。当前各等级使用相同 +战斗数值,等级只影响到场概率;这不是已确认的最终平衡数值,后续可逐行调整。 +两个 NPC 的成长数组和技能数组分别创建,不共享可变状态。 +其它新增 NPC 的配置未补齐时可以结识、选择与成长,但不能参战。 不继承原版武将技能。`deck` 和 `intents` 无需预先填写,已有默认兜底。 `portrait` 为空时不显示原版立绘。 @@ -112,4 +118,9 @@ outcome: { `tools/test_bonds.mjs` 覆盖初始化、事件重复结识、单选、概率边界、 成长溢出/上限、濒死/救助、独立牌组、结算重复提交和多目标意图。 另覆盖默认牌库与意图兜底、独立循环、费用不足、自定义意图和击晕期间禁止执行。 -这些检查不等同于游戏原生战斗运行验证;空缺战斗配置待补充后再实机验收。 +这些检查不等同于游戏原生战斗运行验证。 + +2026-10-03 修复说明:此前两名 NPC 的全部成长字段为 `null`,导致 +`getBondCombat` 在概率判定前返回空,已指定的助战始终不会生成。 +现补齐基础配置,并加强等级数值边界与累计技能数组校验。 +本次仅检查源码语法和导入文件;未运行行为测试、浏览器探针或实机验收。 diff --git a/mengsan/docs/card-description-and-library-performance-20261004.md b/mengsan/docs/card-description-and-library-performance-20261004.md new file mode 100644 index 0000000..cc62021 --- /dev/null +++ b/mengsan/docs/card-description-and-library-performance-20261004.md @@ -0,0 +1,25 @@ +# 卡牌介绍与图鉴滚动优化(2026-10-04) + +根据 66.png 与本次要求修正此前“介绍包含当前费用”的约定。 + +## 描述 + +- 卡牌介绍去掉“梦三:”前缀,不再重复普通主动使用费用或普通响应免耗费用。 +- 费用角标与图鉴费用筛选继续保留,费用支付和卡牌效果不变。 +- X 费用保留消耗规则;X 表示本次消耗的剩余费用。获能、减费、临时免费及濒死救援等特殊机制仍需说明。 +- 已强化卡牌继续显示当前效果数值,不追加升级前后对比。 +- 模式注册的动态介绍与静态翻译统一清理;图鉴、持有牌、奖励卡片也使用同一清理入口。 + +## 图鉴与持有牌 + +- 以原有卡牌大小与网格排列计算全部行,保留完整滚动高度。 +- 只挂载可见行与上下各两行预览;继续显示的卡牌复用,离开范围后回收。 +- 滚动监听不阻塞原生滚动,并合并到每帧更新;关闭弹窗时取消更新并移除监听。 +- 图鉴介绍与搜索文本按弹窗缓存;持有牌介绍按实例缓存。搜索输入等待 140 毫秒后更新,刷新牌堆时清理旧缓存。 +- 图片异步解码,卡牌绘制范围隔离;避免滚动时重新解析全部介绍和重建全部图片。 +- 详情返回保留原滚动位置;窗口尺寸变化重算行列;方向键与 Tab 可访问尚未挂载的后续卡牌。 +- 不修改牌组、存档或抽牌顺序。战斗中分区牌堆保留原有顺序展示。 + +## 验收范围 + +仅进行源码、语法与差异核对,未运行行为测试或实机性能测试。实机应核对:暴走等普通牌介绍仅显示效果;旋风斩、倾泻等 X 牌有明确消耗说明;大量卡牌时滚动、筛选、返回详情、缩放与键盘连续浏览正常。 diff --git a/mengsan/docs/ceremonial-beast.md b/mengsan/docs/ceremonial-beast.md new file mode 100644 index 0000000..7093c5b --- /dev/null +++ b/mengsan/docs/ceremonial-beast.md @@ -0,0 +1,11 @@ +# 仪式兽 CEREMONIAL_BEAST +来源(2026-10-03):https://sts2.huijiwiki.com/wiki/仪式兽;https://sts2.huijiwiki.com/wiki/横冲直撞;https://sts2.huijiwiki.com/wiki/昏眩 +普通252生命,Boss,无初始能力。第一层bossEnemies追加仪式兽,保留墨影幻灵和同族小队及act1Map.boss旧兼容值。单单位Boss,不加入普通/精英池,不变更其他层及角色专属开局。 +第一阶段:首行动跺地,获得150层横冲直撞;之后重复18伤害并自身力量+2。阶段二:野兽咆哮(昏眩)→踩踏15→碾碎17且自身力量+3→野兽咆哮。固定选择不消耗随机数;原生费用不足仍推进,击晕advance/retry沿用现有体系。 +横冲直撞非护甲、不吸收伤害、不逐回合衰减;普通252最大生命的阈值150,明确覆盖最大生命按150/252等比取整。阶段一获得状态后必须发生damageEnd且伤害为正、角色存活、生命≤层数才触发;此前低血量不自动触发,loseHp不触发。移除横冲直撞、无论原力量正负重置0、跳过一次意图和原生出牌阶段;恢复后从野兽咆哮开始,不重复跺地。第二阶段不返回阶段一。 +阶段切换中止当前意图后续目标/增益,避免反伤在横冲直撞途中击晕后仍获得力量。死亡、换场、会话结束同样中止后续效果。存活Boss仍阻止胜利。 +昏眩是负面状态,不是晕眩卡;人工制品拦截一次施加。每位存活敌对角色独立至多主动使用1张实体/虚拟牌,零费牌同样计入,多目标仍算一张,失败/取消及响应不计数。useCard0在费用扣除前拒绝第二张强制使用,useCard1幂等计数,cardEnabled/cardEnabled2同步限制;自身回合结束/死亡/会话结束解除。梦三响应体系无STS对应,本实现不封锁闪/无懈响应。 +Boss费用4,抽3,上限7;意图费用1,仍保留原生出牌。力量沿用每段攻击与原生攻击牌,不重复加算。永久牌组不变;独立不透明3:4密林立绘,基于网页原画二次绘制。 +进阶A8 HP262,A9跺地160、横冲直撞20、踩踏17、碾碎19/+4,仅记录,不混入普通数值;多人没有自动新增血量缩放规则,仅跟随实际maxHp比例。 +原画https://huiji-public.huijistatic.com/sts2/uploads/8/80/Ceremonial_beast.png,版权归原权利方;文本CC BY-NC-SA4.0。提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\beast-prompts.json(built-in image_gen)。 +所有隔离检查不能称为实机验收。 diff --git a/mengsan/docs/install-uninstall-fixes-20261004.md b/mengsan/docs/install-uninstall-fixes-20261004.md new file mode 100644 index 0000000..daa7ddf --- /dev/null +++ b/mengsan/docs/install-uninstall-fixes-20261004.md @@ -0,0 +1,52 @@ +# 梦三安装、卸载与发布修复记录(2026-10-04) + +本文件对应 `install-uninstall-release-review-20261004.md` 的问题修复。修改的是实际游戏源码;没有执行真实安装、卸载、标签推送或发布。 + +## 已修复的流程 + +| 原问题 | 当前处理 | +| --- | --- | +| Gitea 在标签推送后生成清单,客户端无法从标签读取同一清单 | 发布流程只校验标签中已提交的清单;缺少清单、版本不符、资源哈希不符或内部导入缺失都会停止发布。 | +| 写入失败仍报告安装完成 | 统一识别字符串、Error、FileError 和写入事件错误;每次写入后读取并逐字节比对,模块完成后再校验全部文件的 SHA-256。 | +| 删除失败、网络失败或手机端提前回调 | 等待真实删除完成,再确认文件不存在;检查失败不会当成文件缺失;文件操作有超时处理。 | +| 卸载中断后仍注册残缺模式 | 删除前原子保存 `uninstalling` 和拥有的文件清单;重启后暂停注册,管理按钮可继续卸载。全部删除确认后才保存 `disabled`。 | +| 旧版清单不可用时改用最新版卸载,遗漏旧资源 | 正常安装保存本地所有权清单;完整 ZIP 也附带模块与迁移清单。卸载优先使用这些记录。明确的旧版本无法取回完整清单时停止操作,不猜测所有权。 | +| 回滚失败后仍删除恢复文件 | 每次操作使用独立临时目录;回滚失败时保留目录及旧文件,尽可能写入 `recovery.json`,安装记录保留恢复目录。 | + +安装、更新和查漏补缺共享模块状态处理:首次文件改动前保存 `installing`;文件与安装记录均完成后才标为 `installed`。中断或最终记录保存失败时,保持待修复状态。回滚完整且此前已有可靠安装记录时,恢复此前状态。用户存档没有被删除。 + +IndexedDB 的两项安装配置在同一个事务中提交,并等待 `oncomplete`;本地存储环境一次写入配置字典。数据库写入失败不会切换到其他存储后端,也不会继续删文件。 + +网络响应丢失或写入超时可能留下未结束的文件操作,此时本次会话停止进一步文件改动,提示重启后修复,避免后到的回调覆盖另一次操作。恢复文件不被后续更新覆盖。 + +## 发布资源范围 + +清单生成器排除本地《杀戮尖塔 2》解包目录、`mengsan/assets/sts2`、依赖目录、运行日志、临时文件和环境配置;扫描时跳过这些目录,不再遍历其中数万个文件。 + +当前工作区资源清单:主包 278 个文件,梦三 457 个文件,共 735 个资源文件。旧版迁移清单记录 500 个已知梦三路径。版本暂保留 `2.0.1.1-rc.1`,没有替用户决定新发布版本或创建标签。 + +## Gitea 正式发布步骤 + +1. 设定正式要发布的版本号,更新 `extension.js` 中的版本,并提交全部运行源码、素材和发布工具。特别注意新增的梦三运行文件;只提交原先已跟踪文件会遗漏它们。 +2. 运行 `tools/generate_manifest.bat`,或者在扩展仓库中运行: + + ```text + python tools/prepare_release_manifest.py --root . --version 你的版本号 + ``` + + 工具要求源码已经提交,从 Git 快照生成和校验清单,避免 Windows 换行格式与标签内容不同。它不会创建标签、提交或推送。 +3. 提交 `dist/manifest.json`、`dist/modules/mengsan.json` 和 `dist/modules/mengsan.legacy.json`。主清单原有忽略规则仍存在,新仓库首次纳入主清单时需要明确加入该文件。 +4. 从包含这些清单的提交创建全新的对应版本标签,再推送。Gitea 工作流会对标签导出的文件重新校验,通过后才打包和发布。 + +GitHub 发布工作流继续从已提交的 Git 快照生成清单,在生成后和标签打包前各校验一次。两种完整 ZIP 都包含模块清单及旧版迁移清单,供本地所有权识别使用。 + +`tools/generate_manifest.py` 仍可生成工作区检查清单;这种清单不能直接当成正式标签清单,发布使用上述快照工具或 GitHub 工作流。 + +## 本次检查与验收边界 + +- 修改后的 JavaScript 和三个 Python 工具通过语法检查。 +- 两份工作流中的 PowerShell 脚本、内嵌 Python 打包脚本通过语法检查。 +- 当前工作区的版本、文件集合、大小、SHA-256、模块归属、迁移清单及内部相对导入检查通过。 +- 安装、更新、查漏补缺和卸载的成功、失败、中断路径做了源码复核;没有执行游戏运行时探针或模拟验收。 + +实机仍需验证正常安装后重启、卸载后重启、安装中断后修复、卸载中断后续删,以及已有角色存档在卸载和重装后仍可使用。本次静态结果不代表这些实机流程已经验收。 diff --git a/mengsan/docs/install-uninstall-release-review-20261004.md b/mengsan/docs/install-uninstall-release-review-20261004.md new file mode 100644 index 0000000..e71520a --- /dev/null +++ b/mengsan/docs/install-uninstall-release-review-20261004.md @@ -0,0 +1,78 @@ +# 梦三安装、卸载与发布前审查(2026-10-04) + +结论:当前存在发布前应处理的问题,不能把语法检查通过视为安装、卸载验收通过。本次只审查实际源码与本地发布材料,没有修改安装器,没有执行真实安装、卸载、更新、断网模拟或发布操作。 + +## 发现的问题 + +### 1. P1:当前 Gitea 发布材料不能直接用于客户端安装 + +位置:`.gitea/workflows/release.yml:61`、`update.js:238`、`tools/UPDATE.md:20`。 + +Gitea 工作流在标签已经创建后,克隆标签,在临时目录生成清单并上传附件;它不会把生成的清单写回标签。客户端却从该标签的 raw 路径读取 `dist/manifest.json` 和模块清单,不读取附件。 + +当前本地 `dist/manifest.json` 仍是 2.0.1.1,缺少 `update_channels` 与 `modules`;本地 `dist/modules` 不存在。如果从这种源码状态创建新版 Gitea 标签,附件即使生成成功,客户端也会在清单校验或获取模块清单时失败。 + +发布前必须把同版本的主包清单、模块清单和迁移清单提交到标签所指向的提交;Gitea 工作流也应先核对标签内清单,而不是仅核对临时生成的附件。GitHub 工作流已包含先生成并提交清单、再创建标签的步骤,两条流程不能混为一谈。 + +### 2. P1:非 Error 类型的写入失败被当成成功 + +位置:`update.js:470`;适配证据:引擎 `noname/init/browser.js:128`、`noname/init/cordova.js:287`。 + +包装器只拒绝 `result instanceof Error`。浏览器端写入失败传回字符串,Cordova 的文件错误可以是含 code 的对象,写入结束回调也可能通过事件目标携带错误。这些情况都会进入 resolve。 + +如果临时下载已成功,而最后覆盖真实文件失败,`updateAllFiles` 不会再读取真实目标做哈希校验,仍可能推进安装版本并提示完成。应规范化各端错误,并在真实文件写入后读取校验;只有最终资源校验成功才标记安装完成。 + +### 3. P1:删除错误被丢弃,浏览器异常还可能一直占用操作锁 + +位置:`update.js:483`;适配证据:引擎 `noname/init/node.js:186`、`noname/init/browser.js:138`、`noname/init/cordova.js:300`。 + +删除包装器忽略回调参数,也没有向支持第三个参数的浏览器实现传入错误回调。浏览器本地文件服务请求失败时,默认错误处理不会结算包装器的 Promise,安装或卸载可以一直停住,finally 无法释放锁。 + +删除后虽然有存在性检查,但 `safeCheckFile` 又把检查错误当成缺失;删除失败同时检查失败时仍可能提示卸载成功。Cordova 实现还在实际删除完成前调用回调,因此立即检查也有误报失败的竞态。 + +应可靠等待删除完成,传播删除与检查错误,仅把确定的“文件不存在”当作可忽略结果。 + +### 4. P1:卸载部分失败后仍保留已安装状态 + +位置:`update.js:1194`、`extension.js:253`。 + +卸载按列表直接删除,直到全部成功才设置 disabled。若前面若干文件已删除,后续文件因锁定、权限等原因失败,catch 仅提示失败,安装版本仍保持原值。 + +重启后启动逻辑仍按这个版本判断模块可用;若入口仍在而依赖文件已被删除,会注册一个无法正常进入的梦三模式。卸载应有可恢复的处理中状态:处理中不注册模式,重试时继续使用原所有权清单,全部删除成功才完成卸载。 + +### 5. P2:读取旧版清单失败后用新版清单卸载,可能遗漏旧文件 + +位置:`update.js:1173`、`update.js:1180`。 + +已安装模块标签不可用,或切换到没有该旧标签的下载源时,会回退到主包版本或最新模块清单。新版清单没有旧版已经移除的路径,这些文件就不会被删除;循环结束后仍设置 disabled 并提示文件已卸载。 + +应在安装或成功更新时保存本地模块所有权清单,卸载依据实际安装的清单。无法取得准确所有权时,不应把仅删除新版路径说成完整卸载;不能用递归删除整个目录代替,以免删除用户自行放入的文件。 + +### 6. P2:回滚失败后仍删除唯一旧文件备份 + +位置:`update.js:826`、`update.js:832`。 + +回写旧文件失败只记录到控制台,finally 仍删除 old 下的备份。例如真实目标被锁定或空间不足导致恢复失败,而临时目录可以正常删除,此时旧版本的恢复副本会被清除。 + +应跟踪未恢复的文件,保留对应备份与恢复信息,明确提示恢复未完成,不能执行无条件清理。 + +## 核对正常路径的结果 + +- 安装下载先做 SHA-256 和大小校验,随后才进入真实文件替换。 +- 主包与梦三使用独立清单;disabled 状态下主包更新和查漏补缺不会主动安装梦三。 +- 模块清单校验限制文件属于 `mengsan/`,拒绝路径穿越;正常卸载按清单删除,不递归清空模块目录。 +- 卸载入口没有清除梦三征程存档或通关记录。 +- 安装、卸载取消确认后会进入 finally,释放菜单操作锁。 +- 模式使用延迟加载;只有进入梦三时才导入完整模式。 +- 明确成功的普通安装或更新会在资源步骤后保存模块版本;存在上述跨端错误回调问题,因此该顺序本身不足以保证资源已正确落盘。 + +## 本次静态核对 + +- `extension.js`、`menu.js`、`update.js`、`mengsan/register.js` 语法检查通过。 +- 扫描梦三源码 398 条显式相对模块引用,未发现目标文件缺失;这不覆盖运行时动态路径和网络清单。 +- 最近新增的描述模块、虚拟网格模块、地图存档清理模块和刘备立绘在实际目录中存在。 +- 当前梦三目录有 267 个未跟踪文件,近期新增的运行时代码和资源也在其中。发布工作流基于 Git 标签打包,发布前必须把需要发布的文件纳入提交,不能只核对本地目录存在。 + +## 修复后的实机验收范围 + +首次安装、卸载后重装、已安装版本升级、旧整包迁移、保留存档后重装、取消操作、写入失败、删除失败、断网重试,以及 Windows、浏览器文件服务与支持的手机端。还需分别验证 GitHub 和 Gitea 标签内 raw 清单与发布附件、资源文件完全对应。 diff --git a/mengsan/docs/map-only-save-and-defeat-20261004.md b/mengsan/docs/map-only-save-and-defeat-20261004.md new file mode 100644 index 0000000..6ffe7d6 --- /dev/null +++ b/mengsan/docs/map-only-save-and-defeat-20261004.md @@ -0,0 +1,35 @@ +# 地图存档与阵亡处理规则(2026-10-04) + +本说明修正此前的战斗重试、持久化待领奖与失败恢复规则,以当前源码为准。 + +## 存档位置 + +- 自动存档保存征程角色、成长、牌组、金币和已经提交的地图进度。 +- 不保存当前战斗、重试节点、重试遭遇、未确认的奖励选择或本场助战到场状态。 +- 战斗结算候选与已选结果只在内存保存。过关并完成奖励选择后,一次性提交奖励、战后成长与完成节点。 +- 战斗中卡牌带来的永久成长先更新内存,随过关结果保存,不单独覆盖地图存档。 +- 战斗中或领奖确认前读档,回到上一次保存的地图;未完成节点仍可重新选择。 +- 读取旧存档时忽略 `battleFlow`、`bondBattle` 与战斗临时卡牌效果 `sharedBattleCardEffects`,不自动进入旧战斗或失败弹窗。以后写入存档也不保留这些字段。 + +## 角色阵亡 + +阵亡后显示两个选项,无倒计时,也不自动提交失败: + +1. **重新开始**:结束旧征程,以阵亡角色重新创建第一章征程,初始牌组、金币、生命、先天羁绊等按该角色的新征程配置初始化,不再次选择武将。 +2. **返回主菜单**:结束旧征程、清除当前征程存档,并打开游戏模式选择菜单。 + +只在玩家点击其中一个选项后提交变更。等待选择期间,上一次地图存档一直保留,玩家无需抢在失败写档前读档。阵亡界面不提供绕过这两个选项的额外流程菜单。 + +旧征程的结束记录与存档替换或清除在同一事务中写入。写入失败时保持旧存档;内存中已选的新征程和奖励结果不重新计算,重试保存不会重复发奖或重复创建征程。重复提交使用同一已提交结果。 + +## 核对入口 + +- `progression/map-checkpoint.js`、`progression/mode-storage.js`:地图存档字段清理、旧档读取与事务写入。 +- `battle/battle-settlement.js`:内存结算、同角色重开与最终提交。 +- `battle/battle-flow.js`:阵亡等待选择及结算、清理顺序。 +- `mode.js`:阵亡按钮、地图恢复、主菜单及战斗中成长延后保存。 +- `ui/flow-ui.js`:阵亡界面关闭额外流程菜单。 + +实机验收建议:战斗中读档应回地图;阵亡弹窗停留后读档仍可继续原征程;点击重新开始应沿用当前角色从头开始;点击返回主菜单后旧征程应结束;过关奖励选择或跳过后再次读档不应重复发奖。 + +本次仅核对实际源码、语法和差异,未启动游戏或运行行为测试。 diff --git a/mengsan/docs/public-judgement-pile-20261004.md b/mengsan/docs/public-judgement-pile-20261004.md new file mode 100644 index 0000000..6c6d870 --- /dev/null +++ b/mengsan/docs/public-judgement-pile-20261004.md @@ -0,0 +1,12 @@ +# 判定使用公共牌堆(2026-10-04) + +本说明补充个人牌堆规则,以当前源码为准。 + +- 玩家摸牌、弃牌、个人牌堆洗牌继续使用个人抽牌堆与个人弃牌堆。 +- 普通判定从公共牌堆取判定牌,公共牌堆不足时沿用原生公共洗牌流程,不消耗个人牌堆或触发个人牌堆不足失败。 +- 创建判定事件时,默认判定牌结算位置使用公共弃牌堆。判定、判定回调和对应的处理区清理均使用公共节点,避免公共判定牌被个人弃牌堆收录。 +- 判定中触发的独立摸牌或弃牌事件仍按其所属角色处理,不因父事件是判定而被改为公共摸牌或弃牌。 +- 原生改判、获取判定牌及明确指定结算位置的技能保持原有处理;玩家自己的改判牌仍由个人牌堆的归属回收逻辑处理。 +- 接管只作用于梦三战斗会话,结束后恢复原有牌堆访问和角色判定方法,不修改全局引擎文件。 + +实现入口:`cards/personal-piles.js`。本次仅检查实际源码与语法,未运行游戏验收。 diff --git a/mengsan/docs/the-kin.md b/mengsan/docs/the-kin.md new file mode 100644 index 0000000..0e253f3 --- /dev/null +++ b/mengsan/docs/the-kin.md @@ -0,0 +1,13 @@ +# 同族小队 THE KIN + +2026-10-03来源:https://sts2.huijiwiki.com/wiki/同族小队 +首领遭遇由1名同族神官KIN_PRIEST和2名同族信徒KIN_FOLLOWER组成,不是一个融合角色。注册mengsan_kin_priest_shuying / mengsan_kin_follower_shuying。 +普通神官HP190;信徒58~59,入场使用征程随机源独立抽取,默认资料值59;覆盖HP不消耗随机数。进阶A8神官199/信徒62~63,A9法球9及仪式/舞蹈3只记录,不混入当前普通数值。 +第一层act1Map.boss保留墨影幻灵作为旧配置兼容值,新增bossEnemies=[墨影幻灵,同族神官],等权种子选择。神官条目自动展开三单位kin-group;信徒不单独加入首领/普通/精英池。专属开局、其他层和奖励保持不变。现有墨影幻灵保留;旧墨影单一首领测试新增对应池选择适配,旧报告不覆盖。 +网页SVG神官:开始→脆弱法球8+1脆弱→虚弱法球8+1虚弱→灵魂光束3×3→黑暗仪式自身力量+2→脆弱法球。信徒:开始→快斩5→回旋镖2×2→力量之舞自身力量+2→快斩。两个信徒同一起始动作,没有虚构错位开场、召唤/复活、群体力量/治疗、阶段切换。 +爪牙不单独阻止胜利:所属神官被击杀即可获胜,无需击败存活信徒;不额外发第二份奖励。与普通敌人混合时普通敌人仍须击败。绑定关卡单位ID,不串到另一神官;孤立信徒整回合取消、抽牌0,不能继续原生出牌。神官死亡即时清空所属信徒意图,没有杀死信徒的副作用或复活逻辑。 +沿用梦三神官Boss抽3/上限7/费用4,信徒普通抽1/上限3/费用2,每次意图消耗1费用,随后保留原生出牌;费用不足不发动但推进循环;击晕advance/retry沿用同一恢复。力量按每段攻击和原生攻击牌生效,脚本不重复加。脆弱/虚弱通过已有人工制品拦截入口,伤害先结算,目标存活才施加;虚弱是weak,不误标成易伤。伤害作用所有存活敌对角色,沿用逐目标护甲与状态。 +固定循环不抽随机数;战斗结束、源死亡、换场后停止后续攻击及增益。三角色状态分别记录,会话清理与旧会话保护,永久牌组不变。 +两张独立3:4不透明密林立绘,基于各自网页原画。原画URL:https://huiji-public.huijistatic.com/sts2/uploads/a/a4/Kin_priest.png;https://huiji-public.huijistatic.com/sts2/uploads/5/51/Kin_follower.png(返回WEBP,保存真实扩展名)。 +内置image_gen提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\kin-prompts.json +原画版权归原权利方,网页文本CC BY-NC-SA4.0。隔离行为检查不是实机验收。 diff --git a/mengsan/docs/vantom.md b/mengsan/docs/vantom.md new file mode 100644 index 0000000..ea0d950 --- /dev/null +++ b/mengsan/docs/vantom.md @@ -0,0 +1,11 @@ +# 墨影幻灵 VANTOM + +2026-10-03 来源:https://sts2.huijiwiki.com/wiki/墨影幻灵;https://sts2.huijiwiki.com/wiki/滑溜;https://sts2.huijiwiki.com/wiki/伤口 +角色 mengsan_vantom_shuying,Vantom / HEALTH / MONSTER_TYPES。普通HP173,第一层act1Map.boss由re_lvbu改为此首领,普通池与精英池不变,专属开局及其他层不变。A8生命183、A9墨迹8/长枪7×2/肢解30只记录,不混入当前普通数值。 +网页正文及滑溜说明写开场9层,更新历史0.104.0写9→8(9),来源矛盾,本次正文优先采用9。滑溜仅初始获得,不每回合刷新;每次实际生命损失最多1,实际掉血才消耗1层,完全格挡和取消事件不消耗;多段逐段消耗。沿用墨宝同一原生changeHp限幅,保留正常护甲消耗、loseHp与非攻击伤害规则。 +SVG:开始→墨迹(7)→墨水长枪(6×2)→肢解(26+个人弃牌堆3伤口)→准备(+2永久力量)→墨迹。固定循环不抽随机数。力量叠加每段脚本攻击及原生攻击牌,准备不附加格挡。没有阶段转换、召唤墨宝、额外被动;轶事不当作战斗机制。 +沿用梦三Boss抽3/手牌上限7/费用4,意图消耗1费用,随后保留原生出牌阶段;脚本攻击作用所有存活敌对角色,逐目标状态计算,护甲原生抵扣。费用不足推进循环但不攻击、不塞牌、不加力量;击晕advance/retry保持原生流程。 +伤口是不能被打出的状态牌,只有unplayable;没有虚无、消耗、掉血、抽牌或升级机制。肢解全部伤害结算完后只给仍存活的敌对角色个人弃牌堆加入3张,角色死亡、源死亡、结束或换场不继续;不污染公共牌堆和永久牌组。本人及支援各自牌堆、唯一ID;无个人牌堆则跳过。 +独立密林Boss3:4不透明立绘;伤口独立5:7宣纸水墨卡面,fullimage和本地选择器,无文字牌框。原画:https://huiji-public.huijistatic.com/sts2/uploads/f/f0/Vantom.png;https://huiji-public.huijistatic.com/sts2/uploads/a/a4/Wound.png +生成提示词:D:\noname\resources\app\.codex-artifacts\mengsan-hand-fan-20261001\vantom-prompts.json +原画版权归原权利方,网页文本CC BY-NC-SA4.0。隔离验证不是实机验收。 diff --git a/mengsan/mode.js b/mengsan/mode.js index 9126dbb..49d31f1 100644 --- a/mengsan/mode.js +++ b/mengsan/mode.js @@ -1,14 +1,24 @@ import { createScenarioCharacters, scenarioTranslations } from "./content/scenario-characters.js"; import { createMengsanCards } from "./cards/mode-cards.js"; import { createStrikeCounter } from "./cards/strike-counter.js"; +import { resolveRetiredCardReward } from "./cards/retired-cards.js"; import { createBlockDrawer } from "./cards/block-draw.js"; +import { createHandUpgrader } from "./cards/hand-upgrade.js"; +import { createLifeLossBlocker } from "./cards/life-loss-block.js"; +import { createRampage } from "./cards/rampage.js"; +import { createSpite } from "./cards/spite.js"; +import { createWhirlwind } from "./cards/whirlwind.js"; import { createHavocPlayer } from "./cards/havoc-play.js"; import { createHandExhauster } from "./cards/hand-exhaust.js"; +import { createCardPackRuntime } from "./cards/packs/index.js"; +import { ironcladBlocksDraw, ironcladPreservesBlock, afterIroncladDraw, beforeIroncladBlock } from "./cards/ironclad-hooks.js"; +import { afterSharedDraw, beforeSharedBlock, sharedStrengthPenalty } from "./cards/shared-hooks.js"; +import { hasCardAffix } from "./cards/intrinsic-affixes.js"; import { createBattleEnergy } from "./cards/battle-energy.js"; import { createRandomHandExhauster } from "./cards/random-hand-exhaust.js"; import { createBattleCardCopier } from "./cards/battle-card-copy.js"; import { createBattleCardReclaimer } from "./cards/battle-card-reclaim.js"; -import { createTemporaryStrength, temporaryStrengthAmount, temporaryStrengthExpires, clearTemporaryStrength, forgetTemporaryStrength } from "./cards/temporary-strength.js"; +import { createTemporaryStrength, temporaryStrengthAmount, temporaryStrengthExpires, expireTemporaryStrength, clearTemporaryStrength, forgetTemporaryStrength } from "./cards/temporary-strength.js"; import { createRelicBattle, createRelicSkills } from "./relics/battle.js"; import { grantRelic, getRelic, initializeRelicMaxHp } from "./relics/definitions.js"; @@ -19,7 +29,7 @@ import { applyRoomRelics, restRecovery } from "./relics/rooms.js"; import { buildBattlePlan, consumeSupports, createBattleDirector, grantSupport } from "./battle/battle-director.js"; import { installPersonalPiles } from "./cards/personal-piles.js"; import { createMonster } from "./monsters/monster.js"; -import { prepareBondBattle } from "./bonds/state.js"; +import { prepareBondBattle, bondBattleStatus } from "./bonds/state.js"; import { initializeBondUnit, recordBondDeath } from "./bonds/battle.js"; import { bondDefinitions } from "./bonds/definitions.js"; import { createBondIntentActions } from "./bonds/intents.js"; @@ -36,6 +46,9 @@ import { isMawler, recordMawlerAction } from "./monsters/mawler-intent.js"; import { isVine, recordVineAction } from "./monsters/vine-intent.js"; import { isCubex, recordCubexAction, initializeCubex } from "./monsters/cubex-intent.js"; import { isEffigy, recordEffigyAction, initializeEffigy } from "./monsters/effigy-intent.js"; +import { isVantom, recordVantomAction, initializeVantom } from "./monsters/vantom-intent.js"; +import { isBeast, initializeBeast, recoverBeastStun, canBreakPlow, breakPlow, ringingLimited, countRingingCard, clearRinging } from "./monsters/beast-intent.js"; +import { KIN_FOLLOWER_CHARACTER, isKinActor, isKinFollower, canActKin, recordKinAction, initializeKin, rollKinFollowerHp, clearKinOwnerIntents } from "./monsters/kin-intent.js"; import { slowPercent, canCountSlowCard, recordSlowCard, resetSlow, slowApplies, slowAttackDamage } from "./monsters/effigy-slow.js"; import { BYRDONIS_CHARACTER, isByrdonis, rollByrdonisHp, recordByrdonisAction, initializeByrdonis, resolveTerritorial } from "./monsters/byrdonis-intent.js"; import { NIBBIT_CHARACTER, isNibbit, rollNibbitHp, recordNibbitAction, bindNibbitOpenings } from "./monsters/nibbit-intent.js"; @@ -73,7 +86,7 @@ import { recordFlyconidAction } from "./monsters/flyconid-intent.js"; import { recordRaiderAction } from "./monsters/raider-intent.js"; import { PLAYER_ENERGY, PLAYER_HAND_LIMIT, cardCost, - canPayCard, payCard, isActiveCardUse, + canPayCard, payCard, isActiveCardUse, isXCostCard, } from "./battle/combat-rules.js"; import { mountBattlePiles } from "./ui/card-library.js"; import { mountGMManager } from "./ui/gm-manager.js"; @@ -145,26 +158,21 @@ const chooseCharacter = async (backLabel = "返回存档选择") => { name: get.translation(name), description: lib.translate[`${name}_title`] || "作为本次征程的角色", })); - return chooseButtons("选择出征武将", [...(choices.length ? choices : [{ id: "mengsan_liubei_shuying", name: "界刘备" }]), { id: "back", name: backLabel, description: "不会修改现有征程" }], "此武将将陪伴你完成本次征程。", { back: "back", eyebrow: "出征准备" }); + return chooseButtons("选择出征武将", [...(choices.length ? choices : [{ id: "mengsan_liubei_shuying", name: "刘备" }]), { id: "back", name: backLabel, description: "不会修改现有征程" }], "此武将将陪伴你完成本次征程。", { back: "back", eyebrow: "出征准备" }); }; -const chooseDefeatAction = async () => { - while (true) { - const action = await chooseButtons("战斗失败", [ - {id:"retry", name:"重新尝试", description:"恢复战前生命、牌组和征程状态,重新挑战当前战斗。"}, - {id:"new", name:"新开征程", danger:true, description:"选择出征武将,确认选择后替换本次征程。"}, - ], "本次失败不会发放战斗奖励。请选择接下来要做什么。", {eyebrow:"败而不馁"}); - if (action === "retry") return "retry"; - if (action !== "new") throw new Error("未知的失败流程选择"); - const character = await chooseCharacter("返回失败选择"); - if (character !== "back") return {action:"new", character}; - } -}; +const chooseDefeatAction = async run => chooseButtons("角色阵亡", [ + {id:"restart", name:"重新开始", description:`以当前角色${get.translation(run.player.character)}从头开始新征程。`}, + {id:"menu", name:"返回主菜单", description:"结束本次征程,清除当前存档并返回模式选择。"}, +], "选择前保留上一次地图存档,不自动处理。本次阵亡不发放战斗奖励。", { + eyebrow:"征程结束", menu:false, +}); const createCardInstance = (run, name = "sha", upgrade = 0, affixes = []) => createRandomCardData(run, name, { random: () => nextRandom(run), upgrade, affixes }); const applyReward = (run, rewardId) => { + rewardId = resolveRetiredCardReward(rewardId); const player = run.player; const relicId = config.rewards[rewardId]?.relic; if (relicId) { @@ -214,7 +222,9 @@ const applyReward = (run, rewardId) => { }; const chooseReward = async (run, rewardConfig = {}) => { - const choices = getRandomRewardChoices(run, rewardConfig.rewardPool).filter(choice => choice.name); + const choices = getRandomRewardChoices(run, rewardConfig.rewardPool, 3, { + allowSharedCards: rewardConfig.allowSharedCardRewards === true, + }).filter(choice => choice.name); const title = rewardConfig.rewardTitle || (rewardConfig.boss ? "极品奖励(三选一)" : "战斗奖励(三选一)"); const rewardId = await chooseButtons(title, choices, rewardConfig.description); const reward = choices.find(choice => choice.id == rewardId); @@ -355,6 +365,8 @@ const initBattleUnit = (player, spec) => { { ...spec, hp: rollShrinkerHp(() => nextRandom(_status.mengsanRun_shuying)) } : spec.character === PHROG_CHARACTER ? { ...spec, hp: rollPhrogHp(() => nextRandom(_status.mengsanRun_shuying)) } : + spec.character === KIN_FOLLOWER_CHARACTER ? + { ...spec, hp: rollKinFollowerHp(() => nextRandom(_status.mengsanRun_shuying)) } : spec.character === WRIGGLER_CHARACTER ? { ...spec, hp: rollWrigglerHp(() => nextRandom(_status.mengsanRun_shuying)) } : spec.character === BYRDONIS_CHARACTER ? @@ -379,6 +391,9 @@ const initBattleUnit = (player, spec) => { initializeCubex(player, activeBattle); initializeByrdonis(player, activeBattle); initializeEffigy(player, activeBattle); + initializeVantom(player, activeBattle); + initializeBeast(player, activeBattle); + initializeKin(player, spec, activeBattle); initializePhrog(player, spec, activeBattle); if (isInklet(player)) { const position = spec.inkletPosition ?? 1; @@ -417,6 +432,9 @@ const { executeVineIntent, executeCubexIntent, executeByrdonisIntent, + executeVantomIntent, + executeBeastIntent, + executeKinIntent, executeEffigyIntent, executePhrogIntent, executeNibbitIntent, @@ -504,10 +522,18 @@ const prepareBattle = async (run, node, encounter, session, resources) => { const phrogCount = scheduledUnits.filter(spec => spec.character === PHROG_CHARACTER).length; if (scheduledUnits.length + fogmogCount + phrogCount * INFESTED_COUNT > 7) throw new Error("召唤需要预留空席位;请减少本场支援数量"); if (run.bondBattle) { - game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name},${ - bondUnit ? "本场将到场" : "本场未到场"}`); + game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name},${bondBattleStatus(run.bondBattle)}`); + } + if (encounter.encounterPool === "weak" || encounter.encounterPool === "strong") { + game.log(`普通战斗第${encounter.normalBattleNumber}场:${encounter.encounterPool === "weak" ? "弱怪池" : "强怪池"},${encounter.name}`); } for (const spec of [...plan.units, ...plan.rules.flatMap(r => r.effects.filter(e => e.type === "spawn").map(e => e.unit))]) { + // Bond NPCs may belong to a disabled character pack. Resolve them the + // same way as native Player.init, without enabling the whole pack. + if (spec.bondId && !lib.character[spec.character]) { + const character = get.character(spec.character); + if (character && !character.isNull) lib.character[spec.character] = character; + } if (!lib.character[spec.character]) throw new Error("关卡武将未加载:" + spec.character); for (const skill of spec.skills || []) if (!lib.skill[skill]) throw new Error("关卡技能未加载:" + skill); const slots = new Set(); @@ -560,6 +586,7 @@ const prepareBattle = async (run, node, encounter, session, resources) => { const monsters = plan.units.map((spec, index) => initBattleUnit(participants[index], spec)); bindNibbitOpenings(participants); plan.units.forEach((spec, index) => installMonsterPile(participants[index], monsters[index], currentBattle)); + if (run.bondBattle?.arrived) game.log(`羁绊助战:${bondDefinitions[run.bondBattle.id].name}已到场,使用独立牌堆`); participants.forEach((player, index) => { if (monsters[index]) currentBattle.intentUI.set(player, mountEnemyIntent(player, session, STYLE_PATH, document, @@ -590,6 +617,7 @@ const prepareBattle = async (run, node, encounter, session, resources) => { run.player.maxHp = me.maxHp; run.player.hp = me.hp; } + delete run.sharedBattleCardEffects; initializeRelicMaxHp(run); me.maxHp = run.player.maxHp; me.hp = Math.max(1, Math.min(run.player.hp, me.maxHp)); @@ -608,12 +636,12 @@ const prepareBattle = async (run, node, encounter, session, resources) => { if (lib.skill[skill] && !me.hasSkill(skill)) me.addSkill(skill); }); for (const skill of [ - "mengsan_draw_shuying", "mengsan_card_use_shuying", + "mengsan_draw_shuying", "mengsan_card_use_shuying", "mengsan_ic_bridge_shuying", "mengsan_shared_bridge_shuying", "mengsan_card_affixes_shuying", "mengsan_scenario_shuying", "mengsan_card_payment_shuying", "mengsan_monster_draw_shuying", "mengsan_flyconid_action_shuying", "mengsan_raider_action_shuying", "mengsan_fogmog_action_shuying", "mengsan_mawler_action_shuying", - "mengsan_phrog_action_shuying", "mengsan_wriggler_spawned_shuying", "mengsan_infection_damage_shuying", "mengsan_effigy_action_shuying", "mengsan_byrdonis_action_shuying", "mengsan_cubex_action_shuying", "mengsan_vine_action_shuying", "mengsan_nibbit_action_shuying", + "mengsan_phrog_action_shuying", "mengsan_wriggler_spawned_shuying", "mengsan_infection_damage_shuying", "mengsan_beast_action_shuying", "mengsan_kin_action_shuying", "mengsan_kin_orphan_shuying", "mengsan_vantom_action_shuying", "mengsan_effigy_action_shuying", "mengsan_byrdonis_action_shuying", "mengsan_cubex_action_shuying", "mengsan_vine_action_shuying", "mengsan_nibbit_action_shuying", "mengsan_shrinker_action_shuying", "mengsan_twigmedium_action_shuying", "mengsan_twigslime_action_shuying", @@ -665,8 +693,8 @@ const prepareBattle = async (run, node, encounter, session, resources) => { currentBattle.personalPiles.trimOpeningHand( 4 + currentBattle.relics.openingHandBonus); } - if ((isRaider(player) || player.storage.mengsanBond_shuying) && - player.hujia > 0) await player.changeHujia(-player.hujia); + if (currentBattle.players.has(player) && player.hujia > 0 && !ironcladPreservesBlock(player)) + await player.changeHujia(-player.hujia); if (player.storage.mengsanMaxEnergy_shuying != null) { player.storage.mengsanEnergy_shuying = player.storage.mengsanMaxEnergy_shuying; currentBattle.energyUI.get(player)?.(); @@ -732,12 +760,21 @@ const makeFlow = (releaseSkills = async () => {}) => createBattleFlow({ }), chooseDefeat:chooseDefeatAction, showMap, - showEnding:async route => { game.over(route === "victory"); }, + showEnding:async (route, run) => { + if (route === "menu") { await openModeSelection(); return; } + if (route === "victory") { + await playDialogue(config.acts[run?.actIndex]?.completionDialogue, + { run, title: "未完待续" }); + } + game.over(route === "victory"); + }, quiesce:() => quiesceEngine(_status), }); const requestBattleFinish = outcome => { const current = activeBattle; if (!current) return false; + // Complete the original card and all queued replays before snapshotting a victory. + if(outcome==="victory" && current.sharedPendingUses?.size){current.sharedDeferredVictory=true;return false;} for (const player of game.dead) { recordBondDeath(player, _status.mengsanRun_shuying, message => game.log(message)); @@ -782,6 +819,7 @@ const playBattle = async (run, node, encounter) => { current = {session, flow, finished, resources, players: new Set(), monsterPiles: new Map(), energyUI: new Map(), intentUI: new Map()}; activeBattle = current; const root = new lib.element.GameEvent("mengsanBattle", false, _status.eventManager); + current.root = root; const previousRoot = _status.eventManager.rootEvent; session.ownResource({}, () => { if (_status.eventManager.rootEvent === root) _status.eventManager.rootEvent = previousRoot; @@ -793,10 +831,13 @@ const playBattle = async (run, node, encounter) => { }}); await signal; // External controller, never an engine event content await. if (current.engineError) { + reportFlowError(current.engineError); session.requestStop(); current.handUI?.dispose(); document.body.classList.remove("mengsan-battle-ui-shuying"); - await chooseButtons("战斗流程异常", [{id:"exit",name:"保留存档并返回模式选择"}], "无法确认旧事件已退出,已停止继续开战。上次成功存档仍保留。"); + const reason = current.engineError?.message || String(current.engineError); + await chooseButtons("战斗流程异常", [{id:"exit",name:"保留存档并返回模式选择"}], + `错误原因:${reason}。已停止继续开战,上次成功存档仍保留。`); await openModeSelection(); return; } const result = await awaitSettlement(flow); @@ -804,20 +845,10 @@ const playBattle = async (run, node, encounter) => { return result; }; const setupBattle = async (run, node, encounter = getNodeEncounter(run, node)) => { - // Retry iteratively: repeated defeats must not grow a recursive battle promise chain. - while (true) { - const result = await playBattle(run, node, encounter); - if (!result) return; - if (result.route === "retry") { - run = result.run; - node = result.retryNode; - encounter = result.retryEncounter || getNodeEncounter(run, node); - continue; - } - if (result.route === "map") await routeRun(result.run, result.nextNode, true); - else if (result.route === "new") await routeRun(result.run); - return; - } + const result = await playBattle(run, node, encounter); + if (!result) return; + if (result.route === "map") await routeRun(result.run, result.nextNode, true); + else if (result.route === "new") await routeRun(result.run); }; const routeRun = async (run, selected = null, supplied = false) => { while (true) { @@ -837,7 +868,7 @@ const routeRun = async (run, selected = null, supplied = false) => { }; const startJourney = async () => { lib.translate.restart = "返回"; - let run = copy(lib.storage[config.saveKey] || null); + let run = modeStorage.read()[config.saveKey] || null; while (true) { const action = await chooseRun(run); if (action === "continue") break; @@ -854,20 +885,6 @@ const startJourney = async () => { await saveRun(run); break; } - if (run.battleFlow?.pending) { - const flow = makeFlow(); - const result = await awaitSettlement(flow, flow.resumePending({session:createBattleSession(`recovery-${++battleSequence}`)})); - if (result.route === "map") await routeRun(result.run, result.nextNode, true); - else if (result.route === "retry") await setupBattle(result.run, result.retryNode, - result.retryEncounter || getNodeEncounter(result.run, result.retryNode)); - else if (result.route === "new") await routeRun(result.run); - return; - } - if (run.battleFlow?.retry) { - const retry = run.battleFlow.retry; - await setupBattle(run, retry.node, retry.encounter || getNodeEncounter(run, retry.node)); - return; - } const canRefreshOldMap = config.characters.includes(run.player.character) && !run.map?.completedNodeIds?.length && run.map?.layoutVersion != config.mapLayoutVersion; if (!run.map || run.map.actIndex != run.actIndex || canRefreshOldMap) { run.map = generateActMap(run, run.actIndex); @@ -881,19 +898,28 @@ const createMode = identityMode => { const identityPlayer = identityElement.player || {}; const nativeChangeHujia = identityPlayer.changeHujia || lib.element.Player.prototype.changeHujia; const nativeChangeHp = identityPlayer.changeHp || lib.element.Player.prototype.changeHp; - const modeCards = createMengsanCards({ + const cardContext = { + game, get, lib, random:nextRandom, status:_status, getBattle:()=>activeBattle, copyToDiscard: createBattleCardCopier(game, () => activeBattle), reclaimFromDiscard: createBattleCardReclaimer(game, () => activeBattle, get), temporaryStrength: createTemporaryStrength(game, () => activeBattle), countStrikeCards: createStrikeCounter(game, () => activeBattle, lib), blockDraw: createBlockDrawer(game, () => activeBattle), + blockUpgrade: createHandUpgrader(game, () => activeBattle, get), + lifeLossBlock: createLifeLossBlocker(() => activeBattle), + whirlwind: createWhirlwind(game, () => activeBattle), + spite: createSpite(game, () => activeBattle), + rampage: createRampage(game, () => activeBattle), playDrawTop: createHavocPlayer(game, () => activeBattle, _status), handExhaust: createHandExhauster(game, () => activeBattle, get, createRandomHandExhauster(game, () => activeBattle, () => _status.mengsanRun_shuying, nextRandom)), battleEnergy: createBattleEnergy(game, () => activeBattle), randomHandExhaust: createRandomHandExhauster(game, () => activeBattle, () => _status.mengsanRun_shuying, nextRandom), isBattleActive: () => Boolean(activeBattle?.session.active), - }); + }; + const cardPacks = createCardPackRuntime(cardContext); + const modeCards = createMengsanCards({cardPacks,isBattleActive:cardContext.isBattleActive}); + return { ...identityMode, name: MODE_ID, @@ -911,6 +937,9 @@ const createMode = identityMode => { player: { ...identityPlayer, async dieAfter() { + // 原生死亡已成立;在战斗结算关闭会话前落实斩杀的永久收益。 + if(this.isDead())await cardPacks.onDeath(_status.event); + clearKinOwnerIntents(game, activeBattle, this); clearConstrictSource(activeBattle, this); clearShrinkSource(activeBattle, this); activeBattle?.fogmog?.onDeath(this); @@ -930,6 +959,7 @@ const createMode = identityMode => { return next; }, changeHujia(num, type) { + if(activeBattle?.session.active && type!=="damage")num=beforeIroncladBlock(this,beforeSharedBlock(this,num??1,_status.event),_status.event); // 脆弱只削减正向获得的护甲,不改变受伤时消耗护甲的数值。 if (this.storage?.mengsanFrail_shuying > 0 && (num == null || num > 0) && type !== "damage") { num = Math.floor((num ?? 1) * 0.75); @@ -979,6 +1009,9 @@ const createMode = identityMode => { const cubex = isCubex(player); const byrdonis = isByrdonis(player); const effigy = isEffigy(player); + const vantom = isVantom(player); + const beast = isBeast(player); + const kin = isKinActor(player); const phrog = isPhrogActor(player); const nibbit = isNibbit(player); const shrinker = isShrinker(player); @@ -1009,7 +1042,10 @@ const createMode = identityMode => { player.storage.mengsanCubexIntent_shuying : byrdonis ? player.storage.mengsanByrdonisIntent_shuying : effigy ? player.storage.mengsanEffigyIntent_shuying : phrog ? - player.storage.mengsanPhrogIntent_shuying : null); + player.storage.mengsanPhrogIntent_shuying : vantom ? + player.storage.mengsanVantomIntent_shuying : kin ? + player.storage.mengsanKinIntent_shuying : beast ? + player.storage.mengsanBeastIntent_shuying : null); const restore = recover || (flyconid ? (choice, original) => { const state = @@ -1126,7 +1162,19 @@ const createMode = identityMode => { const next = choice === "retry" ? original : null; player.storage.mengsanEffigyIntent_shuying = next; return next; - } : null); + } : vantom ? (choice, original) => { + player.storage.mengsanVantomState_shuying = + recordVantomAction(player.storage.mengsanVantomState_shuying || {}, choice !== "retry"); + const next = choice === "retry" ? original : null; + player.storage.mengsanVantomIntent_shuying = next; + return next; + } : kin ? (choice, original) => { + player.storage.mengsanKinState_shuying = + recordKinAction(player.storage.mengsanKinState_shuying || {}, choice !== "retry"); + const next = choice === "retry" ? original : null; + player.storage.mengsanKinIntent_shuying = next; + return next; + } : beast ? (choice, original) => recoverBeastStun(player, choice, original) : null); const onRecover = async (choice, original) => { const next = await restore(choice, original); update(next === undefined ? @@ -1142,6 +1190,8 @@ const createMode = identityMode => { return true; }, async mengsanPersistRun_shuying(run) { + // 战斗中(包括阵亡等待选择期间)的永久成长先留在内存,过关才提交。 + if (activeBattle && activeBattle.session.state !== "disposed") return; await saveRun(run); }, async mengsanClearRun_shuying() { @@ -1153,6 +1203,7 @@ const createMode = identityMode => { }, async mengsanDraw_shuying(player, number = 1) { if (!player?.storage?.mengsanPlayer_shuying || number <= 0) return []; + if(ironcladBlocksDraw(player))return []; const battle = _status.mengsanBattle_shuying; const run = _status.mengsanRun_shuying; if (!battle || battle.drawPile.length + battle.discardPile.length < number) { @@ -1164,6 +1215,8 @@ const createMode = identityMode => { player.directgain(result); player.$draw(result.length); game.log(player, "从个人牌堆摸了", get.cnNumber(result.length), "张牌"); + await afterSharedDraw(player,result); + await afterIroncladDraw(player,result); return result; }, mengsanFinishBattle_shuying() { @@ -1172,6 +1225,7 @@ const createMode = identityMode => { checkResult() { const current = activeBattle; if (!current?.session.active || !current.director?.ready || current.director.busy || current.phrog?.pending || current.resultCheckQueued) return; + if(current.sharedPendingUses?.size && game.me?.isAlive())return; current.resultCheckQueued = true; const next = game.createEvent("mengsanScenarioResult", false); next.setContent(async () => { @@ -1190,21 +1244,24 @@ const createMode = identityMode => { }, skill: { ...(identityMode.skill || {}), + ...cardPacks.skills, ...createRelicSkills(() => activeBattle, () => game.me), ...createRelicCombatSkills(() => activeBattle, () => game.me), mengsan_taoyuan_bond_shuying: { locked: true, mark: true, marktext: "义", - intro: { content: "先天结识关羽、张飞,初始羁绊均为8级。助战角色在行军菜单中选择。" }, + intro: { content: "先天结识关羽、张飞,初始羁绊均为8级,默认选择关羽助战。可在行军菜单中更换。" }, }, mengsan_card_payment_shuying: { forced: true, silent: true, popup: false, firstDo: true, priority: 10000, trigger: { global: "useCard0" }, filter(event) { return event.player?.storage?.mengsanMaxEnergy_shuying != null && - lib.card[event.card?.name]?.mengsanCost_shuying != null && isActiveCardUse(event, event.player); }, + lib.card[event.card?.name]?.mengsanCost_shuying != null && + (isActiveCardUse(event, event.player) || + isXCostCard(event.card) && event.name === "useCard"); }, async content(event, trigger) { if (trigger.mengsanPaid_shuying) return; - if (!payCard(trigger.player, trigger.card, trigger)) { + if (!payCard(trigger.player, trigger.card, trigger, activeBattle)) { trigger.cancel(); game.log(trigger.player, "费用不足,不能使用", trigger.card); return; @@ -1221,7 +1278,8 @@ const createMode = identityMode => { player.storage?.mengsanBond_shuying); }, async content(event, trigger, player) { - trigger.num = isStunned(player) || isToothedEye(player) ? 0 : + trigger.num = isStunned(player) || isToothedEye(player) || + isKinFollower(player) && !canActKin(game, activeBattle, player) ? 0 : (player.storage.mengsanMonster_shuying || player.storage.mengsanBond_shuying).draw; }, @@ -1256,7 +1314,7 @@ const createMode = identityMode => { intro: { content(storage, player) { return `滑溜${player.storage.mengsanSlippery_shuying || 0}:下一次实际生命损失限制为1,完全格挡不消耗`; } }, trigger: { player: "changeHp" }, forced: true, silent: true, popup: false, - filter(event, player) { return Boolean(activeBattle?.session.active && isInklet(player) && + filter(event, player) { return Boolean(activeBattle?.session.active && (isInklet(player) || isVantom(player)) && player.storage.mengsanSlippery_shuying > 0 && event.num < 0 && event.changedHp < 0); }, async content(event, trigger, player) { player.storage.mengsanSlippery_shuying = Math.max(0, player.storage.mengsanSlippery_shuying - 1); @@ -1352,6 +1410,41 @@ const createMode = identityMode => { event.player.storage.mengsanByrdonisIntent_shuying); }, async content(event, trigger) { await executeByrdonisIntent(trigger.player); }, }, + mengsan_vantom_action_shuying: { + trigger: { global: "phaseUseBefore" }, + forced: true, silent: true, popup: false, priority: 100, + filter(event) { return Boolean(activeBattle?.session.active && + event.player?.isAlive() && !isStunned(event.player) && isVantom(event.player) && + event.player.storage.mengsanVantomIntent_shuying); }, + async content(event, trigger) { await executeVantomIntent(trigger.player); }, + }, + mengsan_beast_action_shuying: { + trigger: { global: "phaseUseBefore" }, + forced: true, silent: true, popup: false, priority: 100, + filter(event) { return Boolean(activeBattle?.session.active && + event.player?.isAlive() && !isStunned(event.player) && isBeast(event.player) && + event.player.storage.mengsanBeastIntent_shuying); }, + async content(event, trigger) { await executeBeastIntent(trigger.player); }, + }, + mengsan_kin_action_shuying: { + trigger: { global: "phaseUseBefore" }, + forced: true, silent: true, popup: false, priority: 100, + filter(event) { return Boolean(activeBattle?.session.active && + event.player?.isAlive() && !isStunned(event.player) && isKinActor(event.player) && + event.player.storage.mengsanKinIntent_shuying); }, + async content(event, trigger) { await executeKinIntent(trigger.player, trigger); }, + }, + mengsan_kin_orphan_shuying: { + trigger: { global: "phaseBefore" }, + forced: true, silent: true, popup: false, priority: 1000, + filter(event) { return Boolean(activeBattle?.session.active && event.player?.isAlive() && + isKinFollower(event.player) && !canActKin(game, activeBattle, event.player)); }, + async content(event, trigger) { trigger.cancel(); }, + }, + mengsan_kin_minion_shuying: { + mark: true, marktext: "爪", + intro: { content: "爪牙:击败所属同族神官即可获胜,不需要击败信徒;神官死亡后不再行动。" }, + }, mengsan_phrog_action_shuying: { trigger: { global: "phaseUseBefore" }, forced: true, silent: true, popup: false, priority: 100, @@ -1414,7 +1507,7 @@ const createMode = identityMode => { trigger: { source: "damageBegin1", global: ["phaseAfter", "dieAfter"] }, forced: true, silent: true, popup: false, priority: 85, forceDie: true, forceOut: true, mark: true, marktext: "力", - intro: { content(storage, player) { return `本回合攻击伤害增加${player.storage.mengsanSetupStrength_shuying || 0}点`; } }, + intro: { content(storage, player) { return `临时力量:攻击伤害增加${player.storage.mengsanSetupStrength_shuying || 0}点`; } }, onremove(player) { forgetTemporaryStrength(player); }, filter(event, player) { if (event.name === "damage") { @@ -1429,19 +1522,47 @@ const createMode = identityMode => { if (event.triggername === "damageBegin1") { trigger.num += temporaryStrengthAmount(player, activeBattle); trigger.mengsanSetupStrengthApplied_shuying = true; - } else clearTemporaryStrength(player); + } else if(trigger.name==="phase")expireTemporaryStrength(player,trigger); + else clearTemporaryStrength(player); }, }, mengsan_raider_card_strength_shuying: { trigger: { source: "damageBegin1" }, forced: true, silent: true, popup: false, priority: 90, filter(event, player) { return Boolean(activeBattle?.session.active && - (isRaider(player) || isFogmog(player) || isCrawler(player) || isInklet(player) || isJaxfruit(player) || isStrangler(player) || isLeafslime(player) || isLeafslimeMedium(player) || isTwigslime(player) || isTwigmedium(player) || isShrinker(player) || isVine(player) || isNibbit(player) || isCubex(player) || isByrdonis(player) || isEffigy(player) || isPhrogActor(player) || player.storage.mengsanBond_shuying) && - player.storage.mengsanStrength_shuying > 0 && + (isRaider(player) || isFogmog(player) || isCrawler(player) || isInklet(player) || isJaxfruit(player) || isStrangler(player) || isLeafslime(player) || isLeafslimeMedium(player) || isTwigslime(player) || isTwigmedium(player) || isShrinker(player) || isVine(player) || isNibbit(player) || isCubex(player) || isByrdonis(player) || isEffigy(player) || isPhrogActor(player) || isVantom(player) || isKinActor(player) || isBeast(player) && !isStunned(player) || player.storage.mengsanBond_shuying) && + (player.storage.mengsanStrength_shuying > 0 || sharedStrengthPenalty(player)>0) && event.card && !event.mengsanScriptedSkill_shuying && !event.mengsanCardStrengthApplied_shuying); }, async content(event, trigger, player) { - trigger.num += player.storage.mengsanStrength_shuying; + trigger.num = Math.max(0,trigger.num+(player.storage.mengsanStrength_shuying||0)-sharedStrengthPenalty(player)); + }, + }, + mengsan_plow_shuying: { + mark: true, marktext: "撞", + intro: { content(storage, player) { return `横冲直撞${player.storage.mengsanPlow_shuying || 0}层:受到伤害后生命不高于此值,击晕、清零力量并进入第二阶段。`; } }, + onremove(player) { delete player.storage.mengsanPlow_shuying; }, + trigger: { player: "damageEnd" }, forced: true, silent: true, popup: false, priority: 1000, + filter(event, player) { return canBreakPlow(player, activeBattle, event); }, + async content(event, trigger, player) { breakPlow(game, activeBattle, player, trigger); }, + }, + mengsan_ringing_shuying: { + mark: true, marktext: "眩", + intro: { content: "昏眩:本次自身回合至多主动使用1张牌,回合结束解除;不限制响应。不可叠加。" }, + onremove(player) { clearRinging(player); }, + mod: { + cardEnabled(card, player) { if (activeBattle?.session.active && ringingLimited(player, _status.event)) return false; }, + cardEnabled2(card, player) { if (activeBattle?.session.active && ringingLimited(player, _status.event)) return false; }, + }, + trigger: { player: ["useCard0", "useCard1", "phaseBefore", "phaseAfter", "dieAfter"] }, + forced: true, silent: true, popup: false, firstDo: true, priority: 11000, + filter(event, player) { return Boolean(activeBattle?.session.active && player.storage.mengsanRinging_shuying && + (event.name !== "useCard" || isActiveCardUse(event, player))); }, + async content(event, trigger, player) { + if (event.triggername === "useCard0") { if (ringingLimited(player, trigger)) trigger.cancel(); } + else if (event.triggername === "useCard1") countRingingCard(player, activeBattle, trigger); + else if (event.triggername === "phaseBefore") player.storage.mengsanRingingUsed_shuying = 0; + else { player.removeSkill("mengsan_ringing_shuying"); activeBattle?.handUI?.refresh(); } }, }, mengsan_raider_strength_shuying: { @@ -1587,7 +1708,7 @@ const createMode = identityMode => { }, async content(event, trigger, player) { if (event.triggername === "damageBegin1") { - trigger.num = Math.ceil(trigger.num * 1.5); + trigger.num = Math.ceil(trigger.num * (1.5 + cardPacks.vulnerableBonus(trigger.source, player))); return; } const remaining = Math.max(0, (player.storage.mengsanVulnerable_shuying || 0) - 1); @@ -1613,12 +1734,13 @@ const createMode = identityMode => { silent: true, priority: 10000, filter(event, player) { - return Boolean(player.storage?.mengsanPlayer_shuying && event.num > 0); + return Boolean(event.num > 0 && (player.storage?.mengsanPlayer_shuying || ironcladBlocksDraw(player))); }, async content(event, trigger, player) { const number = trigger.num; trigger.cancel(); - await game.mengsanDraw_shuying(player, number); + const cards=ironcladBlocksDraw(player)?[]:await game.mengsanDraw_shuying(player, number); + trigger.result={bool:true,cards};trigger.mengsanIcDrawHandled_shuying=true; }, }, mengsan_card_use_shuying: { @@ -1664,12 +1786,12 @@ const createMode = identityMode => { trigger: { global: ["phaseDiscardBegin", "cardsDiscardAfter"] }, filter(event) { if (!activeBattle?.session.active) return false; - if (event.name === "phaseDiscard") return event.player?.getCards("h").some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void")); + if (event.name === "phaseDiscard") return event.player?.getCards("h").some(card => hasCardAffix(card,"void")); return event.cards?.some(card => card.storage?.mengsanCard_shuying?.affixes?.includes("ingenious")); }, async content(event, trigger) { if (event.triggername === "phaseDiscardBegin") { - const cards = trigger.player.getCards("h").filter(card => card.storage?.mengsanCard_shuying?.affixes?.includes("void")); + const cards = trigger.player.getCards("h").filter(card => hasCardAffix(card,"void")); if (cards.length) { await trigger.player.lose(cards, ui.special); for (const card of cards) card.remove(); @@ -1690,8 +1812,8 @@ const createMode = identityMode => { } }, mod: { - ignoredHandcard(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("retain")) return true; }, - cardEnabled2(card) { if (card.storage?.mengsanCard_shuying?.affixes?.includes("unplayable")) return false; }, + ignoredHandcard(card) { if (hasCardAffix(card,"retain")) return true; }, + cardEnabled2(card) { if (hasCardAffix(card,"unplayable")) return false; }, }, }, mengsan_yingyong_shuying: { @@ -1723,6 +1845,13 @@ const createMode = identityMode => { mengsan_cubex_action_shuying: "立柱构造体行动", mengsan_byrdonis_action_shuying: "多尼斯异鸟行动", mengsan_effigy_action_shuying: "旧日雕像行动", + mengsan_vantom_action_shuying: "墨影幻灵行动", + mengsan_beast_action_shuying: "仪式兽行动", + mengsan_plow_shuying: "横冲直撞", + mengsan_ringing_shuying: "昏眩", + mengsan_kin_action_shuying: "同族行动", + mengsan_kin_orphan_shuying: "爪牙退场", + mengsan_kin_minion_shuying: "爪牙", mengsan_phrog_action_shuying: "异蛙寄生行动", mengsan_wriggler_spawned_shuying: "生成眩晕", mengsan_infection_damage_shuying: "感染结算", diff --git a/mengsan/monsters/actions.js b/mengsan/monsters/actions.js index ef1a448..a0b2753 100644 --- a/mengsan/monsters/actions.js +++ b/mengsan/monsters/actions.js @@ -11,6 +11,10 @@ import { isMawler, selectMawlerMove, recordMawlerAction } from "./mawler-intent. import { isVine, selectVineMove, recordVineAction } from "./vine-intent.js"; import { isCubex, selectCubexMove, recordCubexAction } from "./cubex-intent.js"; import { isByrdonis, selectByrdonisMove, recordByrdonisAction } from "./byrdonis-intent.js"; +import { isVantom, selectVantomMove, recordVantomAction } from "./vantom-intent.js"; +import { isBeast, selectBeastMove, recordBeastAction, gainPlow, applyRinging } from "./beast-intent.js"; +import { isKinActor, isKinPriest, canActKin, selectKinPriestMove, selectKinFollowerMove, recordKinAction } from "./kin-intent.js"; +import { applyWound } from "../cards/wound-card.js"; import { isEffigy, selectEffigyMove, recordEffigyAction } from "./effigy-intent.js"; import { isPhrog, isPhrogActor, selectPhrogMove, selectWrigglerMove, recordPhrogAction } from "./phrog-intent.js"; import { applyInfection } from "../cards/infection-card.js"; @@ -112,6 +116,21 @@ export function createMonsterIntentActions(game, getActiveBattle) { const move = (isPhrog(player) ? selectPhrogMove : selectWrigglerMove)(player.storage.mengsanPhrogState_shuying || {}); player.storage.mengsanPhrogIntent_shuying = move; game.mengsanSetEnemyIntent_shuying(player, move); + } else if (isKinActor(player)) { + if (!canActKin(game, getActiveBattle(), player) || player.storage.mengsanKinIntent_shuying) return; + const move = (isKinPriest(player) ? selectKinPriestMove : selectKinFollowerMove)(player.storage.mengsanKinState_shuying || {}); + player.storage.mengsanKinIntent_shuying = move; + game.mengsanSetEnemyIntent_shuying(player, move); + } else if (isBeast(player)) { + if (!getActiveBattle()?.session.active || player.storage.mengsanBeastIntent_shuying) return; + const move = selectBeastMove(player.storage.mengsanBeastState_shuying || {}); + player.storage.mengsanBeastIntent_shuying = move; + game.mengsanSetEnemyIntent_shuying(player, move); + } else if (isVantom(player)) { + if (!getActiveBattle()?.session.active || player.storage.mengsanVantomIntent_shuying) return; + const move = selectVantomMove(player.storage.mengsanVantomState_shuying || {}); + player.storage.mengsanVantomIntent_shuying = move; + game.mengsanSetEnemyIntent_shuying(player, move); } else if (isByrdonis(player)) { if (!getActiveBattle()?.session.active || player.storage.mengsanByrdonisIntent_shuying) return; const move = selectByrdonisMove(player.storage.mengsanByrdonisState_shuying || {}); @@ -412,6 +431,67 @@ export function createMonsterIntentActions(game, getActiveBattle) { await executeEffects(player, move); }; + const executeKinIntent = async (player, phase) => { + const current = getActiveBattle(); + if (!isKinActor(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return; + if (!canActKin(game, current, player)) { phase?.cancel(); return; } + const move = player.storage.mengsanKinIntent_shuying; + if (!move) return; + player.storage.mengsanKinIntent_shuying = null; + game.mengsanSetEnemyIntent_shuying(player, null); + player.storage.mengsanKinState_shuying = + recordKinAction(player.storage.mengsanKinState_shuying || {}); + if (player.storage.mengsanEnergy_shuying < 1) { + game.log(player, "费用不足,未发动", move.name); return; + } + player.storage.mengsanEnergy_shuying--; + current.energyUI.get(player)?.(); + game.log(player, "消耗1费用发动", move.name); + await createIntentExecutor(game, () => getActiveBattle() === current && canActKin(game, current, player) ? current : null)(player, move); + }; + + const executeBeastIntent = async player => { + const current = getActiveBattle(); + if (!isBeast(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return; + const move = player.storage.mengsanBeastIntent_shuying; + if (!move) return; + player.storage.mengsanBeastIntent_shuying = null; + game.mengsanSetEnemyIntent_shuying(player, null); + player.storage.mengsanBeastState_shuying = + recordBeastAction(player.storage.mengsanBeastState_shuying || {}); + if (player.storage.mengsanEnergy_shuying < 1) { + game.log(player, "费用不足,未发动", move.name); return; + } + player.storage.mengsanEnergy_shuying--; + current.energyUI.get(player)?.(); + game.log(player, "消耗1费用发动", move.name); + const revision = player.storage.mengsanBeastState_shuying.revision; + const valid = () => getActiveBattle() === current && current.session.active && player.isAlive() && + !isStunned(player) && player.storage.mengsanBeastState_shuying?.revision === revision; + if (move.plow) gainPlow(player); + else if (move.ringing) { for (const target of getIntentHostiles(game, player)) { if (!valid()) break; applyRinging(current, target); } } + else await createIntentExecutor(game, () => valid() ? current : null)(player, move); + }; + + const executeVantomIntent = async player => { + const current = getActiveBattle(); + if (!isVantom(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return; + const move = player.storage.mengsanVantomIntent_shuying; + if (!move) return; + player.storage.mengsanVantomIntent_shuying = null; + game.mengsanSetEnemyIntent_shuying(player, null); + player.storage.mengsanVantomState_shuying = + recordVantomAction(player.storage.mengsanVantomState_shuying || {}); + if (player.storage.mengsanEnergy_shuying < 1) { + game.log(player, "费用不足,未发动", move.name); return; + } + player.storage.mengsanEnergy_shuying--; + current.energyUI.get(player)?.(); + game.log(player, "消耗1费用发动", move.name); + await createIntentExecutor(game, () => getActiveBattle() === current ? current : null)(player, move); + if (getActiveBattle() === current && move.wound) applyWound(game, current, player, move.wound); + }; + const executeByrdonisIntent = async player => { const current = getActiveBattle(); if (!isByrdonis(player) || !player.isAlive() || isStunned(player) || !current?.session.active) return; @@ -508,6 +588,9 @@ export function createMonsterIntentActions(game, getActiveBattle) { executeVineIntent, executeCubexIntent, executeByrdonisIntent, + executeVantomIntent, + executeBeastIntent, + executeKinIntent, executeEffigyIntent, executePhrogIntent, executeNibbitIntent, diff --git a/mengsan/monsters/artifact-status.js b/mengsan/monsters/artifact-status.js index a88d34b..ff42483 100644 --- a/mengsan/monsters/artifact-status.js +++ b/mengsan/monsters/artifact-status.js @@ -1,3 +1,4 @@ +import { afterIroncladVulnerable } from "../cards/ironclad-hooks.js"; const KEY = "mengsanArtifact_shuying", SKILL = "mengsan_artifact_shuying"; const bindings = new WeakMap(); const DEBUFFS = Object.freeze({ @@ -29,11 +30,13 @@ export function consumeArtifact(player) { return true; } // 一次施加抵消一次,不按负面层数逐层消耗;伤害和正面增益不走此入口。 -export function applyMengsanDebuff(player, kind, amount) { +export function applyMengsanDebuff(player, kind, amount, source = null) { if (!Object.hasOwn(DEBUFFS, kind) || !Number.isSafeInteger(amount) || amount < 0) throw new RangeError("负面状态参数无效"); if (!amount || !player?.isAlive()) return false; const [key, skill] = DEBUFFS[kind], old = player.storage[key] ?? 0; if (!Number.isSafeInteger(old) || old < 0 || amount > Number.MAX_SAFE_INTEGER - old) throw new RangeError("负面状态层数越界"); if (consumeArtifact(player)) return false; - player.storage[key] = old + amount; player.addSkill(skill); player.markSkill(skill); return true; + player.storage[key] = old + amount; player.addSkill(skill); player.markSkill(skill); + if(kind === "vulnerable" && source)afterIroncladVulnerable(source,player); + return true; } diff --git a/mengsan/monsters/beast-intent.js b/mengsan/monsters/beast-intent.js new file mode 100644 index 0000000..f8ec8e0 --- /dev/null +++ b/mengsan/monsters/beast-intent.js @@ -0,0 +1,94 @@ +import { consumeArtifact } from "./artifact-status.js"; +import { isActiveCardUse } from "../battle/combat-rules.js"; +import { skipStunnedAction } from "../battle/stun-intent.js"; +export const BEAST_CHARACTER = "mengsan_ceremonial_beast_shuying"; +export const BEAST_MOVES = Object.freeze({ + stamp: Object.freeze({ id: "stamp", name: "跺地", plow: 150 }), + plow: Object.freeze({ id: "plow", name: "横冲直撞", damage: 18, strength: 2 }), + cry: Object.freeze({ id: "cry", name: "野兽咆哮", ringing: true }), + stomp: Object.freeze({ id: "stomp", name: "踩踏", damage: 15 }), + crush: Object.freeze({ id: "crush", name: "碾碎", damage: 17, strength: 3 }), +}); +export const isBeast = player => player?.name === BEAST_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy"; +function checked(state) { + const s = { phase: 1, step: 0, turnsTaken: 0, revision: 0, ...state }; + if (![1, 2].includes(s.phase) || !Number.isSafeInteger(s.step) || s.step < 0 || s.step > (s.phase === 1 ? 1 : 2) || + !Number.isSafeInteger(s.turnsTaken) || s.turnsTaken < 0 || !Number.isSafeInteger(s.revision) || s.revision < 0) throw new RangeError("仪式兽行动状态无效"); + return s; +} +export function selectBeastMove(state = {}) { + const s = checked(state); + return s.phase === 1 ? (s.step === 0 ? BEAST_MOVES.stamp : BEAST_MOVES.plow) : [BEAST_MOVES.cry, BEAST_MOVES.stomp, BEAST_MOVES.crush][s.step]; +} +export function recordBeastAction(state = {}, advance = true) { + const s = checked(state); + if (typeof advance !== "boolean" || advance && s.turnsTaken === Number.MAX_SAFE_INTEGER) throw new RangeError("仪式兽行动推进无效"); + return { ...s, step: advance ? (s.phase === 1 ? 1 : (s.step + 1) % 3) : s.step, turnsTaken: s.turnsTaken + (advance ? 1 : 0) }; +} +export function recoverBeastStun(player, choice, original) { + const s = checked(player.storage.mengsanBeastState_shuying); + if (s.justBroken) { player.storage.mengsanBeastState_shuying = { ...s, justBroken: false }; player.storage.mengsanBeastIntent_shuying = null; return null; } + player.storage.mengsanBeastState_shuying = recordBeastAction(s, choice !== "retry"); + const next = choice === "retry" ? original : null; + player.storage.mengsanBeastIntent_shuying = next; return next; +} +const bindings = new WeakMap(); +export function initializeBeast(player, current) { + if (!isBeast(player)) return false; + if (current?.session.active && bindings.get(player)?.battle !== current) { + const entry = { battle: current }; + current.session.ownResource(entry, () => { + if (bindings.get(player) !== entry) return; + bindings.delete(player); delete player.storage.mengsanBeastState_shuying; delete player.storage.mengsanBeastIntent_shuying; + player.removeSkill("mengsan_plow_shuying"); player.removeSkill("mengsan_raider_strength_shuying"); + }); bindings.set(player, entry); + } + player.storage.mengsanBeastState_shuying = checked({}); player.storage.mengsanBeastIntent_shuying = null; + player.removeSkill("mengsan_plow_shuying"); player.removeSkill("mengsan_raider_strength_shuying"); return true; +} +export function gainPlow(player) { + if (!isBeast(player) || !player.isAlive()) return false; + const max = player.maxHp; + if (!Number.isSafeInteger(max) || max <= 0 || !Number.isSafeInteger(max * 150)) throw new RangeError("仪式兽血量缩放无效"); + player.storage.mengsanPlow_shuying = Math.max(1, Math.round(150 * max / 252)); + player.addSkill("mengsan_plow_shuying"); player.markSkill("mengsan_plow_shuying"); return true; +} +export const canBreakPlow = (player, current, damage) => Boolean(current?.session.active && isBeast(player) && player.isAlive() && + damage?.name === "damage" && damage.player === player && damage.num > 0 && player.storage.mengsanPlow_shuying > 0 && + player.hp <= player.storage.mengsanPlow_shuying && player.storage.mengsanBeastState_shuying?.phase === 1); +export function breakPlow(game, current, player, damage) { + if (!canBreakPlow(player, current, damage)) return false; + const s = checked(player.storage.mengsanBeastState_shuying); + if (s.revision === Number.MAX_SAFE_INTEGER) throw new RangeError("仪式兽阶段版本溢出"); + player.removeSkill("mengsan_plow_shuying"); player.removeSkill("mengsan_raider_strength_shuying"); + player.storage.mengsanStrength_shuying = 0; + player.storage.mengsanBeastState_shuying = { ...s, phase: 2, step: 0, revision: s.revision + 1, justBroken: true }; + player.storage.mengsanBeastIntent_shuying = null; + game.mengsanStunEnemy_shuying(player, { resume: "retry" }); + const phaseUse = damage.getParent?.("phaseUse"); + if (phaseUse?.player === player) skipStunnedAction(player, phaseUse); + return true; +} +const ringing = new WeakMap(); +export function clearRinging(player) { + ringing.delete(player); delete player.storage.mengsanRinging_shuying; delete player.storage.mengsanRingingUsed_shuying; +} +export function applyRinging(current, player, phasePlayer = null) { + if (!current?.session.active || !player?.isAlive() || !current.players?.has(player)) return false; + if (consumeArtifact(player)) return false; + if (ringing.get(player)?.battle === current && player.storage.mengsanRinging_shuying) return true; + const entry = { battle: current, seen: new WeakSet() }; + current.session.ownResource(entry, () => { if (ringing.get(player) !== entry) return; player.removeSkill("mengsan_ringing_shuying"); clearRinging(player); }); + ringing.set(player, entry); player.storage.mengsanRinging_shuying = 1; + // Mid-turn applications use already played cards; ordinary Beast Cry arrives before the target's next turn. + player.storage.mengsanRingingUsed_shuying = phasePlayer === player ? (player.getHistory?.("useCard", e => !e.cancelled && !e._cancelled && isActiveCardUse(e, player)).length || 0) : 0; + player.addSkill("mengsan_ringing_shuying"); player.markSkill("mengsan_ringing_shuying"); current.handUI?.refresh(); return true; +} +export const ringingLimited = (player, event) => Boolean(player.storage?.mengsanRinging_shuying && + player.storage.mengsanRingingUsed_shuying >= 1 && isActiveCardUse(event, player)); +export function countRingingCard(player, current, event) { + const entry = ringing.get(player); + if (!current?.session.active || entry?.battle !== current || !player.isAlive() || !player.storage.mengsanRinging_shuying || + event?.name !== "useCard" || !event.card || event.cancelled || event._cancelled || !isActiveCardUse(event, player) || entry.seen.has(event)) return false; + entry.seen.add(event); player.storage.mengsanRingingUsed_shuying = 1; player.markSkill("mengsan_ringing_shuying"); current.handUI?.refresh(); return true; +} diff --git a/mengsan/monsters/fogmog-intent.js b/mengsan/monsters/fogmog-intent.js index e25d9f4..12e0e00 100644 --- a/mengsan/monsters/fogmog-intent.js +++ b/mengsan/monsters/fogmog-intent.js @@ -1,4 +1,5 @@ // 灰机 Wiki 雾菇页的普通难度:74生命,重击8、头槌14。 +import { isKinFollower } from "./kin-intent.js"; export const FOGMOG_CHARACTER = "mengsan_fogmog_shuying"; export const EYE_CHARACTER = "mengsan_eye_with_teeth_shuying"; export const FOGMOG_MOVES = Object.freeze({ @@ -32,4 +33,4 @@ export const isToothedEye = player => player?.name === EYE_CHARACTER && export const isFogmogActor = player => isFogmog(player) || isToothedEye(player); // 爪牙不单独构成胜利目标;雾菇死亡后无需继续杀死或等待其幻象。 export const isEncounterEnemy = player => player?.isAlive() && - player.storage?.mengsanCamp_shuying === "enemy" && !isToothedEye(player); + player.storage?.mengsanCamp_shuying === "enemy" && !isToothedEye(player) && !isKinFollower(player); diff --git a/mengsan/monsters/inklet-intent.js b/mengsan/monsters/inklet-intent.js index 75ce85b..7d912ec 100644 --- a/mengsan/monsters/inklet-intent.js +++ b/mengsan/monsters/inklet-intent.js @@ -1,3 +1,4 @@ +import { isVantom } from "./vantom-intent.js"; // 灰机 Wiki 墨宝:奇数位刺击起手;偶数位旋风起手后刺击;以后随机招与刺击交替。 export const INKLET_CHARACTER = "mengsan_inklet_shuying"; export const INKLET_MOVES = Object.freeze({ @@ -41,7 +42,7 @@ export const isInklet = player => player?.name === INKLET_CHARACTER && // 在梦三的原生changeHp事件创建后限幅;保留护甲消耗量,只将穿透后的生命损失限制为1。 // 层数由实际changeHp通知消耗,完全格挡、零伤害及取消事件不消耗。 export function capInkletHpLoss(player, event) { - if (!isInklet(player) || !(player.storage.mengsanSlippery_shuying > 0) || + if (!(isInklet(player) || isVantom(player)) || !(player.storage.mengsanSlippery_shuying > 0) || !Number.isFinite(event.num) || event.num >= 0) return event; const parent = event.getParent(); if (parent?.unreal) return event; diff --git a/mengsan/monsters/kin-intent.js b/mengsan/monsters/kin-intent.js new file mode 100644 index 0000000..63f320f --- /dev/null +++ b/mengsan/monsters/kin-intent.js @@ -0,0 +1,76 @@ +export const KIN_PRIEST_CHARACTER = "mengsan_kin_priest_shuying"; +export const KIN_FOLLOWER_CHARACTER = "mengsan_kin_follower_shuying"; +export const KIN_PRIEST_MOVES = Object.freeze({ + frailty: Object.freeze({ id: "frailty", name: "脆弱法球", damage: 8, debuff: "脆弱", stacks: 1 }), + weakness: Object.freeze({ id: "weakness", name: "虚弱法球", damage: 8, debuff: "虚弱", stacks: 1 }), + beam: Object.freeze({ id: "beam", name: "灵魂光束", damage: 3, hits: 3 }), + ritual: Object.freeze({ id: "ritual", name: "黑暗仪式", strength: 2 }), +}); +export const KIN_FOLLOWER_MOVES = Object.freeze({ + slash: Object.freeze({ id: "slash", name: "快斩", damage: 5 }), + boomerang: Object.freeze({ id: "boomerang", name: "回旋镖", damage: 2, hits: 2 }), + dance: Object.freeze({ id: "dance", name: "力量之舞", strength: 2 }), +}); +const priestCycle = Object.freeze(Object.values(KIN_PRIEST_MOVES)); +const followerCycle = Object.freeze(Object.values(KIN_FOLLOWER_MOVES)); +const turnsOf = state => { + const turns = state.turnsTaken ?? 0; + if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("同族小队行动记录无效"); + return turns; +}; +export const selectKinPriestMove = (state = {}) => priestCycle[turnsOf(state) % priestCycle.length]; +export const selectKinFollowerMove = (state = {}) => followerCycle[turnsOf(state) % followerCycle.length]; +export function recordKinAction(state = {}, advance = true) { + const turns = turnsOf(state); + if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("同族小队行动推进无效"); + return { turnsTaken: turns + (advance ? 1 : 0) }; +} +export function rollKinFollowerHp(random) { + const value = random(); + if (!Number.isFinite(value) || value < 0 || value >= 1) throw new RangeError("同族信徒随机数必须在[0,1)内"); + return 58 + Math.floor(value * 2); +} +export const isKinPriest = player => player?.name === KIN_PRIEST_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy"; +export const isKinFollower = player => player?.name === KIN_FOLLOWER_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy"; +export const isKinActor = player => isKinPriest(player) || isKinFollower(player); +export function canActKin(game, current, player) { + if (!current?.session.active || !player?.isAlive() || !isKinActor(player)) return false; + if (isKinPriest(player)) return true; + const owner = player.storage.mengsanKinOwner_shuying; + return Boolean(owner && game.players.some(p => p.isAlive() && isKinPriest(p) && p.storage.mengsanUnitId_shuying === owner)); +} +const bindings = new WeakMap(); +export function initializeKin(player, spec = {}, current = null) { + if (!isKinActor(player)) return false; + if (isKinFollower(player) && spec.kinOwner != null && + (typeof spec.kinOwner !== "string" || !/^[\w.-]+$/.test(spec.kinOwner))) throw new TypeError("同族信徒的神官ID无效"); + if (current?.session.active && bindings.get(player)?.battle !== current) { + const entry = { battle: current }; + current.session.ownResource(entry, () => { + if (bindings.get(player) !== entry) return; + bindings.delete(player); + delete player.storage.mengsanKinState_shuying; + delete player.storage.mengsanKinIntent_shuying; + delete player.storage.mengsanKinOwner_shuying; + player.removeSkill("mengsan_kin_minion_shuying"); + player.removeSkill("mengsan_raider_strength_shuying"); + }); + bindings.set(player, entry); + } + player.storage.mengsanKinState_shuying = { turnsTaken: 0 }; + player.storage.mengsanKinIntent_shuying = null; + if (isKinFollower(player)) { + player.storage.mengsanKinOwner_shuying = spec.kinOwner ?? null; + player.addSkill("mengsan_kin_minion_shuying"); + player.markSkill("mengsan_kin_minion_shuying"); + } + return true; +} +export function clearKinOwnerIntents(game, current, priest) { + if (!current?.session.active || !isKinPriest(priest)) return; + for (const player of game.players) { + if (!isKinFollower(player) || player.storage.mengsanKinOwner_shuying !== priest.storage.mengsanUnitId_shuying) continue; + player.storage.mengsanKinIntent_shuying = null; + game.mengsanSetEnemyIntent_shuying(player, null); + } +} diff --git a/mengsan/monsters/monster.js b/mengsan/monsters/monster.js index 2d01c61..f5e254c 100644 --- a/mengsan/monsters/monster.js +++ b/mengsan/monsters/monster.js @@ -23,6 +23,9 @@ const HEALTH = Object.freeze({ mengsan_cubex_construct_shuying: 65, mengsan_byrdonis_shuying: 82, mengsan_bygone_effigy_shuying: 127, + mengsan_vantom_shuying: 173, + mengsan_ceremonial_beast_shuying: 252, + mengsan_kin_priest_shuying: 190, mengsan_kin_follower_shuying: 59, mengsan_phrog_parasite_shuying: 62, mengsan_wriggler_shuying: 19, mengsan_shrinker_beetle_shuying: 39, @@ -137,6 +140,20 @@ export class PhrogParasite extends Monster { export class Wriggler extends Monster { constructor(options = {}) { super({ ...options, character: "mengsan_wriggler_shuying", tier: "elite", hp: options.hp ?? 19 }); } } +export class CeremonialBeast extends Monster { + constructor(options = {}) { super({ ...options, character: "mengsan_ceremonial_beast_shuying", tier: "boss", hp: options.hp ?? 252 }); } +} +export class KinPriest extends Monster { + constructor(options = {}) { super({ ...options, character: "mengsan_kin_priest_shuying", tier: "boss", hp: options.hp ?? 190 }); } +} +export class KinFollower extends Monster { + constructor(options = {}) { super({ ...options, character: "mengsan_kin_follower_shuying", tier: "normal", hp: options.hp ?? 59 }); } +} +export class Vantom extends Monster { + constructor(options = {}) { + super({ ...options, character: "mengsan_vantom_shuying", tier: "boss", hp: options.hp ?? 173 }); + } +} export class BygoneEffigy extends Monster { constructor(options = {}) { super({ ...options, character: "mengsan_bygone_effigy_shuying", tier: "elite", hp: options.hp ?? 127 }); @@ -195,6 +212,9 @@ export const MONSTER_TYPES = Object.freeze({ mengsan_cubex_construct_shuying: CubexConstruct, mengsan_byrdonis_shuying: Byrdonis, mengsan_bygone_effigy_shuying: BygoneEffigy, + mengsan_vantom_shuying: Vantom, + mengsan_ceremonial_beast_shuying: CeremonialBeast, + mengsan_kin_priest_shuying: KinPriest, mengsan_kin_follower_shuying: KinFollower, mengsan_phrog_parasite_shuying: PhrogParasite, mengsan_wriggler_shuying: Wriggler, mengsan_shrinker_beetle_shuying: ShrinkerBeetle, diff --git a/mengsan/monsters/vantom-intent.js b/mengsan/monsters/vantom-intent.js new file mode 100644 index 0000000..b8056d7 --- /dev/null +++ b/mengsan/monsters/vantom-intent.js @@ -0,0 +1,43 @@ +// 网页普通主表:墨迹→墨水长枪→肢解→准备→墨迹;开场滑溜9。 +export const VANTOM_CHARACTER = "mengsan_vantom_shuying"; +export const VANTOM_MOVES = Object.freeze({ + blot: Object.freeze({ id: "blot", name: "墨迹", damage: 7 }), + lance: Object.freeze({ id: "lance", name: "墨水长枪", damage: 6, hits: 2 }), + dismember: Object.freeze({ id: "dismember", name: "肢解", damage: 26, wound: 3 }), + prepare: Object.freeze({ id: "prepare", name: "准备", strength: 2 }), +}); +const cycle = Object.freeze(Object.values(VANTOM_MOVES)); +const turnsOf = state => { + const turns = state.turnsTaken ?? 0; + if (!Number.isSafeInteger(turns) || turns < 0) throw new RangeError("墨影幻灵行动记录无效"); + return turns; +}; +export const selectVantomMove = (state = {}) => cycle[turnsOf(state) % cycle.length]; +export function recordVantomAction(state = {}, advance = true) { + const turns = turnsOf(state); + if (typeof advance !== "boolean" || advance && turns === Number.MAX_SAFE_INTEGER) throw new RangeError("墨影幻灵行动推进无效"); + return { turnsTaken: turns + (advance ? 1 : 0) }; +} +export const isVantom = player => player?.name === VANTOM_CHARACTER && player.storage?.mengsanCamp_shuying === "enemy"; +const bindings = new WeakMap(); +export function initializeVantom(player, current = null) { + if (!isVantom(player)) return false; + if (current?.session.active && bindings.get(player)?.battle !== current) { + const entry = { battle: current }; + current.session.ownResource(entry, () => { + if (bindings.get(player) !== entry) return; + bindings.delete(player); + delete player.storage.mengsanVantomState_shuying; + delete player.storage.mengsanVantomIntent_shuying; + player.removeSkill("mengsan_slippery_shuying"); + player.removeSkill("mengsan_raider_strength_shuying"); + }); + bindings.set(player, entry); + } + player.storage.mengsanVantomState_shuying = { turnsTaken: 0 }; + player.storage.mengsanVantomIntent_shuying = null; + player.storage.mengsanSlippery_shuying = 9; + player.addSkill("mengsan_slippery_shuying"); + player.markSkill("mengsan_slippery_shuying"); + return true; +} diff --git a/mengsan/monsters/vine-tangled.js b/mengsan/monsters/vine-tangled.js index 84623f8..e050665 100644 --- a/mengsan/monsters/vine-tangled.js +++ b/mengsan/monsters/vine-tangled.js @@ -2,10 +2,14 @@ import { consumeArtifact } from "./artifact-status.js"; import { cardDefinitions } from "../cards/card-definitions.js"; const KEY = "mengsanTangled_shuying"; const SKILL = "mengsan_tangled_shuying"; -// 梦三伤害牌类别;未进永久奖励注册表的原生攻击锦囊也适用。 +// 原版攻击分类优先;未配置类型的旧牌及原生攻击锦囊保留兼容判断。 const nativeAttacks = new Set(["nanman", "wanjian", "huogong"]); -export const isAttackCard = card => Boolean(card && (nativeAttacks.has(card.name) || - Object.hasOwn(cardDefinitions, card.name) && cardDefinitions[card.name].category === "damage")); +export const isAttackCard = card => { + if (!card) return false; + const definition = Object.hasOwn(cardDefinitions, card.name) ? cardDefinitions[card.name] : null; + return definition?.cardType ? definition.cardType === "attack" : + nativeAttacks.has(card.name) || definition?.category === "damage"; +}; const clear = target => { delete target.storage[KEY]; target.removeSkill(SKILL); }; const turnsOf = target => { const turns = target.storage?.[KEY] ?? 0; diff --git a/mengsan/progression/encounter-progress.js b/mengsan/progression/encounter-progress.js new file mode 100644 index 0000000..9ef8f7e --- /dev/null +++ b/mengsan/progression/encounter-progress.js @@ -0,0 +1,31 @@ +export const WEAK_BATTLE_COUNT = 3; + +// 与结算事务一起提交;事件、精英、Boss、固定专属关卡、失败和重试均不增加此计数。 +export function normalBattleCount(run) { + const saved = run.statistics?.normalBattles; + if (saved != null) { + if (!Number.isSafeInteger(saved) || saved < 0) throw new Error("普通战斗计数无效"); + return saved; + } + // 旧存档按第一章当前地图已完成的普通节点恢复;第二章起不再使用本次弱池阶段。 + const completed = (run.map?.nodes || []).filter(node => node.completed && node.type === "battle" && !node.contentId).length; + return run.actIndex > 0 ? Math.max(WEAK_BATTLE_COUNT, completed) : completed; +} + +// 已生成的地图也受保护;转换后的节点按普通战斗结算,未来楼层不提前转换。 +export function protectEarlyBattleNodes(run, nodes) { + if (run.actIndex !== 0 || normalBattleCount(run) >= WEAK_BATTLE_COUNT) return; + for (const node of nodes) { + if (node && !node.completed && !node.contentId && node.type === "elite") { + node.type = "battle"; + } + } +} + +export function recordNormalBattleVictory(run, node, encounter) { + if (run.actIndex !== 0 || node.type !== "battle" || node.contentId || encounter.requiredCharacter || + encounter.boss || encounter.tier !== "normal") return; + const next = normalBattleCount(run) + 1; + if (!Number.isSafeInteger(next)) throw new RangeError("普通战斗计数越界"); + run.statistics.normalBattles = next; +} diff --git a/mengsan/progression/map-checkpoint.js b/mengsan/progression/map-checkpoint.js new file mode 100644 index 0000000..9cc473e --- /dev/null +++ b/mengsan/progression/map-checkpoint.js @@ -0,0 +1,8 @@ +// 输入为存档副本;只保留征程成长和地图进度,不恢复战斗或未确认的结算。 +export function stripBattleProgress(run) { + if (!run || typeof run !== "object") return run; + delete run.battleFlow; + delete run.bondBattle; + delete run.sharedBattleCardEffects; + return run; +} diff --git a/mengsan/progression/mode-storage.js b/mengsan/progression/mode-storage.js index 437af15..94735a8 100644 --- a/mengsan/progression/mode-storage.js +++ b/mengsan/progression/mode-storage.js @@ -1,9 +1,17 @@ // Candidate factory. Storage writer body reused from batch2 MODIFIED_STORAGE.js. +import { migrateRetiredCards } from "../cards/retired-cards.js"; +import { stripBattleProgress } from "./map-checkpoint.js"; export function createModeStorage({lib, game, config, localStorage, alert = () => {}, console = globalThis.console}) { const MODE_ID = config.modeId; const copy = value => JSON.parse(JSON.stringify(value)); +const copyStorage = () => { + const storage = copy(lib.storage); + stripBattleProgress(storage[config.saveKey]); + migrateRetiredCards(storage[config.saveKey]); + return storage; +}; // Keep one mode-level writer; a failed write must not poison later retries. let modeStorageQueue = Promise.resolve(); @@ -42,8 +50,9 @@ const persistModeStorage = (update = () => {}) => { // reload2 also drains the engine's queued database operations on release. lib.status.reload++; const operation = modeStorageQueue.then(async () => { - const storage = copy(lib.storage); + const storage = copyStorage(); update(storage); + stripBattleProgress(storage[config.saveKey]); storage.version = lib.version; await writeModeStorage(storage); lib.storage = storage; @@ -55,7 +64,7 @@ const persistModeStorage = (update = () => {}) => { const saveRun = async run => { try { // Capture before enqueue: subsequent gameplay mutations cannot alter this save. - const snapshot = copy(run); + const snapshot = migrateRetiredCards(stripBattleProgress(copy(run))); await persistModeStorage(storage => { storage[config.saveKey] = snapshot; }); @@ -77,7 +86,7 @@ const clearRun = async () => { return Object.freeze({ - read: () => copy(lib.storage), + read: copyStorage, saveRun, clearRun, async update(mutator) { let value; diff --git a/mengsan/progression/reward.js b/mengsan/progression/reward.js index 6aa7f54..634269c 100644 --- a/mengsan/progression/reward.js +++ b/mengsan/progression/reward.js @@ -1,6 +1,6 @@ import config from "../config.js"; import { nextRandom } from "./state.js"; -import { canAcquireCard } from "../cards/card-definitions.js"; +import { canAcquireCard, cardDefinitions } from "../cards/card-definitions.js"; import { hasUpgradeableCard } from "../cards/upgrades.js"; import { canAcquireRelic } from "../relics/definitions.js"; @@ -17,10 +17,41 @@ export const canAcquireReward = (run, reward) => { return !name || canAcquireCard(run?.player?.character, name); }; -export const getRandomRewardChoices = (run, poolId, count = 3) => { +// 精英先抽稀有度再抽牌,避免牌池中各等级数量不同影响概率。 +// 每个候选:普通20%、罕见60%、稀有20%;同一组选项不重复。 +const eliteRarityWeights = Object.freeze({ common: 20, uncommon: 60, rare: 20 }); +const rewardRarity = id => cardDefinitions[rewardCardName(config.rewards[id])]?.rarity; + +export const getRandomRewardChoices = (run, poolId, count = 3, { allowSharedCards = false } = {}) => { + if (!Number.isSafeInteger(count) || count < 0) throw new RangeError("奖励候选数量无效"); const fallbackId = "shared.pool.battle.normal"; + const boss = poolId === "shared.pool.boss.premium"; const pool = (config.rewardPools[poolId] || config.rewardPools[fallbackId]) - .filter(id => canAcquireReward(run, config.rewards[id])).slice(); + .filter(id => { + const reward = config.rewards[id]; + if (!canAcquireReward(run, reward)) return false; + const name = rewardCardName(reward); + // 普通随机卡牌奖励仅来自当前角色;特殊剧情可显式允许通用牌。 + if (name && !allowSharedCards && cardDefinitions[name]?.owner !== run.player.character) return false; + return !boss || name && cardDefinitions[name]?.rarity === "rare"; + }); + if (boss && pool.length < count) throw new Error("角色专属稀有卡奖励不足,拒绝降级奖励"); + if (poolId === "shared.pool.battle.elite") { + const groups = Object.entries(eliteRarityWeights).map(([rarity, weight]) => ({ + weight, ids: pool.filter(id => rewardRarity(id) === rarity), + })); + const choices = []; + while (choices.length < count) { + const available = groups.filter(group => group.ids.length); + if (!available.length) break; + let cursor = nextRandom(run) * available.reduce((sum, group) => sum + group.weight, 0); + const group = available.find(group => (cursor -= group.weight) < 0) || available[available.length - 1]; + const index = Math.floor(nextRandom(run) * group.ids.length); + const [id] = group.ids.splice(index, 1); + choices.push({ id, ...config.rewards[id] }); + } + return choices; + } for (let index = pool.length - 1; index > 0; index--) { const target = Math.floor(nextRandom(run) * (index + 1)); [pool[index], pool[target]] = [pool[target], pool[index]]; diff --git a/mengsan/progression/state.js b/mengsan/progression/state.js index ba9a78e..fa6c127 100644 --- a/mengsan/progression/state.js +++ b/mengsan/progression/state.js @@ -1,3 +1,5 @@ +import { KIN_PRIEST_CHARACTER } from "../monsters/kin-intent.js"; +import { createKinBattlePlan } from "../content/acts/act1/kin-group.js"; import { createStartingDeck } from "../cards/starting-deck.js"; import config from "../config.js"; import { initializeInnateBonds } from "../bonds/state.js"; @@ -5,6 +7,8 @@ import { RAIDER_TRIO_ENCOUNTER, createRaiderTrioBattlePlan } from "../content/ac import { INKLET_CHARACTER } from "../monsters/inklet-intent.js"; import { createInkletBattlePlan } from "../content/acts/act1/inklet-group.js"; import { NIBBIT_PAIR_ENCOUNTER, createNibbitPairBattlePlan } from "../content/acts/act1/nibbit-pair.js"; +import { chooseEncounterEntry, createEncounterPlan } from "../content/encounters.js"; +import { normalBattleCount, protectEarlyBattleNodes, WEAK_BATTLE_COUNT } from "./encounter-progress.js"; const hashText = text => { let value = 2166136261; @@ -120,6 +124,10 @@ export const generateActMap = (run, actIndex) => { } else if (floor == 0) type = "battle"; else type = weightedType(run, act.nodeWeights); + // 专属开场后的前三层不生成随机精英;跳过战斗的路线还需按胜场继续保护。 + if (actIndex === 0 && floor <= WEAK_BATTLE_COUNT && !fixedNode && type === "elite") { + type = "battle"; + } const id = `${act.id}_f${floor}_n${index}_${Math.floor(nextRandom(run) * 1e6)}`; const node = { id, @@ -175,6 +183,7 @@ export const createRun = character => { handLimitBonus: 0, }, statistics: { + normalBattles: 0, completedNodes: 0, defeatedEnemies: 0, goldEarned: 0, @@ -188,11 +197,12 @@ export const createRun = character => { export const getSelectableNodes = run => { const map = run.map; - if (!map.currentNodeId) { - return map.nodes.filter(node => node.floor == 0 && !node.completed); - } - const ids = map.edges.filter(edge => edge[0] == map.currentNodeId).map(edge => edge[1]); - return map.nodes.filter(node => ids.includes(node.id) && !node.completed); + const ids = new Set(map.currentNodeId + ? map.edges.filter(edge => edge[0] == map.currentNodeId).map(edge => edge[1]) + : map.nodes.filter(node => node.floor == 0).map(node => node.id)); + // 同时修正旧存档的早期楼层和当前可走节点,使地图图标与实际遭遇一致。 + protectEarlyBattleNodes(run, map.nodes.filter(node => node.floor <= WEAK_BATTLE_COUNT || ids.has(node.id))); + return map.nodes.filter(node => ids.has(node.id) && !node.completed); }; export const completeNode = (run, nodeId) => { @@ -231,6 +241,11 @@ export const insertStoryNode = (run, sourceId) => { }; export const enterNextAct = run => { + if (config.acts[run.actIndex]?.endsCurrentContent) { + run.status = "completed"; + run.revision++; + return false; + } run.actIndex++; if (run.actIndex >= config.acts.length) { run.status = "completed"; @@ -245,11 +260,34 @@ export const getNodeEncounter = (run, node, override = null) => { const act = config.acts[run.actIndex]; const content = node.contentId ? config.nodeContents?.[node.contentId] : null; const encounter = override || content; + if (act.encounterPools && !encounter?.enemies?.length && !encounter?.battlePlan && + ["battle", "elite", "boss"].includes(node.type)) { + // 直接进入节点时也检查;交接传入的节点可能是副本,须同步地图中的结算类型。 + protectEarlyBattleNodes(run, [node, run.map?.nodes.find(current => current.id === node.id)]); + const count = normalBattleCount(run); + const poolName = node.type === "battle" ? (count < WEAK_BATTLE_COUNT ? "weak" : "strong") : node.type; + const id = chooseEncounterEntry(act.encounterPools[poolName], () => nextRandom(run)); + const definition = config.encounters[id]; + const battlePlan = createEncounterPlan(definition, () => nextRandom(run)); + const boss = node.type === "boss", elite = node.type === "elite"; + return { + name: definition.name, encounterId: id, encounterPool: poolName, + normalBattleNumber: node.type === "battle" ? count + 1 : null, + enemy: battlePlan.units.find(unit => unit.camp === "enemy").character, + battlePlan, tier: definition.tier, boss, + gold: act.baseGold * (boss ? 3 : elite ? 2 : 1), + rewardPool: boss ? "shared.pool.boss.premium" : elite ? "shared.pool.battle.elite" : "shared.pool.battle.normal", + guaranteedRelicPool: elite ? "shared.pool.elite.relics" : null, + rewardTitle: "战斗奖励(三选一)", + openingDialogue: [], victoryDialogue: [], fixedRewards: [], contentId: null, + }; + } let pool = encounter?.enemies || act.enemies; if (node.type == "elite" || node.type == "story") pool = act.eliteEnemies?.length ? act.eliteEnemies : act.enemies; if (encounter?.enemies?.length) pool = encounter.enemies; if (node.type == "boss") { - return { battlePlan: encounter?.battlePlan || act.bossBattlePlan || null, enemy: act.boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" }; + const boss = act.bossEnemies?.length ? randomGet(run, act.bossEnemies.filter(Boolean)) : act.boss; + return { name: boss === KIN_PRIEST_CHARACTER ? "同族小队" : "", battlePlan: encounter?.battlePlan || (boss === KIN_PRIEST_CHARACTER ? createKinBattlePlan() : act.bossBattlePlan || null), enemy: boss, tier: "boss", gold: act.baseGold * 3, boss: true, rewardPool: "shared.pool.boss.premium" }; } const enemy = randomGet(run, pool.filter(Boolean)); return { @@ -269,7 +307,9 @@ export const getNodeEncounter = (run, node, override = null) => { boss: false, contentId: node.contentId || null, description: encounter?.description || "", - rewardPool: encounter?.rewardPool || "shared.pool.battle.normal", + rewardPool: encounter?.rewardPool || (node.type === "elite" ? "shared.pool.battle.elite" : "shared.pool.battle.normal"), + allowSharedCardRewards: encounter?.allowSharedCardRewards === true, + guaranteedRelicPool: node.type === "elite" ? "shared.pool.elite.relics" : null, rewardTitle: encounter?.rewardTitle || "战斗奖励(三选一)", }; }; diff --git a/mengsan/relics/combat.js b/mengsan/relics/combat.js index 7502e44..23ed660 100644 --- a/mengsan/relics/combat.js +++ b/mengsan/relics/combat.js @@ -1,4 +1,5 @@ import { applyMengsanDebuff } from "../monsters/artifact-status.js"; +import { ironcladBlocksEnergy, ironcladStrengthPenalty } from "../cards/ironclad-hooks.js"; export function createOpeningEffects(game, owner, refreshEnergy) { const enemies = () => game.players.filter(player => @@ -6,14 +7,14 @@ export function createOpeningEffects(game, owner, refreshEnergy) { player.storage?.mengsanCamp_shuying === "enemy"); const addStatus = (player, key, skill, amount) => { if (key === "mengsanVulnerable_shuying" || key === "mengsanWeak_shuying") - return applyMengsanDebuff(player, key === "mengsanWeak_shuying" ? "weak" : "vulnerable", amount); + return applyMengsanDebuff(player, key === "mengsanWeak_shuying" ? "weak" : "vulnerable", amount, owner); player.storage[key] = (player.storage[key] || 0) + amount; player.addSkill(skill); player.markSkill(skill); }; return async relic => { if (relic.effect === "firstTurnEnergy") { - owner.storage.mengsanEnergy_shuying += relic.amount; + if (!ironcladBlocksEnergy(owner)) owner.storage.mengsanEnergy_shuying += relic.amount; refreshEnergy(); } else if (relic.effect === "openingStrength") { addStatus(owner, "mengsanStrength_shuying", @@ -48,13 +49,13 @@ export function createRelicCombatSkills(getBattle, getOwner) { return Boolean(getBattle()?.session.active && event.card && !event.mengsanScriptedSkill_shuying && ((source === getOwner() && - source?.storage?.mengsanStrength_shuying > 0) || + (source?.storage?.mengsanStrength_shuying > 0 || ironcladStrengthPenalty(source) > 0)) || source?.storage?.mengsanWeak_shuying > 0)); }, async content(event, trigger) { const source = trigger.source; if (source === getOwner()) { - trigger.num += source.storage.mengsanStrength_shuying || 0; + trigger.num = Math.max(0, trigger.num + (source.storage.mengsanStrength_shuying || 0) - ironcladStrengthPenalty(source)); } if (source.storage.mengsanWeak_shuying > 0) { trigger.num = Math.floor(trigger.num * 0.75); diff --git a/mengsan/ui/bonds.js b/mengsan/ui/bonds.js index 910bf37..23f9c8e 100644 --- a/mengsan/ui/bonds.js +++ b/mengsan/ui/bonds.js @@ -55,7 +55,8 @@ export function renderBonds(parent, run, saveRun, onBusy = () => {}) { card.className = "mengsan-bond-card-shuying"; root.appendChild(card); text("h3", definition.name, card); - const ready = Boolean(getBondCombat(id, npc.level)); + const combat = getBondCombat(id, npc.level); + const ready = Boolean(combat); const state = npc.down ? "濒死 · 等待救助" : ready ? "可助战" : "战斗配置待补"; text("p", `${state}${npc.down && !ready ? @@ -69,14 +70,14 @@ export function renderBonds(parent, run, saveRun, onBusy = () => {}) { text("p", npc.level === 10 ? "羁绊已满级" : `升级进度 ${npc.progress}% / 100%`, card); const button = text("button", bonds.selected === id ? - "已指定助战" : "指定助战", card); + "已指定(按概率到场)" : "指定助战", card); button.type = "button"; button.setAttribute("aria-pressed", String(bonds.selected === id)); button.addEventListener("click", () => choose(id)); buttons.push(button); - const combat = getBondCombat(id, npc.level); text("p", combat ? `生命 ${combat.maxHp} · 初始手牌 ${combat.hand}` : "生命、初始手牌及技能等待配置。", card); + if (combat) text("p", `每回合摸 ${combat.draw} 张 · 手牌上限 ${combat.handLimit} · 每回合费用 ${combat.energy}`, card); text("p", `独立牌库 ${getBondDeck(definition).length} 张 · 意图 ${ getBondIntents(definition).map(move => move.name).join(" → ")}`, card); @@ -86,6 +87,7 @@ export function renderBonds(parent, run, saveRun, onBusy = () => {}) { rescue.disabled = true; } } - text("p", "指定 NPC 后,每场战斗结束:到场 +50%,未到场 +25%。"); + text("p", "指定助战后,每场按当前等级概率判定到场;濒死或本等级未配置完整时无法上场。角色专属首战不触发助战,也不累计羁绊成长。"); + text("p", "其余战斗完成结算:到场 +50%,未到场 +25%。"); return root; } diff --git a/mengsan/ui/card-library.js b/mengsan/ui/card-library.js index 818c5ba..006a15c 100644 --- a/mengsan/ui/card-library.js +++ b/mengsan/ui/card-library.js @@ -1,11 +1,17 @@ import { lib, get, ui } from "../../../../noname.js"; import { cardDefinitions } from "../cards/card-definitions.js"; +import { cardTypes, cardRarityLabels } from "../cards/classification-types.js"; import { DAZED_NAME } from "../cards/status-cards.js"; import { SLIMED_NAME } from "../cards/slimed-card.js"; import { cardCost } from "../battle/combat-rules.js"; import { AFFIX_INFO } from "../cards/affixes.js"; +import { cardAffixes } from "../cards/intrinsic-affixes.js"; import { cardUpgradeLevel, cardUpgradeRule } from "../cards/upgrades.js"; +import { getCardRarity, applyCardRarity } from "../cards/rarity.js"; +import { cleanCardDescription } from "../cards/description.js"; +import { createVirtualCardGrid } from "./virtual-card-grid.js"; + const suits = { spade: "♠ 黑桃", heart: "♥ 红桃", club: "♣ 梅花", diamond: "♦ 方块" }; const types = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌", status: "状态牌" }; const affixes = Object.fromEntries(Object.entries(AFFIX_INFO).map(([key, info]) => [key, `${info.name}:${info.description}`])); @@ -23,12 +29,13 @@ export function describeLibraryCard(card) { return { name: translate(card.name), suit: suits[card.suit] || "无花色", number: ({ 1: "A", 11: "J", 12: "Q", 13: "K" })[card.number] || String(card.number ?? "—"), - nature: nature || "无属性", type: types[info?.type] || "未知类别", + nature: nature || "无属性", type: cardTypes[cardDefinitions[card.name]?.cardType || info?.cardType] || types[info?.type] || "未知类别", red: card.suit === "heart" || card.suit === "diamond", - description: text(cardUpgradeRule(card.name) && info?.cardPrompt + description: cleanCardDescription(text((cardUpgradeRule(card.name)||info?.mengsanShared_shuying) && info?.cardPrompt ? info.cardPrompt(card) - : lib.translate[`${card.name}_info`] || "暂无卡牌介绍。"), - affixes: (Array.isArray(card.affixes) ? card.affixes : []).filter(key => key !== "annihilate").map(key => affixes[key] || `词缀:${translate(key)}`), + : lib.translate[`${card.name}_info`] || "暂无卡牌介绍。")), + affixes: cardAffixes(card).filter(key => key !== "annihilate").map(key => affixes[key] || `词缀:${translate(key)}`), + rarity: getCardRarity(card), upgrade: cardUpgradeLevel(card), upgradeLimit: cardUpgradeRule(card.name)?.maxLevel || 0, available: Boolean(info), @@ -44,14 +51,14 @@ export function getCardCatalog() { .map(name => ({ name, nature: null, affixes: [], upgrade: 0 })); } -const categories = { damage: "攻击", utility: "辅助", recovery: "恢复", status: "状态" }; +const categories = { ...cardTypes, unknown: "未分类" }; function describeCatalogCard(card) { const details = describeLibraryCard(card), info = lib.card[card.name]; const definition = cardDefinitions[card.name]; const category = info?.type === "status" || card.name === DAZED_NAME || card.name === SLIMED_NAME - ? "status" : definition?.category || "utility"; + ? "status" : definition?.cardType || info?.cardType || "unknown"; const unplayable = info?.enable === false; - const cost = info && !unplayable ? cardCost(card) : null; + const cost = info && !unplayable ? (info.mengsanXCost_shuying ? "X" : cardCost(card)) : null; return { ...details, category, categoryName: categories[category], cost, unplayable, costLabel: !info ? "未加载" : unplayable ? "不可打出" : `${cost}费`, exclusive: definition?.owner ? translate(definition.owner) : "" }; @@ -75,9 +82,11 @@ function button(label, parent, action, className = "ms-deck-button") { // Read-only DOM previews, never game.createCard or Card.init: no IDs, triggers or pile writes. function preview(card, details, parent, catalog = false) { const face = element("div", "ms-deck-face", null, parent); + applyCardRarity(face, card); element("span", `ms-deck-corner${details.red ? " ms-deck-red" : ""}`, catalog ? details.costLabel : `${details.suit} ${details.number}`, face); const art = element("div", "ms-deck-art", null, face); - element("span", "ms-deck-glyph", details.name, art); + if (card.name === "mengsan_infection_shuying") art.dataset.mengsanOverlay = "infection"; + const glyph = element("span", "ms-deck-glyph", details.name, art); const imageName = lib.card[card.name]?.cardimage || card.name; const modeImage = lib.card[card.name]?.image; const localArt = typeof modeImage === "string" && @@ -88,11 +97,15 @@ function preview(card, details, parent, catalog = false) { const image = element("img", "ms-deck-image", null, art); image.alt = ""; image.loading = "lazy"; - image.addEventListener("error", () => image.remove(), { once: true }); + image.decoding = "async"; + image.addEventListener("load", () => { glyph.hidden = true; }, { once: true }); + image.addEventListener("error", () => { image.remove(); glyph.hidden = false; }, { once: true }); image.src = `${lib.assetURL || ""}${localArt || `image/card/${imageName}.png`}`; } element("strong", "ms-deck-name", details.name, face); - element("span", "ms-deck-meta", catalog ? `${details.categoryName} · ${details.exclusive ? "专属:" + details.exclusive : details.category === "status" ? "战斗生成" : "通用"}` : `${details.type} · ${details.nature}`, face); + element("span", "ms-deck-meta", catalog ? `${details.categoryName} · ${details.exclusive ? "专属:" + details.exclusive : cardDefinitions[card.name]?.generatedOnly ? "战斗生成" : "通用"}` : `${details.type} · ${details.nature}`, face); + face.setAttribute("aria-label", `${details.name},稀有度:${details.rarity.label}`); + face.title = `稀有度:${details.rarity.label}`; if (details.upgrade || details.affixes.length) { element("span", "ms-deck-tags", [details.upgrade ? `+${details.upgrade}` : "", ...details.affixes.map(value => value.split(":")[0])].filter(Boolean).join(" · "), face); } @@ -109,10 +122,14 @@ export function openCardLibrary(run, options = {}) { const title = element("div", "ms-deck-title-group", null, header); element("span", "ms-deck-eyebrow", options.eyebrow || "梦三 · 行囊", title); element("h2", "ms-deck-title", options.title || "卡牌库", title); - let closed = false; + let closed = false, sizeObserver = null, virtualGrid = null, searchTimer = 0; const close = () => { if (closed) return; closed = true; + clearTimeout(searchTimer); + virtualGrid?.dispose(); + sizeObserver?.disconnect(); + if (typeof window !== "undefined") window.removeEventListener?.("resize", fitCards); if (dialog.open) dialog.close(); dialog.remove(); if (previousFocus?.isConnected) previousFocus.focus(); @@ -137,17 +154,26 @@ export function openCardLibrary(run, options = {}) { for (const [key, label] of Object.entries({ "": "全部类型", ...categories })) { const option = element("option", "", label, categoryFilter); option.value = key; } + const rarityFilter = element("select", "ms-catalog-select", null, field("稀有度")); + rarityFilter.setAttribute("aria-label", "图鉴稀有度"); + for (const [key, label] of Object.entries({ "": "全部稀有度", ...cardRarityLabels })) { + const option = element("option", "", label, rarityFilter); option.value = key; + } const costFilter = element("select", "ms-catalog-select", null, field("费用")); costFilter.setAttribute("aria-label", "图鉴费用"); - for (const [key, label] of [["", "全部费用"], ["0", "0费"], ["1", "1费"], ["2", "2费"], ["3+", "3费及以上"], ["unplayable", "不可打出"]]) { + for (const [key, label] of [["", "全部费用"], ["0", "0费"], ["1", "1费"], ["2", "2费"], ["3+", "3费及以上"], ["X", "X费"], ["unplayable", "不可打出"]]) { const option = element("option", "", label, costFilter); option.value = key; } button("清空筛选", filters, () => { - search.value = ""; categoryFilter.value = ""; costFilter.value = ""; + search.value = ""; categoryFilter.value = ""; rarityFilter.value = ""; costFilter.value = ""; render(); search.focus(); }); - search.addEventListener("input", render); + search.addEventListener("input", () => { + clearTimeout(searchTimer); + searchTimer = setTimeout(render, 140); + }); categoryFilter.addEventListener("change", render); + rarityFilter.addEventListener("change", render); costFilter.addEventListener("change", render); const summary = element("p", "ms-deck-summary", null, dialog); summary.setAttribute("aria-live", "polite"); @@ -155,15 +181,20 @@ export function openCardLibrary(run, options = {}) { const footer = element("p", "ms-deck-footer", "点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序", dialog); const current = Array.isArray(run?.player?.deck) ? run.player.deck : []; let detail = false, selectedButton = null, scrollTop = 0; - const list = element("div", "ms-deck-grid", null, body); + const list = element("div", options.sections ? "ms-deck-grid" : "ms-deck-grid ms-library-grid", null, body); const detailPanel = element("section", "ms-deck-detail", null, body); detailPanel.hidden = true; + let catalogRecords = null, descriptionCache = new WeakMap(); + if (!options.sections) virtualGrid = createVirtualCardGrid(body, list, createTile); function back() { detail = false; detailPanel.hidden = true; list.hidden = false; - detailPanel.replaceChildren(); body.scrollTop = scrollTop; - selectedButton?.focus({ preventScroll: true }); + detailPanel.replaceChildren(); body.scrollTop = scrollTop; fitCards(); + virtualGrid?.resume(); + if (selectedButton?.isConnected) selectedButton.focus({ preventScroll: true }); + else if (virtualGrid && selectedButton) virtualGrid.focus(Number(selectedButton.dataset.cardIndex)); } function showDetail(card, details, owner, catalog = false) { + clearTimeout(searchTimer); selectedButton = owner; scrollTop = body.scrollTop; detail = true; list.hidden = true; detailPanel.hidden = false; detailPanel.replaceChildren(); @@ -172,18 +203,45 @@ export function openCardLibrary(run, options = {}) { preview(card, details, content, catalog); const copy = element("div", "ms-deck-detail-copy", null, content); element("h3", "", details.name, copy); - element("p", "", catalog ? `${details.costLabel} · ${details.categoryName} · ${details.type}` : + element("p", "", catalog ? `${details.costLabel} · ${details.categoryName}` : `${details.suit} ${details.number} · ${details.type} · ${details.nature}`, copy); if (catalog && details.exclusive) element("p", "ms-deck-rules", `专属角色:${details.exclusive}`, copy); + element("p", "ms-deck-rarity-label", `稀有度:${details.rarity.label}`, copy); element("h4", "", "卡牌介绍", copy); element("p", "ms-deck-rules", details.description, copy); if (!details.available) element("p", "ms-deck-warning", "该卡牌定义未加载;保留存档记录,不删除此牌。", copy); for (const affix of details.affixes) element("p", "ms-deck-rules", affix, copy); body.scrollTop = 0; backButton.focus(); } + function fitCards() { + if (options.sections || closed || !body.clientHeight) return; + const padding = getComputedStyle(body); + const available = body.clientHeight - parseFloat(padding.paddingTop) - parseFloat(padding.paddingBottom); + const height = Math.max(64, Math.min(218, Math.floor((available - 8) / 2))); + dialog.style.setProperty("--ms-card-height", String(height) + "px"); + dialog.style.setProperty("--ms-card-art-width", String(Math.max(8, Math.min(98, Math.floor((height - 80) * 5 / 7)))) + "px"); + virtualGrid?.layout(height); + } + function createTile(record, index) { + const { card, catalog = false, ordered = false } = record; + let details = record.details || descriptionCache.get(card); + if (!details) { + details = describeLibraryCard(card); descriptionCache.set(card, details); + } + const tile = button("", null, () => showDetail(card, details, tile, catalog), "ms-deck-card"); + tile.setAttribute("aria-label", catalog ? `${details.name},${details.categoryName},${details.costLabel},查看详情` : + `${ordered ? `第 ${index + 1} 张,` : ""}${details.name},${details.suit}${details.number},${details.nature},查看详情`); + if (ordered) element("span", "ms-deck-order", index === 0 ? "1 · 下一张" : `${index + 1}`, tile); + preview(card, details, tile, catalog); + element("span", "ms-deck-excerpt", details.description, tile); + return tile; + } function render() { + clearTimeout(searchTimer); detail = false; list.hidden = false; detailPanel.hidden = true; - detailPanel.replaceChildren(); list.replaceChildren(); body.scrollTop = 0; + detailPanel.replaceChildren(); + if (!virtualGrid) list.replaceChildren(); + body.scrollTop = 0; selectedButton = null; const catalog = !options.sections && view === "catalog"; filters.hidden = !catalog; @@ -193,21 +251,36 @@ export function openCardLibrary(run, options = {}) { let records = []; if (catalog) { const query = search.value.trim().toLocaleLowerCase(); - records = getCardCatalog().map(card => ({ card, details: describeCatalogCard(card) })); + catalogRecords ||= getCardCatalog().map(card => { + const details = describeCatalogCard(card); + return { card, catalog: true, details, + searchText: `${details.name} ${card.name} ${details.description}`.toLocaleLowerCase() }; + }); + records = catalogRecords; const total = records.length; - records = records.filter(({ card, details }) => - (!query || `${details.name} ${card.name} ${details.description}`.toLocaleLowerCase().includes(query)) && + records = records.filter(({ details, searchText }) => + (!query || searchText.includes(query)) && (!categoryFilter.value || details.category === categoryFilter.value) && + (!rarityFilter.value || details.rarity.key === rarityFilter.value) && (!costFilter.value || (costFilter.value === "unplayable" ? details.unplayable : - costFilter.value === "3+" ? details.cost !== null && details.cost >= 3 : + costFilter.value === "X" ? details.cost === "X" : + costFilter.value === "3+" ? typeof details.cost === "number" && details.cost >= 3 : details.cost !== null && details.cost === Number(costFilter.value)))); - summary.textContent = `梦三卡牌图鉴 · 共 ${total} 种 · 当前显示 ${records.length} 种`; + summary.textContent = `卡牌图鉴 · 共 ${total} 种 · 当前显示 ${records.length} 种`; } else { summary.textContent = options.sections ? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。" : `本次征程实际携带 · 共 ${current.length} 张 · 同名不同花色、点数分别展示`; } footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" : "点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序"; + if (virtualGrid) { + virtualGrid.setRecords(catalog ? records : current.map(card => ({ card }))); + if (!(catalog ? records : current).length) { + element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : "此处暂无卡牌。", list); + } + fitCards(); + return; + } const sections = catalog ? [{ title: "", cards: records.map(record => record.card) }] : options.sections ? options.sections() : [{ title: "", cards: current }]; for (const section of sections) { @@ -215,13 +288,7 @@ export function openCardLibrary(run, options = {}) { if (section.title) element("h3", "ms-deck-section", `${section.title} · ${cards.length} 张`, list); if (!cards.length) element("p", "ms-deck-empty", catalog ? "没有符合筛选的卡牌,可清空筛选后查看全部图鉴。" : section.ordered ? "剩余牌堆为空;下一次摸牌需按规则洗切弃牌堆,洗切后的顺序尚未确定。" : "此处暂无卡牌。", list); cards.forEach((card, index) => { - const details = catalog ? records[index].details : describeLibraryCard(card); - const tile = button("", list, () => showDetail(card, details, tile, catalog), "ms-deck-card"); - tile.setAttribute("aria-label", catalog ? `${details.name},${details.categoryName},${details.costLabel},查看详情` : - `${section.ordered ? `第 ${index + 1} 张,` : ""}${details.name},${details.suit}${details.number},${details.nature},查看详情`); - if (section.ordered) element("span", "ms-deck-order", index === 0 ? "1 · 下一张" : `${index + 1}`, tile); - preview(card, details, tile, catalog); - element("span", "ms-deck-excerpt", details.description, tile); + list.appendChild(createTile({ card, ordered: section.ordered }, index)); }); } } @@ -232,12 +299,21 @@ export function openCardLibrary(run, options = {}) { render(); document.body.appendChild(dialog); try { dialog.showModal(); } catch (error) { close(); throw error; } + fitCards(); + if (!options.sections && typeof ResizeObserver !== "undefined") { + sizeObserver = new ResizeObserver(fitCards); + sizeObserver.observe(body); + } else if (!options.sections && typeof window !== "undefined") { + window.addEventListener?.("resize", fitCards); + } header.querySelector("button").focus(); close.refresh = () => { if (closed) return; // Rebuild from live arrays after a pile mutation, including while viewing details. const wasDetail = detail, offset = body.scrollTop; + catalogRecords = null; descriptionCache = new WeakMap(); render(); body.scrollTop = offset; + virtualGrid?.resume(); if (wasDetail) header.querySelector("button").focus(); }; return close; diff --git a/mengsan/ui/flow-ui.js b/mengsan/ui/flow-ui.js index 8b9d83e..dbb0116 100644 --- a/mengsan/ui/flow-ui.js +++ b/mengsan/ui/flow-ui.js @@ -48,7 +48,7 @@ export const chooseButtons = (title, choices, description = "", options = {}) => button.addEventListener("click", () => finish(choice.id)); buttons.appendChild(button); }); - mountMenu(overlay); + if (options.menu !== false) mountMenu(overlay); overlay.addEventListener("keydown", event => { if (event.key === "Escape" && options.back) { event.preventDefault(); finish(options.back); return; diff --git a/mengsan/ui/gm-manager.js b/mengsan/ui/gm-manager.js index e52286d..249d8ef 100644 --- a/mengsan/ui/gm-manager.js +++ b/mengsan/ui/gm-manager.js @@ -1,4 +1,5 @@ import { AFFIX_INFO } from "../cards/affixes.js"; +import { hasCardAffix } from "../cards/intrinsic-affixes.js"; import { describeLibraryCard } from "./card-library.js"; const node = (tag, className, label, parent) => { @@ -49,7 +50,7 @@ export function mountGMManager(session, owner, piles, battle, resources, game) { node("h3", "", `编辑:${describeLibraryCard(selected.storage.mengsanCard_shuying).name}`, editor); const choices = node("div", "ms-gm-affixes", null, editor); for (const [key, info] of Object.entries(AFFIX_INFO)) { - const applied = selected.storage.mengsanCard_shuying.affixes.includes(key); + const applied = hasCardAffix(selected,key); const item = button(`${info.name}${applied ? " · 已有" : ""}`, choices, () => { if (!available() || !owner.getCards("h").includes(selected)) { render(); return; } piles.addAffix(selected, key); diff --git a/mengsan/ui/hand/overlays.js b/mengsan/ui/hand/overlays.js index 1159701..19866bd 100644 --- a/mengsan/ui/hand/overlays.js +++ b/mengsan/ui/hand/overlays.js @@ -1,4 +1,6 @@ import { cardUpgradeLevel } from "../../cards/upgrades.js"; +import { isXCostCard } from "../../battle/combat-rules.js"; +import { applyCardRarity } from "../../cards/rarity.js"; export function createHandOverlays(document, cardCost) { const entries = new Map(); @@ -15,14 +17,31 @@ export function createHandOverlays(document, cardCost) { const upgrade = document.createElement("span"); upgrade.className = "mengsan-hand-upgrade-shuying"; upgrade.setAttribute("role", "img"); - entry = { cost, upgrade }; + const rarity = document.createElement("span"); + rarity.className = "mengsan-hand-rarity-shuying"; + rarity.setAttribute("role", "img"); + entry = { cost, upgrade, rarity, border: null }; entries.set(card, entry); } - const cost = String(cardCost(card)); + const rarity = applyCardRarity(card, card); + entry.rarity.setAttribute("aria-label", `稀有度:${rarity.label}`); + if (entry.rarity.parentNode !== card) card.appendChild(entry.rarity); + const cost = isXCostCard(card) ? "X" : String(cardCost(card)); setData(entry.cost, "mengsanCost", cost); entry.cost.setAttribute("aria-label", `费用 ${cost}`); if (card.name === "mengsan_dazed_shuying") entry.cost.remove(); else if (entry.cost.parentNode !== card) card.appendChild(entry.cost); + // Empty decorative node: Decade UI scans child innerText when cards move. + if (card.name === "mengsan_infection_shuying") { + if (!entry.border) { + entry.border = document.createElement("span"); + entry.border.className = "mengsan-infection-border-shuying"; + entry.border.setAttribute("aria-hidden", "true"); + } + if (entry.border.parentNode !== card) card.appendChild(entry.border); + } else { + entry.border?.remove(); + } const level = cardUpgradeLevel(card); if (level) { setData(entry.upgrade, "mengsanUpgrade", String(level)); @@ -37,6 +56,11 @@ export function createHandOverlays(document, cardCost) { if (!entry) return; entry.cost.remove(); entry.upgrade.remove(); + entry.border?.remove(); + entry.rarity.remove(); + delete card.dataset.mengsanRarity; + delete card.dataset.mengsanRarityLabel; + for (const key of ["accent", "dark", "ink", "metal"]) card.style.removeProperty(`--ms-rarity-${key}`); entries.delete(card); } return { diff --git a/mengsan/ui/hand/style.css b/mengsan/ui/hand/style.css index ae11509..13bb9dd 100644 --- a/mengsan/ui/hand/style.css +++ b/mengsan/ui/hand/style.css @@ -187,12 +187,12 @@ body.mengsan-hand-ui-shuying :is( } } -/* 梦三本地竖幅立绘使用完整卡面;只协调梦三手牌里的原生显示节点。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) { +/* 梦三本地竖幅立绘:刘备及四类共享牌统一使用完整卡面;只协调梦三手牌里的原生显示节点。 */ +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) { background-size: cover !important; background-position: center !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { display: block !important; top: auto !important; bottom: 8px !important; @@ -214,14 +214,14 @@ body.mengsan-hand-ui-shuying :is( z-index: 7 !important; pointer-events: none; } -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) br { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) br { display: none; } /* 十周年同时保留竖排名称与 top-name 时,只显示后者。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .top-name) > .name { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip):has(> .top-name) > .name { display: none !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { top: 6px !important; left: 6px !important; right: auto !important; @@ -241,17 +241,17 @@ body.mengsan-hand-ui-shuying :is( pointer-events: none; } -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .image, -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]) > .info:empty { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]) > .image, +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]) > .info:empty { display: none !important; } -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):is([data-suit="heart"], [data-suit="diamond"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { +.mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):is([data-suit="heart"], [data-suit="diamond"]):not(.infohidden):not(.infoflip) > :is(.info:not(.gaintag), .suit-num) { color: #a52219 !important; } /* 强化卡牌的完整卡面样式优先级更高,窄屏同样为角标留出空隙。 */ @media (max-width: 640px), (max-height: 500px) { - .mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"]):not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num { + .mengsan-hand-zone-shuying > .card:is([data-card-name^="mengsan_ic_"], [data-card-name^="mengsan_colorless_"], [data-card-name^="mengsan_event_"], [data-card-name^="mengsan_status_"], [data-card-name^="mengsan_curse_"], [data-card-name="mengsan_zhongsha"], [data-card-name="mengsan_fangyu"], [data-card-name="mengsan_fennu"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"], [data-card-name="mengsan_wuzhuang"], [data-card-name="mengsan_zhanli"], [data-card-name="mengsan_xueqiang"], [data-card-name="mengsan_qiling"], [data-card-name="mengsan_yuanhen"], [data-card-name="mengsan_baozou"], [data-card-name="mengsan_xuanfengzhan"], [data-card-name="mengsan_dazed_shuying"], [data-card-name="mengsan_slimed_shuying"], [data-card-name="mengsan_infection_shuying"], [data-card-name="mengsan_wound_shuying"]):not(.infohidden):not(.infoflip):has(> .mengsan-hand-upgrade-shuying) > .suit-num { top: 4px !important; padding: 0 2px !important; font-size: 18px !important; @@ -260,14 +260,214 @@ body.mengsan-hand-ui-shuying :is( } /* 长牌名完整显示,不挤出卡面;只作用于梦三手牌。 */ -.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { +.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_ashen_strike"], [data-card-name="mengsan_ic_fight_me"], [data-card-name="mengsan_ic_demonic_shield"], [data-card-name="mengsan_ic_forgotten_ritual"], [data-card-name="mengsan_ic_burning_pact"], [data-card-name="mengsan_ic_second_wind"], [data-card-name="mengsan_ic_pacts_end"], [data-card-name="mengsan_ic_fiend_fire"], [data-card-name="mengsan_ic_impervious"], [data-card-name="mengsan_ic_not_yet"], [data-card-name="mengsan_ic_demon_form"], [data-card-name="mengsan_ic_pyre"], [data-card-name="mengsan_ic_crimson_mantle"], [data-card-name="mengsan_ic_flame_barrier"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { font-size: 12px !important; letter-spacing: 0 !important; white-space: nowrap !important; } @media (max-width: 640px), (max-height: 500px) { - .mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + .mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_ashen_strike"], [data-card-name="mengsan_ic_fight_me"], [data-card-name="mengsan_ic_demonic_shield"], [data-card-name="mengsan_ic_forgotten_ritual"], [data-card-name="mengsan_ic_burning_pact"], [data-card-name="mengsan_ic_second_wind"], [data-card-name="mengsan_ic_pacts_end"], [data-card-name="mengsan_ic_fiend_fire"], [data-card-name="mengsan_ic_impervious"], [data-card-name="mengsan_ic_not_yet"], [data-card-name="mengsan_ic_demon_form"], [data-card-name="mengsan_ic_pyre"], [data-card-name="mengsan_ic_crimson_mantle"], [data-card-name="mengsan_ic_flame_barrier"], [data-card-name="mengsan_feijianhuixuanbiao"], [data-card-name="mengsan_jianbingdaji"], [data-card-name="mengsan_quanshenzhuangji"], [data-card-name="mengsan_rongrongzhiquan"], [data-card-name="mengsan_shandianpili"], [data-card-name="mengsan_shuangchongdaji"], [data-card-name="mengsan_tiezhanbo"], [data-card-name="mengsan_touchui"], [data-card-name="mengsan_tupo"], [data-card-name="mengsan_yubeidaji"], [data-card-name="mengsan_wanmeidaji"], [data-card-name="mengsan_yujin"], [data-card-name="mengsan_fangxue"], [data-card-name="mengsan_jianyi"], [data-card-name="mengsan_pomie"], [data-card-name="mengsan_songjianwushi"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { font-size: 10px !important; padding: 2px 4px !important; } } + +/* Infection only; keep cost/name above it and leave native card::after alone. */ +.mengsan-infection-border-shuying { display: none; } +.mengsan-hand-zone-shuying > .card[data-card-name="mengsan_infection_shuying"]:not(.infohidden):not(.infoflip) > .mengsan-infection-border-shuying { + display: block; + z-index: 5; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border-radius: inherit; + pointer-events: none; + background-image: url("../../assets/card-overlays/infection-sprite.webp"); + /* 30 source frames, six columns/five rows, 192x260 per cell. */ + background-size: 600% 500%; + background-position: 0% 0%; + background-repeat: no-repeat; + animation: mengsan-infection-frames-shuying 2s steps(1, end) infinite; +} +@keyframes mengsan-infection-frames-shuying { + 0% { background-position: 0% 0%; } + 3.333333% { background-position: 20% 0%; } + 6.666667% { background-position: 40% 0%; } + 10% { background-position: 60% 0%; } + 13.333333% { background-position: 80% 0%; } + 16.666667% { background-position: 100% 0%; } + 20% { background-position: 0% 25%; } + 23.333333% { background-position: 20% 25%; } + 26.666667% { background-position: 40% 25%; } + 30% { background-position: 60% 25%; } + 33.333333% { background-position: 80% 25%; } + 36.666667% { background-position: 100% 25%; } + 40% { background-position: 0% 50%; } + 43.333333% { background-position: 20% 50%; } + 46.666667% { background-position: 40% 50%; } + 50% { background-position: 60% 50%; } + 53.333333% { background-position: 80% 50%; } + 56.666667% { background-position: 100% 50%; } + 60% { background-position: 0% 75%; } + 63.333333% { background-position: 20% 75%; } + 66.666667% { background-position: 40% 75%; } + 70% { background-position: 60% 75%; } + 73.333333% { background-position: 80% 75%; } + 76.666667% { background-position: 100% 75%; } + 80% { background-position: 0% 100%; } + 83.333333% { background-position: 20% 100%; } + 86.666667% { background-position: 40% 100%; } + 90% { background-position: 60% 100%; } + 93.333333% { background-position: 80% 100%; } + 96.666667% { background-position: 100% 100%; } + 100% { background-position: 100% 100%; } +} +@media (prefers-reduced-motion: reduce) { + .mengsan-hand-zone-shuying > .card[data-card-name="mengsan_infection_shuying"]:not(.infohidden):not(.infoflip) > .mengsan-infection-border-shuying { + animation: none; + background-position: 0% 0%; + } +} + +/* 第二批刘备牌组长牌名,沿用现有手牌字号。 */ +.mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_relentless_assault"], [data-card-name="mengsan_ic_howl_from_beyond"], [data-card-name="mengsan_ic_battle_trance"], [data-card-name="mengsan_ic_infernal_blade"], [data-card-name="mengsan_ic_eager"], [data-card-name="mengsan_ic_feel_no_pain"], [data-card-name="mengsan_ic_stone_armor"], [data-card-name="mengsan_ic_primal_force"], [data-card-name="mengsan_ic_battle_ready"], [data-card-name="mengsan_ic_hellraiser"], [data-card-name="mengsan_ic_dark_embrace"], [data-card-name="mengsan_ic_unshakable"], [data-card-name="mengsan_ic_juggernaut"], [data-card-name="mengsan_ic_rally"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + font-size: 12px !important; letter-spacing: 0 !important; white-space: nowrap !important; +} +@media (max-width: 640px), (max-height: 500px) { + .mengsan-hand-zone-shuying > .card:is([data-card-name="mengsan_ic_relentless_assault"], [data-card-name="mengsan_ic_howl_from_beyond"], [data-card-name="mengsan_ic_battle_trance"], [data-card-name="mengsan_ic_infernal_blade"], [data-card-name="mengsan_ic_eager"], [data-card-name="mengsan_ic_feel_no_pain"], [data-card-name="mengsan_ic_stone_armor"], [data-card-name="mengsan_ic_primal_force"], [data-card-name="mengsan_ic_battle_ready"], [data-card-name="mengsan_ic_hellraiser"], [data-card-name="mengsan_ic_dark_embrace"], [data-card-name="mengsan_ic_unshakable"], [data-card-name="mengsan_ic_juggernaut"], [data-card-name="mengsan_ic_rally"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + font-size: 10px !important; padding: 2px 4px !important; + } +} + + +/* 四类共享牌的长名称与刘备牌组采用相同字号,窄屏仍完整显示。 */ +.mengsan-hand-zone-shuying > .card:is( + [data-card-name="mengsan_colorless_dramatic_entrance"], + [data-card-name="mengsan_colorless_ultimate_strike"], + [data-card-name="mengsan_colorless_gang_up"], + [data-card-name="mengsan_colorless_seeker_strike"], + [data-card-name="mengsan_colorless_thrumming_hatchet"], + [data-card-name="mengsan_colorless_mind_blast"], + [data-card-name="mengsan_colorless_tag_team"], + [data-card-name="mengsan_colorless_dark_shackles"], + [data-card-name="mengsan_colorless_jack_of_all_trades"], + [data-card-name="mengsan_colorless_thinking_ahead"], + [data-card-name="mengsan_colorless_production"], + [data-card-name="mengsan_colorless_believe_in_you"], + [data-card-name="mengsan_colorless_restlessness"], + [data-card-name="mengsan_colorless_panic_button"], + [data-card-name="mengsan_colorless_ultimate_defend"], + [data-card-name="mengsan_colorless_coordinate"], + [data-card-name="mengsan_colorless_panache"], + [data-card-name="mengsan_colorless_prowess"], + [data-card-name="mengsan_colorless_prep_time"], + [data-card-name="mengsan_colorless_hand_of_greed"], + [data-card-name="mengsan_colorless_the_gambit"], + [data-card-name="mengsan_colorless_secret_technique"], + [data-card-name="mengsan_colorless_secret_weapon"], + [data-card-name="mengsan_colorless_master_of_strategy"], + [data-card-name="mengsan_colorless_alchemize"], + [data-card-name="mengsan_colorless_scrawl"], + [data-card-name="mengsan_colorless_hidden_gem"], + [data-card-name="mengsan_colorless_beacon_of_hope"], + [data-card-name="mengsan_colorless_eternal_armor"], + [data-card-name="mengsan_event_byrd_swoop"], + [data-card-name="mengsan_event_mad_science_sapping"], + [data-card-name="mengsan_event_mad_science_violence"], + [data-card-name="mengsan_event_mad_science_choking"], + [data-card-name="mengsan_event_feeding_frenzy"], + [data-card-name="mengsan_event_mad_science_energized"], + [data-card-name="mengsan_event_mad_science_wisdom"], + [data-card-name="mengsan_event_mad_science_chaos"], + [data-card-name="mengsan_event_toric_toughness"], + [data-card-name="mengsan_event_mad_science_expertise"], + [data-card-name="mengsan_event_mad_science_curious"], + [data-card-name="mengsan_event_mad_science_improvement"], + [data-card-name="mengsan_event_neows_fury"], + [data-card-name="mengsan_event_brightest_flame"], + [data-card-name="mengsan_status_frantic_escape"], + [data-card-name="mengsan_curse_spore_mind"], + [data-card-name="mengsan_curse_ascenders_bane"], + [data-card-name="mengsan_curse_curse_of_the_bell"], + [data-card-name="mengsan_curse_poor_sleep"], + [data-card-name="mengsan_status_deprecated_card"] +):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + font-size: 12px !important; + letter-spacing: 0 !important; + white-space: nowrap !important; +} +@media (max-width: 640px), (max-height: 500px) { + .mengsan-hand-zone-shuying > .card:is( + [data-card-name="mengsan_colorless_dramatic_entrance"], + [data-card-name="mengsan_colorless_ultimate_strike"], + [data-card-name="mengsan_colorless_gang_up"], + [data-card-name="mengsan_colorless_seeker_strike"], + [data-card-name="mengsan_colorless_thrumming_hatchet"], + [data-card-name="mengsan_colorless_mind_blast"], + [data-card-name="mengsan_colorless_tag_team"], + [data-card-name="mengsan_colorless_dark_shackles"], + [data-card-name="mengsan_colorless_jack_of_all_trades"], + [data-card-name="mengsan_colorless_thinking_ahead"], + [data-card-name="mengsan_colorless_production"], + [data-card-name="mengsan_colorless_believe_in_you"], + [data-card-name="mengsan_colorless_restlessness"], + [data-card-name="mengsan_colorless_panic_button"], + [data-card-name="mengsan_colorless_ultimate_defend"], + [data-card-name="mengsan_colorless_coordinate"], + [data-card-name="mengsan_colorless_panache"], + [data-card-name="mengsan_colorless_prowess"], + [data-card-name="mengsan_colorless_prep_time"], + [data-card-name="mengsan_colorless_hand_of_greed"], + [data-card-name="mengsan_colorless_the_gambit"], + [data-card-name="mengsan_colorless_secret_technique"], + [data-card-name="mengsan_colorless_secret_weapon"], + [data-card-name="mengsan_colorless_master_of_strategy"], + [data-card-name="mengsan_colorless_alchemize"], + [data-card-name="mengsan_colorless_scrawl"], + [data-card-name="mengsan_colorless_hidden_gem"], + [data-card-name="mengsan_colorless_beacon_of_hope"], + [data-card-name="mengsan_colorless_eternal_armor"], + [data-card-name="mengsan_event_byrd_swoop"], + [data-card-name="mengsan_event_mad_science_sapping"], + [data-card-name="mengsan_event_mad_science_violence"], + [data-card-name="mengsan_event_mad_science_choking"], + [data-card-name="mengsan_event_feeding_frenzy"], + [data-card-name="mengsan_event_mad_science_energized"], + [data-card-name="mengsan_event_mad_science_wisdom"], + [data-card-name="mengsan_event_mad_science_chaos"], + [data-card-name="mengsan_event_toric_toughness"], + [data-card-name="mengsan_event_mad_science_expertise"], + [data-card-name="mengsan_event_mad_science_curious"], + [data-card-name="mengsan_event_mad_science_improvement"], + [data-card-name="mengsan_event_neows_fury"], + [data-card-name="mengsan_event_brightest_flame"], + [data-card-name="mengsan_status_frantic_escape"], + [data-card-name="mengsan_curse_spore_mind"], + [data-card-name="mengsan_curse_ascenders_bane"], + [data-card-name="mengsan_curse_curse_of_the_bell"], + [data-card-name="mengsan_curse_poor_sleep"], + [data-card-name="mengsan_status_deprecated_card"] + ):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + font-size: 10px !important; + padding: 2px 4px !important; + } +} + +/* 稀有度是独立装饰层,不占用原生 card::after,也不改变尺寸、命中区或立绘。 */ +.mengsan-hand-rarity-shuying { display: none; pointer-events: none; } +.mengsan-hand-zone-shuying > .card[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]):not(.infohidden):not(.infoflip) > .mengsan-hand-rarity-shuying { + display: block; position: absolute; inset: 0; z-index: 4; border-radius: inherit; + margin: 0; padding: 0; width: 100%; height: 100%; + box-shadow: inset 0 0 0 2px var(--ms-rarity-accent), inset 0 0 0 4px var(--ms-rarity-dark); + background: linear-gradient(90deg, var(--ms-rarity-dark), var(--ms-rarity-accent), var(--ms-rarity-metal)) top / 100% 5px no-repeat; +} +.mengsan-hand-zone-shuying > .card[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]):not(.infohidden):not(.infoflip) > :is(.name, .top-name) { + background: var(--ms-rarity-accent) !important; color: var(--ms-rarity-ink) !important; + text-shadow: none !important; border-color: var(--ms-rarity-metal) !important; +} +.mengsan-hand-zone-shuying > .card[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]):not(.infohidden):not(.infoflip) > .mengsan-hand-cost-shuying { + background: var(--ms-rarity-dark); color: var(--ms-rarity-metal); border-color: var(--ms-rarity-accent); +} diff --git a/mengsan/ui/intent-display.js b/mengsan/ui/intent-display.js index 83f97d0..22fbc20 100644 --- a/mengsan/ui/intent-display.js +++ b/mengsan/ui/intent-display.js @@ -94,12 +94,17 @@ export function mountEnemyIntent(player, session, assetBase, document, const hasSummon = typeof intent.summon === "string"; const hasSlimed = Number.isInteger(intent.slimed) && intent.slimed > 0; const hasInfection = Number.isInteger(intent.infection) && intent.infection > 0; + const hasWound = Number.isInteger(intent.wound) && intent.wound > 0; + const hasRinging = intent.ringing === true; + const hasPlow = Number.isInteger(intent.plow) && intent.plow > 0; const hasDazed = Number.isInteger(intent.dazed) && intent.dazed > 0; const hasDebuff = typeof intent.debuff === "string" && Number.isInteger(intent.stacks) && intent.stacks > 0; - debuffLine.hidden = !(hasDebuff || hasDazed || hasSlimed || hasInfection); - const debuffSrc = `${assetBase}/assets/intent/${hasDazed || hasSlimed || hasInfection ? "Intent_status_card.png" : "Intent_debuff.png"}`; + debuffLine.hidden = !(hasDebuff || hasDazed || hasSlimed || hasInfection || hasWound || hasRinging); + const debuffSrc = `${assetBase}/assets/intent/${hasDazed || hasSlimed || hasInfection || hasWound ? "Intent_status_card.png" : "Intent_debuff.png"}`; if (debuffIcon.getAttribute("src") !== debuffSrc) debuffIcon.src = debuffSrc; + if (hasRinging) debuffAmount.textContent = "×1"; + if (hasWound) debuffAmount.textContent = `×${intent.wound}`; if (hasInfection) debuffAmount.textContent = `×${intent.infection}`; if (hasSlimed) debuffAmount.textContent = `×${intent.slimed}`; if (hasDazed) debuffAmount.textContent = `×${intent.dazed}`; @@ -114,7 +119,8 @@ export function mountEnemyIntent(player, session, assetBase, document, const buffSrc = `${assetBase}/assets/intent/${hasSummon ? "Intent_summon.png" : "Intent_buff.png"}`; if (buffIcon.getAttribute("src") !== buffSrc) buffIcon.src = buffSrc; blockLine.hidden = !hasBlock; - buffLine.hidden = !(hasStrength || hasSummon); + buffLine.hidden = !(hasStrength || hasSummon || hasPlow); + if (hasPlow) buffAmount.textContent = `撞${Math.max(1, Math.round(intent.plow * player.maxHp / 252))}`; if (hasSummon) buffAmount.textContent = "×1"; if (hasBlock) blockAmount.textContent = String(intent.block); if (hasStrength) { @@ -148,11 +154,14 @@ export function mountEnemyIntent(player, session, assetBase, document, } if (hasDebuff) { const label = intent.debuff === "脆弱" ? "Frail" : - intent.debuff === "易伤" ? "Vulnerable" : intent.debuff === "缩小" ? "Shrink" : intent.debuff === "缠结" ? "Tangled" : "Debuff"; + intent.debuff === "虚弱" ? "Weak" : intent.debuff === "易伤" ? "Vulnerable" : intent.debuff === "缩小" ? "Shrink" : intent.debuff === "缠结" ? "Tangled" : "Debuff"; effects.push(`${label} ×${intent.stacks}`); } + if (hasRinging) effects.push("Ringing ×1"); + if (hasPlow) effects.push(`Plow ${Math.max(1, Math.round(intent.plow * player.maxHp / 252))}`); if (hasSummon) effects.push("Summon: 利齿之眼 ×1"); if (hasDazed) effects.push(`Dazed ×${intent.dazed}`); + if (hasWound) effects.push(`Wound ×${intent.wound}`); if (hasInfection) effects.push(`Infection ×${intent.infection}`); if (hasSlimed) effects.push(`Slimed ×${intent.slimed}`); if (hasBlock) effects.push(`Block ${intent.block}`); @@ -165,7 +174,7 @@ export function mountEnemyIntent(player, session, assetBase, document, effects.push(intent.sleep ? "Sleep" : "Unknown"); } const label = effects.join(", "); - badge.title = hasInfection ? `向敌对角色的个人弃牌堆加入${intent.infection}张感染;不能被打出,回合结束在手牌中每张造成3点非攻击伤害。` : intent.sleep ? "沉睡:本回合不行动,不消耗费用。" : intent.debuff === "缠结" ? "紧绕藤蔓:攻击后施加1回合缠结,攻击牌费用增加1;自身回合结束减少1回合。" : intent.shrink ? "缩小:攻击伤害减少30%,不重复叠加;来源死亡解除。" : hasSlimed ? `黏液:向敌对角色的个人弃牌堆加入${intent.slimed}张黏液;耗1费用、抽1张牌、消耗。` : hasSummon ? "虚幻孢子:召唤1只6生命的利齿之眼。" : + badge.title = hasRinging ? "野兽咆哮:敌对角色下一次自身回合只能主动使用1张牌,不限制响应,回合结束解除。" : hasPlow ? "跺地:获得横冲直撞,受伤且生命不高于其层数时击晕、清零力量并进入第二阶段。" : hasWound ? `肢解:攻击后向存活敌对角色的个人弃牌堆加入${intent.wound}张伤口;不能被打出,无虚无和额外伤害。` : hasInfection ? `向敌对角色的个人弃牌堆加入${intent.infection}张感染;不能被打出,回合结束在手牌中每张造成3点非攻击伤害。` : intent.sleep ? "沉睡:本回合不行动,不消耗费用。" : intent.debuff === "缠结" ? "紧绕藤蔓:攻击后施加1回合缠结,攻击牌费用增加1;自身回合结束减少1回合。" : intent.shrink ? "缩小:攻击伤害减少30%,不重复叠加;来源死亡解除。" : hasSlimed ? `黏液:向敌对角色的个人弃牌堆加入${intent.slimed}张黏液;耗1费用、抽1张牌、消耗。` : hasSummon ? "虚幻孢子:召唤1只6生命的利齿之眼。" : hasDazed ? "牵制:向敌对角色的个人弃牌堆加入3张晕眩;晕眩不能被打出,具有虚无。" : "攻击与负面效果作用于所有存活敌对角色;伤害按各目标状态计算,护甲另行抵扣。"; if (badge.getAttribute("aria-label") !== label) badge.setAttribute("aria-label", label); diff --git a/mengsan/ui/reward-package.js b/mengsan/ui/reward-package.js index 0c6ebbd..69a4ed6 100644 --- a/mengsan/ui/reward-package.js +++ b/mengsan/ui/reward-package.js @@ -72,7 +72,7 @@ export async function chooseRewardPackage(choices, { }; return chooseVictoryOptions([upgrade, relic, { id: "claim-package", label: "领取奖励并继续", - description: "领取必得奖励与已选奖励,未选择的项目视为跳过。", + description: "领取固定奖励与已选奖励,未选择的项目视为跳过。", choose: () => ({ cardId, relicId }), }, { id: "skip-package", label: "跳过", @@ -81,9 +81,9 @@ export async function chooseRewardPackage(choices, { }], { title: "初识的卢", allowSkip: false, rewardLabel: "搜刮 · 战利品列表", - rewardItems: [{ name: `金币 · ${pack.gold}(必得)`, + rewardItems: [{ name: `金币 · ${pack.gold}`, description: "无论选择还是跳过,金币都会结算。" }, - ...fixedRewards.map(reward => ({ ...reward, name: `${reward.name}(必得)` }))], + ...fixedRewards.map(reward => ({ ...reward }))], description: `固定获得 ${pack.gold} 金币与${fixedRewards.map( reward => `【${reward.name}】`).join("、")}。` + "强化与遗物点击后选择,也可跳过。", diff --git a/mengsan/ui/reward-ui.js b/mengsan/ui/reward-ui.js index 4e70616..a04116a 100644 --- a/mengsan/ui/reward-ui.js +++ b/mengsan/ui/reward-ui.js @@ -3,6 +3,9 @@ import { cardCost } from "../battle/combat-rules.js"; import { cardUpgradeRule } from "../cards/upgrades.js"; import { getRelic } from "../relics/definitions.js"; +import { getCardRarity, applyCardRarity } from "../cards/rarity.js"; +import { cleanCardDescription } from "../cards/description.js"; + const element = (tag, className, text, parent) => { const node = document.createElement(tag); node.className = className; @@ -33,11 +36,12 @@ const viewCard = choice => { ? `${lib.assetURL || ""}image/card/${imageName}.png` : null; const type = { basic: "基本牌", trick: "锦囊牌", delay: "延时锦囊", equip: "装备牌" }[info?.type] || "卡牌"; return { - name: get.translation(name), type, image, cost: cardCost(choice.card || { name }), - description: plainText(cardUpgradeRule(name) && info?.cardPrompt + rarity: getCardRarity(name), + name: get.translation(name), type, image, cost: info?.mengsanXCost_shuying ? "X" : cardCost(choice.card || { name }), + description: cleanCardDescription(plainText(cardUpgradeRule(name) && info?.cardPrompt ? info.cardPrompt(choice.card || { name }) : lib.translate[`${name}_info`] || choice.description || - "暂无卡牌介绍。"), + "暂无卡牌介绍。")), }; }; @@ -116,7 +120,8 @@ function chooseTilesDialog(choices, { if (describeChoice) view.description = describeChoice(choice); const card = element("button", "mengsan-card-choice-tile-shuying", null, cards); card.type = "button"; - card.setAttribute("aria-label", `${view.name},${view.type},${view.description}。${actionLabel}`); + if (view.rarity) applyCardRarity(card, cardNameOf(choice)); + card.setAttribute("aria-label", `${view.name},${view.type}${view.rarity ? `,稀有度:${view.rarity.label}` : ""},${view.description}。${actionLabel}`); card.addEventListener("click", () => finish(choice.id)); if (view.cost !== null) { element("span", "mengsan-card-choice-cost-shuying", String(view.cost), card); @@ -129,7 +134,7 @@ function chooseTilesDialog(choices, { image.addEventListener("error", () => image.remove(), { once: true }); } element("span", "mengsan-card-choice-glyph-shuying", view.name.slice(0, 1), artwork); - element("span", "mengsan-card-choice-kind-shuying", view.type, card); + element("span", "mengsan-card-choice-kind-shuying", view.rarity ? `${view.type} · ${view.rarity.label}` : view.type, card); element("span", "mengsan-card-choice-description-shuying", view.description, card); element("span", "mengsan-reward-tile-action-shuying", actionLabel, card); } @@ -240,7 +245,7 @@ export function chooseVictoryOptions(options, { } // Gold and fixed rewards are guaranteed; random loot opens a separate one-of-N selector. -export function chooseBattleReward(choices, { fixedRewards = [] } = {}) { +export function chooseBattleReward(choices, { fixedRewards = [], boss = false, elite = false } = {}) { if (!Array.isArray(choices)) return Promise.resolve(null); const gold = choices.filter(choice => choice.kind === "gold") .reduce((amount, choice) => amount + choice.amount, 0); @@ -250,23 +255,32 @@ export function chooseBattleReward(choices, { fixedRewards = [] } = {}) { const kind = rewards.every(isCardReward) ? "卡牌" : rewards.every(choice => getRelic(choice.relic)) ? "遗物" : "战利品"; options.push({ - id: "choose-reward", label: `${kind}奖励 · ${rewards.length}选1`, - description: `点击查看候选${kind},选择一项领取,或点击跳过。`, - choose: () => chooseRewardDialog(rewards), + id: "choose-reward", label: `${boss && kind === "卡牌" ? "稀有卡牌" : kind}奖励 · ${rewards.length}选1`, + description: elite && kind === "卡牌" + ? "角色专属卡牌,每项候选80%概率为罕见或稀有;可选择一张或跳过。" + : `点击查看候选${kind},选择一项领取,或点击跳过。`, + choose: () => rewards.every(isCardReward) ? chooseCardDialog(rewards, { + title: boss ? "Boss奖励 · 稀有卡牌" : "卡牌奖励", + description: "从本次候选中选择一张加入牌组,也可跳过。", + }) : chooseRewardDialog(rewards), }); } options.push({ id: "skip-loot", label: "跳过", - description: rewards.length ? "跳过本次可选奖励,必得奖励照常领取。" - : "本次没有可选奖励,领取必得奖励后继续。", + description: rewards.length ? "跳过本次可选奖励,其余奖励照常领取。" + : "本次没有可选奖励,领取奖励后继续。", choose: () => null, }); return chooseVictoryOptions(options, { - title: "过关奖励", allowSkip: false, - description: `本关必得 ${gold} 金币。点击奖励选项后择一领取,也可跳过。`, + title: boss ? "Boss奖励" : elite ? "精英奖励" : "过关奖励", allowSkip: false, + description: `本关奖励:${gold} 金币。` + (elite + ? fixedRewards.some(reward => getRelic(reward.relic)) + ? "同时获得一件遗物,跳过卡牌也会领取。" + : "遗物池已全部收集,本次不重复发放。" + : "") + "点击奖励选项后择一领取,也可跳过。", rewardLabel: "搜刮 · 战利品列表", - rewardItems: [{name: `金币 · ${gold}(必得)`, + rewardItems: [{name: `金币 · ${gold}`, description: "无论选择还是跳过,金币都会结算。"}, - ...fixedRewards.map(reward => ({ ...reward, name: `${reward.name}(必得)` }))], + ...fixedRewards.map(reward => ({ ...reward }))], }); } diff --git a/mengsan/ui/style.css b/mengsan/ui/style.css index bf54a6e..c4baa14 100644 --- a/mengsan/ui/style.css +++ b/mengsan/ui/style.css @@ -1231,22 +1231,22 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; } .mengsan-deck-dialog-shuying .ms-deck-button[aria-pressed="true"] { background: #dcb578; color: #241a10; border-color: #f9dda6; } .mengsan-deck-dialog-shuying .ms-deck-summary, .mengsan-deck-dialog-shuying .ms-deck-footer { color: var(--ms-muted); font-size: 13px; flex-shrink: 0; } .mengsan-deck-dialog-shuying .ms-deck-body { flex: 1; min-height: 0; overflow: auto; overscroll-behavior: contain; padding: 5px; scrollbar-gutter: stable; } -.mengsan-deck-dialog-shuying .ms-deck-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 16px; align-items: stretch; } -.mengsan-deck-dialog-shuying .ms-deck-card { display: flex; flex-direction: column; gap: 8px; width: 100%; min-width: 0; padding: 8px; border: 1px solid #9e7b49; background: #e4d0aa; color: #2e2419; text-align: left; box-shadow: 0 4px 10px #0005; } +.mengsan-deck-dialog-shuying .ms-deck-grid { display: grid; grid-template-columns: repeat(auto-fill, 176px); gap: 16px; align-items: start; align-content: start; justify-content: start; } +.mengsan-deck-dialog-shuying .ms-deck-card { display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 8px; width: 100%; min-width: 0; height: 400px; overflow: hidden; padding: 8px; border: 1px solid #9e7b49; background: #e4d0aa; color: #2e2419; text-align: left; box-shadow: 0 4px 10px #0005; } .mengsan-deck-dialog-shuying .ms-deck-card:hover { border-color: #ffe3a5; background: #f0ddb7; } .mengsan-deck-dialog-shuying .ms-deck-face { display: flex; flex-direction: column; gap: 5px; min-width: 0; color: #2e2419; background: #e4d0aa; } .mengsan-deck-dialog-shuying .ms-deck-corner { font-size: 14px; font-weight: 700; } .mengsan-deck-dialog-shuying .ms-deck-red { color: #9b2920; } -.mengsan-deck-dialog-shuying .ms-deck-art { position: relative; display: grid; place-items: center; height: 116px; overflow: hidden; background: linear-gradient(145deg, #8a765b, #c0aa83); border: 1px solid #8b734f; border-radius: 3px; } +.mengsan-deck-dialog-shuying .ms-deck-art { position: relative; display: grid; place-items: center; width: 100%; height: auto; min-height: 0; aspect-ratio: 5 / 7; align-self: stretch; flex-shrink: 0; overflow: hidden; background: linear-gradient(145deg, #8a765b, #c0aa83); border: 1px solid #8b734f; border-radius: 3px; } .mengsan-deck-dialog-shuying .ms-deck-image { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; } .mengsan-deck-dialog-shuying .ms-deck-glyph { font: 700 28px "KaiTi", serif; text-align: center; } .mengsan-deck-dialog-shuying .ms-deck-name { font: 700 21px/1.4 "KaiTi", serif; } .mengsan-deck-dialog-shuying .ms-deck-meta, .mengsan-deck-dialog-shuying .ms-deck-tags { font-size: 12px; } .mengsan-deck-dialog-shuying .ms-deck-tags { color: #813d18; font-weight: 700; } -.mengsan-deck-dialog-shuying .ms-deck-excerpt { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 12px; line-height: 1.5; } +.mengsan-deck-dialog-shuying .ms-deck-excerpt { flex: 0 0 36px; height: 36px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: 12px; line-height: 1.5; } .mengsan-deck-dialog-shuying .ms-deck-detail-content { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 28px; margin-top: 18px; } .mengsan-deck-dialog-shuying .ms-deck-detail-content > .ms-deck-face { padding: 14px; border-radius: 6px; align-self: start; } -.mengsan-deck-dialog-shuying .ms-deck-detail-content .ms-deck-art { height: 190px; } +.mengsan-deck-dialog-shuying .ms-deck-detail-content .ms-deck-art { width: 100%; height: auto; aspect-ratio: 5 / 7; } .mengsan-deck-dialog-shuying .ms-deck-detail-copy { display: flex; flex-direction: column; gap: 14px; min-width: 0; overflow-wrap: anywhere; } .mengsan-deck-dialog-shuying .ms-deck-detail-copy h3 { font-size: 26px; } .mengsan-deck-dialog-shuying .ms-deck-rules { white-space: pre-line; line-height: 1.85; } @@ -1255,7 +1255,7 @@ dialog.mengsan-deck-dialog-shuying::backdrop { background: #080706b8; } .mengsan-deck-dialog-shuying .ms-deck-empty { padding: 24px; grid-column: 1 / -1; } @media (max-width: 600px) { dialog.mengsan-deck-dialog-shuying { width: calc(100vw - 12px); height: calc(100dvh - 12px); max-height: calc(100dvh - 12px); padding: 14px; gap: 10px !important; } - .mengsan-deck-dialog-shuying .ms-deck-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } + .mengsan-deck-dialog-shuying .ms-deck-grid { grid-template-columns: repeat(2, minmax(0, 176px)); gap: 10px; } .mengsan-deck-dialog-shuying .ms-deck-detail-content { grid-template-columns: minmax(0, 1fr); gap: 18px; } .mengsan-deck-dialog-shuying .ms-deck-detail-content > .ms-deck-face { width: 190px; justify-self: center; } .mengsan-deck-dialog-shuying .ms-deck-summary { font-size: 12px; } @@ -1445,3 +1445,117 @@ body.mengsan-battle-ui-shuying :is( .mengsan-deck-dialog-shuying .ms-catalog-field:not(:first-child) { flex: 1 1 90px; } .mengsan-deck-dialog-shuying .ms-catalog-filters > button { padding: 8px 10px; } } + +/* Same stable card geometry in held deck, encyclopedia and personal battle piles. */ +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-face { display: grid; grid-template-rows: 21px auto 30px 18px 18px; gap: 5px; width: 100%; min-width: 0; flex-shrink: 0; align-items: stretch; } +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-corner, +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-name, +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-meta, +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-tags { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-art { width: 100%; height: auto; aspect-ratio: 5 / 7; } +@media (max-width: 600px) { + .mengsan-deck-dialog-shuying .ms-deck-grid .ms-deck-card { height: 374px; } +} + +/* Compact browsing only; full descriptions live in the existing detail view. */ +.mengsan-deck-dialog-shuying .ms-library-grid { grid-template-columns: repeat(auto-fill, 112px); gap: 8px; justify-content: space-between; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-card { position: relative; height: var(--ms-card-height, 218px); padding: 6px; gap: 3px; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-face { flex: 1; min-height: 0; grid-template-rows: 18px minmax(0, 1fr) 20px; gap: 2px; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-art { width: var(--ms-card-art-width, 98px); max-width: 100%; height: auto; aspect-ratio: 5 / 7; justify-self: center; align-self: center; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-corner { font-size: 12px; line-height: 18px; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-name { font-size: 16px; line-height: 20px; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-meta { display: none; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-tags { position: absolute; top: 6px; right: 6px; max-width: 54px; font-size: 12px; line-height: 18px; background: #e4d0aa; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-excerpt { flex: 0 0 18px; height: 18px; line-height: 18px; -webkit-line-clamp: 1; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-card { contain: layout paint style; } +.mengsan-deck-dialog-shuying .ms-library-grid .ms-deck-card:focus-visible { outline-offset: -3px; } +.mengsan-deck-dialog-shuying .ms-deck-body { overflow-anchor: none; scroll-behavior: auto; } +@media (max-width: 600px) { + .mengsan-deck-dialog-shuying .ms-library-grid { grid-template-columns: repeat(2, minmax(0, 112px)); } +} + +@media (max-width: 600px) { + dialog.mengsan-deck-dialog-shuying { padding: 10px; gap: 6px !important; } + .mengsan-deck-dialog-shuying .ms-deck-title { font-size: 22px; } + .mengsan-deck-dialog-shuying .ms-deck-eyebrow { display: none; } + .mengsan-deck-dialog-shuying .ms-catalog-field > span { display: none; } + .mengsan-deck-dialog-shuying .ms-catalog-field:not(:first-child) { flex: 1 1 64px; } + .mengsan-deck-dialog-shuying .ms-catalog-filters > button { padding: 8px; font-size: 13px; } + .mengsan-deck-dialog-shuying .ms-catalog-select { font-size: 13px; padding: 8px 6px; } + .mengsan-deck-dialog-shuying .ms-deck-footer { font-size: 12px; line-height: 18px; } +} + +/* Same transparent infection frame in held deck, atlas, piles and detail. */ +.mengsan-deck-dialog-shuying .ms-deck-art[data-mengsan-overlay="infection"]::before { + content: ""; + z-index: 1; + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + border-radius: inherit; + pointer-events: none; + background-image: url("../assets/card-overlays/infection-sprite.webp"); + /* 30 source frames, six columns/five rows, 192x260 per cell. */ + background-size: 600% 500%; + background-position: 0% 0%; + background-repeat: no-repeat; + animation: mengsan-infection-frames-shuying 2s steps(1, end) infinite; +} +@keyframes mengsan-infection-frames-shuying { + 0% { background-position: 0% 0%; } + 3.333333% { background-position: 20% 0%; } + 6.666667% { background-position: 40% 0%; } + 10% { background-position: 60% 0%; } + 13.333333% { background-position: 80% 0%; } + 16.666667% { background-position: 100% 0%; } + 20% { background-position: 0% 25%; } + 23.333333% { background-position: 20% 25%; } + 26.666667% { background-position: 40% 25%; } + 30% { background-position: 60% 25%; } + 33.333333% { background-position: 80% 25%; } + 36.666667% { background-position: 100% 25%; } + 40% { background-position: 0% 50%; } + 43.333333% { background-position: 20% 50%; } + 46.666667% { background-position: 40% 50%; } + 50% { background-position: 60% 50%; } + 53.333333% { background-position: 80% 50%; } + 56.666667% { background-position: 100% 50%; } + 60% { background-position: 0% 75%; } + 63.333333% { background-position: 20% 75%; } + 66.666667% { background-position: 40% 75%; } + 70% { background-position: 60% 75%; } + 73.333333% { background-position: 80% 75%; } + 76.666667% { background-position: 100% 75%; } + 80% { background-position: 0% 100%; } + 83.333333% { background-position: 20% 100%; } + 86.666667% { background-position: 40% 100%; } + 90% { background-position: 60% 100%; } + 93.333333% { background-position: 80% 100%; } + 96.666667% { background-position: 100% 100%; } + 100% { background-position: 100% 100%; } +} +@media (prefers-reduced-motion: reduce) { + .mengsan-deck-dialog-shuying .ms-deck-art[data-mengsan-overlay="infection"]::before { + animation: none; + background-position: 0% 0%; + } +} + +/* 配色统一来自 cards/rarity.js;pending 不改变旧卡面,尚未分配不等于普通。 */ +.mengsan-deck-dialog-shuying .ms-deck-face[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]), +.mengsan-card-choice-tile-shuying[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) { + border-color: var(--ms-rarity-metal); box-shadow: inset 0 0 0 2px var(--ms-rarity-accent); + background: linear-gradient(145deg, var(--ms-rarity-dark), #231f1b); +} +.mengsan-deck-dialog-shuying .ms-deck-face[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) :is(.ms-deck-name, .ms-deck-corner), +.mengsan-card-choice-tile-shuying[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) :is(.mengsan-card-choice-name-shuying, .mengsan-card-choice-kind-shuying) { + background: var(--ms-rarity-accent); color: var(--ms-rarity-ink); border-color: var(--ms-rarity-metal); +} +.mengsan-deck-dialog-shuying .ms-deck-face[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) .ms-deck-art, +.mengsan-card-choice-tile-shuying[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) .mengsan-card-choice-art-shuying { border-color: var(--ms-rarity-accent); } +.mengsan-deck-dialog-shuying .ms-deck-face[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]) :is(.ms-deck-meta, .ms-deck-tags) { color: #f6ebd6; } +.mengsan-deck-dialog-shuying .ms-deck-rarity-label { color: #d7c5a2; } +.mengsan-card-choice-tile-shuying[data-mengsan-rarity]:not([data-mengsan-rarity="pending"]):hover { border-color: var(--ms-rarity-accent); } diff --git a/mengsan/ui/virtual-card-grid.js b/mengsan/ui/virtual-card-grid.js new file mode 100644 index 0000000..db56e28 --- /dev/null +++ b/mengsan/ui/virtual-card-grid.js @@ -0,0 +1,90 @@ +// 保留完整滚动高度,只挂载可见行与上下各两行;滚动时不重建仍可见的卡牌。 +export function createVirtualCardGrid(viewport, grid, createCard) { + const mounted = new Map(); + let records = [], columns = 1, height = 218, gap = 8, padding = 5, viewportHeight = 0; + let frame = 0, disposed = false; + function draw() { + if (disposed || grid.hidden || !viewportHeight) return; + const stride = height + gap; + const offset = viewport.scrollTop; + const firstRow = Math.max(0, Math.floor((offset - padding) / stride) - 2); + const lastRow = Math.ceil((offset + viewportHeight - padding) / stride) + 2; + const first = firstRow * columns, end = Math.min(records.length, lastRow * columns); + // 不因滚动删除正在使用键盘操作的卡牌。 + for (const [index, node] of mounted) { + if ((index < first || index >= end) && node !== document.activeElement) { + node.remove(); mounted.delete(index); + } + } + for (let index = first; index < end; index++) { + if (mounted.has(index)) continue; + const node = createCard(records[index], index); + node.dataset.cardIndex = String(index); + node.style.gridRow = String(Math.floor(index / columns) + 1); + node.style.gridColumn = String(index % columns + 1); + // 保持 DOM 顺序与屏幕顺序一致,方便键盘与读屏浏览。 + let next = null; + for (const [otherIndex, other] of mounted) { + if (otherIndex > index && (!next || otherIndex < Number(next.dataset.cardIndex))) next = other; + } + grid.insertBefore(node, next); mounted.set(index, node); + } + } + function schedule() { + if (!frame && !disposed) frame = requestAnimationFrame(() => { frame = 0; draw(); }); + } + function layout(cardHeight = height) { + height = cardHeight; + if (disposed || grid.hidden || !grid.clientWidth) return; + const style = getComputedStyle(grid); + viewportHeight = viewport.clientHeight; + const nextColumns = Math.max(1, style.gridTemplateColumns.split(/\s+/).filter(track => parseFloat(track) > 0).length); + gap = parseFloat(style.rowGap) || 0; + padding = parseFloat(getComputedStyle(viewport).paddingTop) || 0; + columns = nextColumns; + const rows = Math.ceil(records.length / columns); + grid.style.gridTemplateRows = rows ? `repeat(${rows}, ${height}px)` : "none"; + for (const [index, node] of mounted) { + node.style.gridRow = String(Math.floor(index / columns) + 1); + node.style.gridColumn = String(index % columns + 1); + } + draw(); + } + function focus(index) { + if (!Number.isInteger(index) || index < 0 || index >= records.length || disposed) return; + const top = padding + Math.floor(index / columns) * (height + gap); + if (top < viewport.scrollTop) viewport.scrollTop = top; + else if (top + height > viewport.scrollTop + viewportHeight) { + viewport.scrollTop = top + height - viewportHeight; + } + draw(); mounted.get(index)?.focus({ preventScroll: true }); + } + function keydown(event) { + const card = event.target.closest("[data-card-index]"); + if (!card || !grid.contains(card) || event.altKey || event.ctrlKey || event.metaKey) return; + const index = Number(card.dataset.cardIndex); + const delta = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -columns, ArrowDown: columns, + Tab: event.shiftKey ? -1 : 1 }[event.key]; + if (delta == null) return; + const target = index + delta; + if (target < 0 || target >= records.length) return; + event.preventDefault(); focus(target); + } + viewport.addEventListener("scroll", schedule, { passive: true }); + grid.addEventListener("keydown", keydown); + return { + setRecords(value) { + // 调用方更新筛选栏后再统一量尺寸,避免用旧高度先生成一批预览。 + records = value; mounted.clear(); grid.replaceChildren(); + }, + layout, + focus, + resume() { draw(); }, + dispose() { + disposed = true; cancelAnimationFrame(frame); + viewport.removeEventListener("scroll", schedule); + grid.removeEventListener("keydown", keydown); + mounted.clear(); records = []; + }, + }; +} diff --git a/tools/generate_manifest.bat b/tools/generate_manifest.bat index c29e309..1fa740e 100644 --- a/tools/generate_manifest.bat +++ b/tools/generate_manifest.bat @@ -1,11 +1,16 @@ @echo off setlocal cd /d "%~dp0.." -set /p VERSION=Input version, for example 2.0.0.4: -if "%VERSION%"=="" ( +set /p "SHUYING_RELEASE_VERSION=Input version, for example 2.0.0.4: " +if not defined SHUYING_RELEASE_VERSION ( echo Version cannot be empty. pause exit /b 1 ) -python tools\generate_manifest.py --root . --version "%VERSION%" --out dist\manifest.json +python tools\prepare_release_manifest.py --root . +if errorlevel 1 ( + echo Release preparation failed. Commit all source changes first. + pause + exit /b 1 +) pause diff --git a/tools/generate_manifest.py b/tools/generate_manifest.py index 9af93c3..7ec67f0 100644 --- a/tools/generate_manifest.py +++ b/tools/generate_manifest.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import hashlib import json +import os from datetime import datetime, timezone from pathlib import Path @@ -17,9 +18,17 @@ EXCLUDE_DIRS = { "tools", "__pycache__", "verification", + "node_modules", + "Slay the Spire 2", + ".godot", + ".codex", + ".agents", + ".aws", + "logs", } -EXCLUDE_FILES = {".gitignore", "log.txt"} -EXCLUDE_EXTENSIONS = {".md", ".mjs"} +EXCLUDE_PATHS = {("mengsan", "assets", "sts2")} +EXCLUDE_FILES = {".gitignore", "log.txt", "Thumbs.db", "Desktop.ini", ".DS_Store"} +EXCLUDE_EXTENSIONS = {".md", ".mjs", ".cjs", ".log", ".pyc", ".tmp", ".bak", ".swp", ".swo"} def sha256_file(path: Path) -> str: @@ -35,16 +44,26 @@ def should_include(path: Path, root: Path) -> bool: return ( path.is_file() and relative.name not in EXCLUDE_FILES + and not relative.name.startswith(".env") and path.suffix.lower() not in EXCLUDE_EXTENSIONS and not any(part in EXCLUDE_DIRS for part in relative.parts) + and not any(relative.parts[:len(prefix)] == prefix for prefix in EXCLUDE_PATHS) ) def scan_files(root: Path) -> list[Path]: - return sorted( - (path for path in root.rglob("*") if should_include(path, root)), - key=lambda path: str(path.relative_to(root)).replace("\\", "/"), - ) + files = [] + for directory, folders, names in os.walk(root): + parent = Path(directory) + folders[:] = [name for name in folders if name not in EXCLUDE_DIRS + and (parent / name).relative_to(root).parts not in EXCLUDE_PATHS] + for name in names: + path = parent / name + if should_include(path, root): + if not path.resolve().is_relative_to(root): + raise ValueError(f"file escapes source root: {path}") + files.append(path) + return sorted(files, key=lambda path: str(path.relative_to(root)).replace("\\", "/")) def normalize_path(path: str) -> str: @@ -329,7 +348,7 @@ def main() -> None: root, version, config, previous_manifest ) output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8", newline="\n") module_output = output.parent / "modules" for name, module_manifest in modules.items(): module_path = module_output / f"{name}.json" @@ -337,6 +356,7 @@ def main() -> None: module_path.write_text( json.dumps(module_manifest, ensure_ascii=False, indent=2), encoding="utf-8", + newline="\n", ) for name, module_manifest in legacy_modules.items(): module_path = module_output / f"{name}.legacy.json" @@ -344,6 +364,7 @@ def main() -> None: module_path.write_text( json.dumps(module_manifest, ensure_ascii=False, indent=2), encoding="utf-8", + newline="\n", ) if module_output.is_dir(): expected = {f"{name}.json" for name in modules} diff --git a/tools/prepare_release_manifest.py b/tools/prepare_release_manifest.py new file mode 100644 index 0000000..51a61cb --- /dev/null +++ b/tools/prepare_release_manifest.py @@ -0,0 +1,85 @@ +"""Generate release manifests from committed Git bytes, without creating a tag.""" +from __future__ import annotations + +import argparse +import os +import re +import shutil +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + + +def git(root: Path, *args: str) -> bytes: + return subprocess.run(["git", "-c", "core.autocrlf=false", *args], cwd=root, + check=True, stdout=subprocess.PIPE).stdout + + +def prepare(root: Path, version: str) -> None: + if not re.fullmatch(r"\d+\.\d+\.\d+\.\d+(?:-rc\.\d+)?", version): + raise ValueError("Invalid release version") + repository = Path(git(root, "rev-parse", "--show-toplevel").decode("utf-8").strip()).resolve() + if repository != root: + raise ValueError("Run this tool from the extension repository") + rows = git(root, "status", "--porcelain=v1", "-z", "--untracked-files=all").decode("utf-8").split("\0") + dirty = [] + i = 0 + while i < len(rows): + row = rows[i] + if row: + path = row[3:] + if not path.startswith("dist/"): + dirty.append(path) + if "R" in row[:2] or "C" in row[:2]: + i += 1 + if i < len(rows) and not rows[i].startswith("dist/"): + dirty.append(rows[i]) + i += 1 + if dirty: + raise ValueError("Commit source and new runtime files first; pending files: " + ", ".join(dirty[:8])) + + with tempfile.TemporaryDirectory(prefix="shuying-release-") as temporary: + workspace = Path(temporary) + archive = workspace / "source.zip" + stage = workspace / "source" + git(root, "archive", "--format=zip", f"--output={archive}", "HEAD") + with zipfile.ZipFile(archive) as source: + # Git trees cannot contain traversal paths; still check before extracting. + for item in source.infolist(): + path = stage / item.filename + if not path.resolve().is_relative_to(stage.resolve()): + raise ValueError("Unsafe archive path") + source.extractall(stage) + subprocess.run([sys.executable, str(stage / "tools/generate_manifest.py"), + "--root", str(stage), "--version", version], check=True) + subprocess.run([sys.executable, str(stage / "tools/verify_manifest.py"), + "--root", str(stage), "--version", version], check=True) + output = root / "dist" + output.mkdir(exist_ok=True) + shutil.copyfile(stage / "dist/manifest.json", output / "manifest.json") + components = output / "modules" + components.mkdir(exist_ok=True) + generated = list((stage / "dist/modules").glob("*.json")) + expected = {path.name for path in generated} + for path in generated: + shutil.copyfile(path, components / path.name) + for stale in components.glob("*.json"): + if stale.name not in expected: + stale.unlink() + print("Manifests are ready. Commit dist/manifest.json and dist/modules/*.json before creating the release tag.") + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--version", default=os.environ.get("SHUYING_RELEASE_VERSION")) + args = parser.parse_args() + if not args.version: + parser.error("--version is required") + prepare(Path(args.root).resolve(), args.version) + + +if __name__ == "__main__": + main() diff --git a/tools/verify_manifest.py b/tools/verify_manifest.py new file mode 100644 index 0000000..fb97b22 --- /dev/null +++ b/tools/verify_manifest.py @@ -0,0 +1,104 @@ +"""Read-only validation of the manifests committed in a release tag.""" +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + +from generate_manifest import build_manifest, load_config, preserve_removals, scan_files + + +def read_json(path: Path) -> dict: + if not path.is_file(): + raise ValueError(f"Missing committed manifest: {path}. Generate and commit manifests before tagging.") + value = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + raise ValueError(f"Manifest must be an object: {path}") + return value + + +def compare_manifest(actual: dict, expected: dict, label: str) -> None: + # generated_at and the export directory's name do not affect installed bytes. + fields = ("version", "algorithm", "update_channels", "file_count", "size_bytes", + "folder_sha256", "core", "assets", "remove", "removeDirectories", "files") + if "module_id" in expected: + fields += ("module_id",) + for field in fields: + if actual.get(field) != expected.get(field): + raise ValueError(f"Stale or invalid {label}: {field}. Regenerate manifests before tagging.") + if actual.get("tree", {}).get("children") != expected["tree"]["children"]: + raise ValueError(f"Invalid {label}: tree") + + +def verify_imports(root: Path, names: set[str]) -> None: + patterns = ( + re.compile(r'''\b(?:import|export)\s+(?:[^;]*?\bfrom\s*)?["'](\.[^"']+)["']'''), + re.compile(r'''\bimport\s*\(\s*["'](\.[^"']+)["']'''), + ) + for name in sorted(names): + if not name.endswith(".js"): + continue + path = root / name + source = path.read_text(encoding="utf-8") + for pattern in patterns: + for match in pattern.finditer(source): + target = (path.parent / match[1]).resolve() + if target.is_relative_to(root): + dependency = target.relative_to(root).as_posix() + if dependency not in names or not target.is_file(): + raise ValueError(f"Missing packaged import: {name} -> {match[1]}. Commit all runtime files before releasing.") + + +def verify(root: Path, version: str) -> None: + if not re.fullmatch(r"\d+\.\d+\.\d+\.\d+(?:-rc\.\d+)?", version): + raise ValueError("Invalid release version") + source = (root / "extension.js").read_text(encoding="utf-8") + match = re.search(r'shuYingLocalVersion\s*=\s*"([^"]+)"', source) + if not match or match[1] != version: + raise ValueError(f"extension.js version must be {version}") + for path in scan_files(root): + if not path.resolve().is_relative_to(root): + raise ValueError(f"Release file escapes source root: {path}") + + output = root / "dist/manifest.json" + actual = read_json(output) + config = preserve_removals(load_config(root, "tools/manifest_config.json"), output) + expected, modules, legacy = build_manifest(root, version, config, actual) + compare_manifest(actual, expected, "base manifest") + if actual.get("modules") != expected.get("modules"): + raise ValueError("Invalid module descriptors") + for name, expected_module in modules.items(): + component = read_json(root / actual["modules"][name]["manifest"]) + compare_manifest(component, expected_module, f"{name} manifest") + packaged_files = set(actual["files"]) + for component in modules.values(): + packaged_files.update(component["files"]) + verify_imports(root, packaged_files) + for name, baseline in legacy.items(): + snapshot = read_json(root / actual["modules"][name]["legacy_manifest"]) + files = snapshot.get("files") + prefix = config["modules"][name]["root"] + "/" + if (snapshot.get("version") != version or snapshot.get("module_id") != name + or snapshot.get("snapshot") != "legacy" or not isinstance(files, list) + or any(not isinstance(path, str) or not path.startswith(prefix) + or "\\" in path or ":" in path + or any(part in {"", ".", ".."} for part in path.split("/")) + for path in files) + or len(files) != len(set(files)) + or not set(baseline["files"]).issubset(files)): + raise ValueError(f"Invalid or incomplete {name} legacy ownership snapshot") + print(f"Verified {version}: {actual['file_count']} base files; " + + ", ".join(f"{name}: {item['file_count']} files" for name, item in modules.items())) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", default=".") + parser.add_argument("--version", required=True) + args = parser.parse_args() + verify(Path(args.root).resolve(), args.version) + + +if __name__ == "__main__": + main() diff --git a/update.js b/update.js index f871df9..ebedf81 100644 --- a/update.js +++ b/update.js @@ -4,6 +4,7 @@ const giteaRepositoryApi = "https://gitea.diuse.work/api/v1/repos/Diuse/noname-s const githubRepositoryApi = "https://api.github.com/repos/Shu-Ying/noname-shuying"; const githubRawBase = "https://raw.githubusercontent.com/Shu-Ying/noname-shuying"; const coreTempDir = "extension/术樱包/.update_tmp"; +let fileMutationUncertain = false; function parseVersion(version) { const match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?$/.exec(version); @@ -275,6 +276,10 @@ function validateManifest(manifest, version, moduleId = "") { getManifestAssetFiles(manifest, coreFiles); getManifestRemoveFiles(manifest); getManifestRemoveDirectories(manifest); + if (moduleId && [...getManifestRemoveFiles(manifest), ...getManifestRemoveDirectories(manifest)] + .some(file => !file.startsWith(`${moduleId}/`))) { + throw new Error(`校验清单包含越界的 ${moduleId} 模块删除项`); + } return manifest; } @@ -446,66 +451,206 @@ async function verifyFile(path, data, manifest) { } function checkFile(path) { - return new Promise((resolve, reject) => { - game.checkFile(path, resolve, reject); + assertLocalPath(path); + return fileOperation(`检查文件:${path}`, (resolve, reject) => { + if (lib.node?.fs && typeof window.__dirname == "string") { + lib.node.fs.stat(lib.node.path.join(window.__dirname, path), (error, stat) => { + if (error?.code == "ENOENT") resolve(-1); + else if (error) reject(error); + else resolve(stat.isFile() ? 1 : 0); + }); + } + else if (hasCordovaFiles()) { + cordovaEntry(path).then(entry => resolve(entry.isFile ? 1 : 0), error => { + if (error.code == 1) resolve(-1); + else reject(error); + }); + } + else if (typeof game.checkFile == "function") game.checkFile(path, resolve, reject); + else reject(new Error("当前环境不支持检查文件")); }); } async function safeCheckFile(path) { - try { - return await checkFile(path); + // An unreadable file must never be treated as a missing file during replacement. + return await checkFile(path); +} + +function assertLocalPath(path) { + if (!isSafeManifestPath(path) || !path.startsWith("extension/术樱包/")) { + throw new Error(`拒绝访问扩展目录外的路径:${path}`); } - catch (error) { - console.error("检查文件失败,按缺失处理:", path, error); - return -1; +} + +function operationError(value) { + if (!value) return null; + if (value instanceof Error) return value; + const error = value.target?.error || value.error; + if (error) return operationError(error); + if (typeof value == "string" || value.code || value.message) { + return Object.assign(new Error(value.message || (value.code ? `文件操作错误:${value.code}` : String(value))), { code: value.code }); } + return null; // A successful Cordova writeend event is not an error. +} + +function assertMutationReady() { + if (fileMutationUncertain) { + const error = new Error("上次文件操作中断,结果尚未确认,请重启游戏后再修复或卸载"); + error.operationUncertain = true; + throw error; + } +} + +function fileOperation(label, start, mutation = false) { + if (mutation) { + try { assertMutationReady(); } + catch (error) { return Promise.reject(error); } + } + return new Promise((resolve, reject) => { + let settled = false; + const finish = (error, result) => { + if (settled) return; + settled = true; + clearTimeout(timer); + if (error) { + const failure = operationError(error) || new Error(`${label}失败`); + if (mutation && !lib.node && !hasCordovaFiles() + && (failure instanceof TypeError || failure instanceof SyntaxError)) failure.operationUncertain = true; + if (mutation && failure.operationUncertain) fileMutationUncertain = true; + reject(failure); + } + else resolve(result); + }; + const timer = setTimeout(() => { + const error = new Error(`${label}超时,请重启后检查或修复文件`); + error.operationUncertain = mutation; + if (mutation) fileMutationUncertain = true; + finish(error); + }, 60000); + try { start(result => finish(null, result), error => finish(error || new Error(`${label}失败`))); } + catch (error) { finish(error); } + }); +} + +function hasCordovaFiles() { + return !!lib.device && typeof window.resolveLocalFileSystemURL == "function"; +} + +function cordovaEntry(path, create = false) { + return fileOperation(`打开文件:${path}`, (resolve, reject) => { + const root = localStorage.getItem("noname_inited"); + if (!root) throw new Error("无法确定游戏文件目录"); + window.resolveLocalFileSystemURL(root, entry => { + try { entry.getFile(path, { create }, resolve, reject); } + catch (error) { reject(error); } + }, reject); + }, create); } function readFile(path) { - return new Promise((resolve, reject) => { - game.readFile(path, resolve, reject); + assertLocalPath(path); + return fileOperation(`读取文件:${path}`, (resolve, reject) => { + if (hasCordovaFiles()) { + cordovaEntry(path).then(entry => entry.file(file => { + try { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(reader.error); + reader.readAsArrayBuffer(file); + } + catch (error) { reject(error); } + }, reject)).catch(reject); + } + else game.readFile(path, resolve, reject); }); } -function writeFile(data, path, name) { - return new Promise((resolve, reject) => { - game.writeFile(data, path, name, result => { - if (result instanceof Error) { - reject(result); - } - else { - resolve(result); - } +async function writeFile(data, path, name) { + const target = `${path.replace(/\/$/, "")}/${name}`; + assertLocalPath(target); + await fileOperation(`写入文件:${target}`, (resolve, reject) => { + if (hasCordovaFiles()) { + cordovaEntry(target, true).then(entry => entry.createWriter(writer => { + let truncated = false; + writer.onerror = () => reject(writer.error || new Error("写入失败")); + writer.onwriteend = () => { + if (writer.error) { reject(writer.error); return; } + if (truncated) { resolve(); return; } + truncated = true; + try { writer.write(new Blob([toUint8Array(data)])); } + catch (error) { reject(error); } + }; + try { writer.truncate(0); } + catch (error) { reject(error); } + }, reject)).catch(reject); + } + else if (!lib.node) { + // The browser adapter omits fetch's rejection callback; use its API directly. + fetch("/writeFile", { + method: "post", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data: typeof data == "string" ? data : Array.from(toUint8Array(data)), path: target }), + }).then(response => { + if (!response.ok) throw new Error(`文件服务请求失败:${response.status}`); + return response.json(); + }).then(result => { + if (result?.success) resolve(); + else reject(new Error(result?.errorMsg || "文件服务写入失败")); + }).catch(error => { + // Losing the response cannot prove that the server stopped writing. + error.operationUncertain = true; + reject(error); + }); + } + else game.writeFile(data, path, name, result => { + const error = operationError(result); + if (error) reject(error); + else resolve(); }); - }); + }, true); + const actual = toUint8Array(await readFile(target)); + const expected = toUint8Array(data); + let equal = actual.length == expected.length; + for (let i = 0; equal && i < actual.length; i++) equal = actual[i] == expected[i]; + if (!equal) { + throw new Error(`文件落盘校验失败:${target}`); + } } -function removeFile(path) { - return new Promise(resolve => { - if (typeof game.removeFile != "function") { - resolve(); - return; +async function removeFile(path) { + assertMutationReady(); + assertLocalPath(path); + const exists = await checkFile(path); + if (exists == -1) return; + if (exists != 1) throw new Error(`待删除路径不是文件:${path}`); + await fileOperation(`删除文件:${path}`, (resolve, reject) => { + if (hasCordovaFiles()) { + cordovaEntry(path).then(entry => entry.remove(resolve, reject)).catch(reject); } - game.removeFile(path, () => resolve()); - }); + else { + if (typeof game.removeFile != "function") throw new Error("当前环境不支持删除文件"); + game.removeFile(path, error => error ? reject(error) : resolve(), reject); + } + }, true); + if (await checkFile(path) != -1) throw new Error(`文件删除后仍存在:${path}`); } -function removeDir(path) { - return new Promise(resolve => { - if (typeof game.removeDir != "function") { - resolve({ removed: false, error: new Error("当前环境不支持删除文件夹") }); - return; - } - game.removeDir( - path, - () => resolve({ removed: true }), - error => resolve({ removed: false, error }) - ); - }); +async function removeDir(path) { + assertLocalPath(path); + try { + await fileOperation(`删除目录:${path}`, (resolve, reject) => { + if (typeof game.removeDir != "function") throw new Error("当前环境不支持删除文件夹"); + game.removeDir(path, resolve, reject); + }, true); + return { removed: true }; + } + catch (error) { + if (error.operationUncertain) throw error; + return { removed: false, error }; + } } function createDir(path) { - return new Promise((resolve, reject) => { + return fileOperation(`创建目录:${path}`, (resolve, reject) => { game.createDir(path, resolve, reject); }); } @@ -555,79 +700,10 @@ async function removeManifestOldPaths(shuYing, manifest) { } async function downloadCoreFiles(shuYing, manifest) { - if (typeof game.readFile != "function" || typeof game.writeFile != "function") { - throw new Error("当前环境不支持安全替换核心文件"); - } - - let coreFiles = []; - - try { - ensureProgressNode(shuYing); - setProgress(shuYing, { title: "核心文件修复", status: "获取校验清单", current: 0, total: 1 }); - - manifest = manifest || await getManifest(); - coreFiles = getManifestCoreFiles(manifest); - if (!coreFiles.length) throw new Error("校验清单缺少 core 核心文件列表"); - - await createDir(`${coreTempDir}/`); - await createFolders(`${coreTempDir}/`, coreFiles); - await createFolders("extension/术樱包/", coreFiles); - - let progress = 0; - const fileData = {}; - for (const file of coreFiles) { - setProgress(shuYing, { title: "核心文件修复", status: "下载并校验", file, current: progress, total: coreFiles.length * 2 }); - fileData[file] = await downloadFile(file, `${coreTempDir}/${file}`, manifest); - progress++; - } - - const backupData = {}; - for (const file of coreFiles) { - try { - backupData[file] = await readFile(`extension/术樱包/${file}`); - } - catch (error) { - backupData[file] = null; - } - } - - try { - for (const file of coreFiles) { - setProgress(shuYing, { title: "核心文件修复", status: "写入中", file, current: progress, total: coreFiles.length * 2 }); - await writeTargetFile(fileData[file], `extension/术樱包/${file}`); - progress++; - } - await removeManifestOldPaths(shuYing, manifest); - setProgress(shuYing, { title: "核心文件修复", status: "完成", current: coreFiles.length * 2, total: coreFiles.length * 2 }); - } - catch (error) { - for (const file of coreFiles) { - try { - if (backupData[file]) { - await writeTargetFile(backupData[file], `extension/术樱包/${file}`); - } - else { - await removeFile(`extension/术樱包/${file}`); - } - } - catch (rollbackError) { - console.error("核心文件回滚失败:", file, rollbackError); - } - } - - throw error; - } - } - finally { - for (const file of coreFiles) { - try { - await removeFile(`${coreTempDir}/${file}`); - } - catch (error) { - console.error("临时文件清理失败:", file, error); - } - } - } + manifest = manifest || await getManifest(); + const files = getManifestCoreFiles(manifest); + if (!files.length) throw new Error("校验清单缺少 core 核心文件列表"); + await updateAllFiles(shuYing, manifest, files); } async function getChangedFiles(manifest) { @@ -697,15 +773,147 @@ async function getLegacyModuleManifest(moduleId, rootManifest) { return { files: Object.fromEntries(files.map(file => [file, {}])) }; } +const mengsanRecordKey = "shuYing_mengsan_installation"; +const mengsanVersionKey = "shuYing_mengsan_installed_version"; + +function getMengsanRecord() { + const record = lib.config[mengsanRecordKey]; + if (record == null) return null; + if (!["installed", "installing", "repair", "uninstalling", "disabled"].includes(record.status) + || !Array.isArray(record.files)) throw new Error("梦三本地安装记录无效,请保留文件并检查日志"); + const files = uniqueManifestList(record.files); + if (files.some(file => !file.startsWith("mengsan/"))) throw new Error("梦三安装记录包含越界文件"); + if (record.status != "disabled" && !files.includes("mengsan/register.js")) { + throw new Error("梦三安装记录缺少模块入口"); + } + return { ...record, files }; +} + +async function saveMengsanRecord(record) { + const version = record.status == "disabled" ? "disabled" : record.version || "legacy"; + // Both keys commit together; saveConfig/putDB request success is too early. + lib.status.reload++; + try { + if (lib.db) { + await new Promise((resolve, reject) => { + let transaction; + let requestError; + try { + transaction = lib.db.transaction(["config"], "readwrite"); + transaction.oncomplete = () => resolve(); + transaction.onerror = event => { requestError = event.target?.error; }; + transaction.onabort = () => reject(transaction.error || requestError || new Error("梦三安装记录未能保存")); + const store = transaction.objectStore("config"); + store.put(record, mengsanRecordKey); + store.put(version, mengsanVersionKey); + } + catch (error) { + try { transaction?.abort(); } catch {} + reject(error); + } + }); + } + else { + const key = `${lib.configprefix}config`; + const config = JSON.parse(localStorage.getItem(key) || "{}"); + if (!config || typeof config != "object" || Array.isArray(config)) throw new Error("本地配置格式无效"); + config[mengsanRecordKey] = record; + config[mengsanVersionKey] = version; + localStorage.setItem(key, JSON.stringify(config)); + } + lib.config[mengsanRecordKey] = record; + lib.config[mengsanVersionKey] = version; + } + finally { game.reload2(); } +} + +async function getPreviousMengsanManifest(rootManifest = null) { + const record = getMengsanRecord(); + if (record && record.status != "disabled") { + return { files: Object.fromEntries(record.files.map(file => [file, {}])) }; + } + const savedVersion = lib.config[mengsanVersionKey]; + const receiptPath = "extension/术樱包/dist/modules/mengsan.json"; + if (await checkFile(receiptPath) == 1) { + const receipt = JSON.parse(new TextDecoder().decode(toUint8Array(await readFile(receiptPath)))); + if (parseVersion(receipt.version || "") + && (!parseVersion(savedVersion || "") || receipt.version == savedVersion)) { + validateManifest(receipt, receipt.version, "mengsan"); + if (!receipt.files["mengsan/register.js"]) throw new Error("本地梦三清单缺少入口"); + // ZIP installs carry their own receipt, so first uninstall also works offline. + if (!parseVersion(savedVersion || "")) { + const legacyPath = "extension/术樱包/dist/modules/mengsan.legacy.json"; + if (await checkFile(legacyPath) == 1) { + const legacy = JSON.parse(new TextDecoder().decode(toUint8Array(await readFile(legacyPath)))); + if (legacy.version != receipt.version || legacy.module_id != "mengsan" + || legacy.snapshot != "legacy" || !Array.isArray(legacy.files)) throw new Error("本地梦三迁移清单无效"); + const owned = uniqueManifestList(legacy.files); + if (owned.some(file => !file.startsWith("mengsan/"))) throw new Error("本地梦三迁移清单包含越界文件"); + return { files: Object.fromEntries([...getManifestFileKeys(receipt), ...owned].map(file => [file, {}])) }; + } + } + return receipt; + } + } + let previous; + if (parseVersion(savedVersion || "")) { + // Never substitute the latest manifest for an unavailable installed version. + previous = await getInstalledModuleManifest("mengsan", savedVersion, rootManifest?.source || getUpdateSource()); + } + else { + previous = await getLegacyModuleManifest("mengsan", rootManifest || await getManifest()); + } + if (!previous || !getManifestFileKeys(previous).includes("mengsan/register.js")) { + throw new Error("无法确认已安装梦三模块的完整文件清单,未改动模块文件;请恢复对应版本清单后重试"); + } + return previous; +} + +async function installMengsanFiles(shuYing, manifest, files, previousManifest = null) { + assertMutationReady(); + const previousRecord = getMengsanRecord(); + const currentFiles = getManifestFileKeys(manifest); + const oldFiles = previousManifest ? getManifestFileKeys(previousManifest) : previousRecord?.files || []; + const operationRoot = newOperationRoot(); + const pending = { + status: "installing", version: manifest.version, source: manifest.source, + files: uniqueManifestList([...oldFiles, ...currentFiles, ...getManifestRemoveFiles(manifest)]), + recoveryDirectory: operationRoot, + }; + await saveMengsanRecord(pending); // Disable loading before the first filesystem mutation. + let filesApplied = false; + try { + await updateAllFiles(shuYing, manifest, files, operationRoot); + filesApplied = true; + for (const file of currentFiles) { + await verifyFile(file, await readFile(`extension/术樱包/${file}`), manifest); + } + await saveMengsanRecord({ status: "installed", version: manifest.version, + source: manifest.source, files: currentFiles }); + } + catch (error) { + let recovery = { ...pending, status: "repair", recoveryDirectory: error.recoveryDirectory || null }; + if (!filesApplied && !error.recoveryIncomplete && !error.operationUncertain) { + // A complete rollback may restore a previously committed installation. + if (previousRecord?.status == "installed" || previousRecord?.status == "disabled") recovery = previousRecord; + else if (!previousManifest && !oldFiles.length) recovery = { status: "disabled", files: [], version: "disabled" }; + } + try { await saveMengsanRecord(recovery); } + catch (saveError) { console.error("安装中断记录仍保持待处理状态:", saveError); } + throw error; + } +} + async function isMengsanInstalled(localVersion, previousManifest, currentManifest) { + const record = getMengsanRecord(); + if (record) return !["disabled", "uninstalling"].includes(record.status); const saved = lib.config.shuYing_mengsan_installed_version; if (saved == "disabled") return false; if (saved) return true; const entryPath = "extension/术樱包/mengsan/register.js"; - if (typeof game.checkFile == "function" - && await safeCheckFile(entryPath) == 1) { - return true; + if (typeof game.checkFile == "function") { + return await safeCheckFile(entryPath) == 1; } if (typeof game.checkFile != "function" && typeof game.readFile == "function") { try { @@ -717,7 +925,7 @@ async function isMengsanInstalled(localVersion, previousManifest, const oldManifest = previousManifest || (parseVersion(localVersion) ? await getManifestFromTag({ name: `v${localVersion}`, version: localVersion, - }, false, currentManifest.source).catch(() => null) + }, false, currentManifest?.source).catch(() => null) : null); return !!oldManifest?.files?.["mengsan/register.js"]; } @@ -748,17 +956,26 @@ async function getObsoleteFiles(manifest) { return existing; } -async function updateAllFiles(shuYing, manifest, files) { +function newOperationRoot() { + return `${coreTempDir}/${Date.now()}-${Math.random().toString(36).slice(2)}`; +} + +async function updateAllFiles(shuYing, manifest, files, operationRoot = newOperationRoot()) { + assertMutationReady(); if (typeof game.writeFile != "function") { throw new Error("当前环境不支持写入文件"); } - const newRoot = `${coreTempDir}/new`; - const oldRoot = `${coreTempDir}/old`; + // Failed recovery directories must not be overwritten by a later operation. + const newRoot = `${operationRoot}/new`; + const oldRoot = `${operationRoot}/old`; const backedUp = new Set(); + const staged = new Set(); + const backupAttempts = new Set(); const installed = []; const obsoleteBackups = []; const obsoleteFiles = getManifestRemoveFiles(manifest); let filesInstalled = false; + let retainRecovery = false; try { ensureProgressNode(shuYing); await createDir(`${newRoot}/`); @@ -772,6 +989,7 @@ async function updateAllFiles(shuYing, manifest, files) { title: "版本更新", status: "下载并校验", file, current: i, total: files.length * 2, }); + staged.add(file); await downloadFile(file, `${newRoot}/${file}`, manifest); } @@ -779,14 +997,14 @@ async function updateAllFiles(shuYing, manifest, files) { for (let i = 0; i < files.length; i++) { const file = files[i]; const target = `extension/术樱包/${file}`; - try { + const exists = await checkFile(target); + if (exists == 0) throw new Error(`待替换路径不是文件:${file}`); + if (exists == 1) { const original = await readFile(target); + backupAttempts.add(file); await writeTargetFile(original, `${oldRoot}/${file}`); backedUp.add(file); } - catch (error) { - if (await safeCheckFile(target) == 1) throw error; - } setProgress(shuYing, { title: "版本更新", status: "写入中", file, @@ -797,22 +1015,21 @@ async function updateAllFiles(shuYing, manifest, files) { } for (const file of obsoleteFiles) { const target = `extension/术樱包/${file}`; - try { + if (await checkFile(target) == 1) { const original = await readFile(target); + backupAttempts.add(file); await writeTargetFile(original, `${oldRoot}/${file}`); backedUp.add(file); obsoleteBackups.push(file); } - catch (error) { - if (await safeCheckFile(target) == 1) throw error; - } } await removeManifestOldPaths(shuYing, manifest); filesInstalled = true; } catch (error) { - const rollbackFiles = filesInstalled ? [] + const rollbackFiles = filesInstalled || error.operationUncertain ? [] : [...installed, ...obsoleteBackups].reverse(); + const failedRollback = []; for (const file of rollbackFiles) { const target = `extension/术樱包/${file}`; try { @@ -824,15 +1041,39 @@ async function updateAllFiles(shuYing, manifest, files) { } } catch (rollbackError) { + failedRollback.push(file); console.error("文件回滚失败:", file, rollbackError); } } + retainRecovery = !!failedRollback.length || !!error.operationUncertain; + if (retainRecovery) { + error.recoveryIncomplete = true; + error.recoveryDirectory = operationRoot; + try { + await writeTargetFile(JSON.stringify({ + version: manifest.version, files, installed, obsoleteBackups, + backedUp: [...backedUp], failedRollback, + operationUncertain: !!error.operationUncertain, + error: getErrorMessage(error), + }, null, 2), `${operationRoot}/recovery.json`); + } + catch (saveError) { console.error("恢复说明写入失败,旧文件仍保留:", operationRoot, saveError); } + await addLog(shuYing, `回滚未完成,恢复文件已保留在:${operationRoot}`); + } throw error; } finally { - for (const file of new Set([...files, ...backedUp])) { - await removeFile(`${newRoot}/${file}`); - if (backedUp.has(file)) await removeFile(`${oldRoot}/${file}`); + if (!retainRecovery) { + for (const file of new Set([...staged, ...backupAttempts])) { + try { + if (staged.has(file)) await removeFile(`${newRoot}/${file}`); + if (backupAttempts.has(file)) await removeFile(`${oldRoot}/${file}`); + } + catch (cleanupError) { + console.error("临时文件清理失败,不改变安装结果:", file, cleanupError); + break; // An unavailable backend must not cause one timeout per file. + } + } } } } @@ -908,12 +1149,6 @@ async function checkVersion(shuYing, options = {}) { const installedMengsan = await isMengsanInstalled( localVersion, previousManifest, manifest ); - if (installedMengsan - && !lib.config.shuYing_mengsan_installed_version - && parseVersion(localVersion)) { - game.saveConfig("shuYing_mengsan_installed_version", - localVersion == manifest.version ? "legacy" : localVersion); - } if (previousManifest) { const retainedModuleFiles = manifest.modules?.mengsan ? getManifestFileKeys(previousManifest) @@ -938,32 +1173,7 @@ async function checkVersion(shuYing, options = {}) { mengsanManifest = await getModuleManifest({ name: manifest.tag_name, version: manifest.version, }, manifest, "mengsan"); - const installedModuleVersion = - lib.config.shuYing_mengsan_installed_version; - if (installedModuleVersion - && installedModuleVersion != "disabled" - && parseVersion(installedModuleVersion) - && installedModuleVersion != manifest.version) { - previousMengsan = await getInstalledModuleManifest( - "mengsan", installedModuleVersion, manifest.source - ); - } - else if (previousManifest && previousManifest.modules?.mengsan) { - previousMengsan = await getModuleManifest({ - name: `v${localVersion}`, version: localVersion, - }, previousManifest, "mengsan", manifest.source); - } - else if (previousManifest) { - previousMengsan = makeLegacyModuleManifest( - previousManifest, "mengsan" - ); - } - if (!previousMengsan - && (!installedModuleVersion || installedModuleVersion == "legacy")) { - previousMengsan = await getLegacyModuleManifest( - "mengsan", manifest - ); - } + previousMengsan = await getPreviousMengsanManifest(manifest); if (previousMengsan) { mergeRemovedFiles( mengsanManifest, @@ -984,6 +1194,9 @@ async function checkVersion(shuYing, options = {}) { if (localVersion == onlineVersion.online_version && !allChangedCount && !allObsoleteCount && !mengsanUpdateError) { + if (mengsanManifest && getMengsanRecord()?.status != "installed") { + await installMengsanFiles(shuYing, mengsanManifest, [], previousMengsan); + } game.saveConfig("shuYing_online_version", onlineVersion.online_version); alert("本地版本为最新版"); return onlineVersion.online_version; @@ -999,8 +1212,7 @@ async function checkVersion(shuYing, options = {}) { await updateAllFiles(shuYing, manifest, changed); if (mengsanManifest && !mengsanUpdateError) { try { - await updateAllFiles(shuYing, mengsanManifest, mengsanChanged); - game.saveConfig("shuYing_mengsan_installed_version", manifest.version); + await installMengsanFiles(shuYing, mengsanManifest, mengsanChanged, previousMengsan); } catch (error) { mengsanUpdateError = error; @@ -1068,35 +1280,7 @@ async function repairMissingFiles(shuYing) { mengsanManifest = await getModuleManifest({ name: manifest.tag_name, version: manifest.version, }, manifest, "mengsan"); - const installedModuleVersion = - lib.config.shuYing_mengsan_installed_version; - if (installedModuleVersion == "legacy") { - previousMengsan = await getLegacyModuleManifest( - "mengsan", manifest - ); - } - else if (installedModuleVersion - && parseVersion(installedModuleVersion) - && installedModuleVersion != manifest.version) { - previousMengsan = await getInstalledModuleManifest( - "mengsan", installedModuleVersion, manifest.source - ); - } - else if (previousManifest?.modules?.mengsan) { - previousMengsan = await getModuleManifest({ - name: `v${localVersion}`, version: localVersion, - }, previousManifest, "mengsan", manifest.source); - } - else if (previousManifest) { - previousMengsan = makeLegacyModuleManifest( - previousManifest, "mengsan" - ); - } - else if (!installedModuleVersion) { - previousMengsan = await getLegacyModuleManifest( - "mengsan", manifest - ); - } + previousMengsan = await getPreviousMengsanManifest(manifest); if (previousMengsan) { mergeRemovedFiles( mengsanManifest, @@ -1115,8 +1299,7 @@ async function repairMissingFiles(shuYing) { await removeManifestOldPaths(shuYing, manifest); if (mengsanManifest) { const moduleChanged = await getChangedFiles(mengsanManifest); - await updateAllFiles(shuYing, mengsanManifest, moduleChanged); - game.saveConfig("shuYing_mengsan_installed_version", manifest.version); + await installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan); } setProgress(shuYing, { title: "查漏补缺", status: "完成", current: files.length, total: files.length }); @@ -1138,93 +1321,65 @@ async function manageMengsanModule(shuYing) { setBusy(shuYing, true); try { shuYing._updateLogs = []; + assertMutationReady(); + const record = getMengsanRecord(); const localVersion = lib.config.shuYing_local_version || "0.0.0.0"; - const rootManifest = await getManifest(); - const installed = await isMengsanInstalled( - localVersion, null, rootManifest - ); - if (installed) { - if (!confirm("确认卸载梦三模式?只删除梦三模块清单拥有的文件。")) return; - let moduleManifest; - const moduleVersion = lib.config.shuYing_mengsan_installed_version; - if (moduleVersion == "legacy" || !moduleVersion) { - const legacyManifest = await getLegacyModuleManifest( - "mengsan", rootManifest - ).catch(() => null); - const currentManifest = await getModuleManifest({ - name: rootManifest.tag_name, version: rootManifest.version, - }, rootManifest, "mengsan"); - let localManifest = null; - if (parseVersion(localVersion) - && localVersion != rootManifest.version) { - localManifest = await getInstalledModuleManifest( - "mengsan", localVersion, rootManifest.source - ).catch(() => null); - } - moduleManifest = { - files: Object.fromEntries([...new Set([ - ...getManifestFileKeys(legacyManifest || { files: {} }), - ...getManifestFileKeys(localManifest || { files: {} }), - ...getManifestFileKeys(currentManifest), - ])].map(file => [file, {}])), - }; - } - else if (parseVersion(moduleVersion || "")) { - moduleManifest = await getInstalledModuleManifest( - "mengsan", moduleVersion, rootManifest.source - ).catch(async error => { - await addLog(shuYing, `读取已安装模块清单失败:${getErrorMessage(error)}`); - return null; - }); - } - if (!moduleManifest && parseVersion(localVersion)) { - moduleManifest = await getInstalledModuleManifest( - "mengsan", localVersion, rootManifest.source - ).catch(() => null); - } - if (!moduleManifest) { - moduleManifest = await getModuleManifest({ - name: rootManifest.tag_name, version: rootManifest.version, - }, rootManifest, "mengsan"); - } - const files = getManifestFileKeys(moduleManifest); - if (typeof game.removeFile != "function") { - throw new Error("当前环境不支持安全卸载文件"); - } - for (const file of files) { + const pendingInstall = record && ["installing", "repair"].includes(record.status); + const installed = record?.status == "uninstalling" + || await isMengsanInstalled(localVersion, null, null); + if (installed && !pendingInstall) { + const message = record?.status == "uninstalling" + ? "上次卸载未完成,是否继续卸载梦三模式?" + : "确认卸载梦三模式?只删除安装清单拥有的文件,保留存档。"; + if (!confirm(message)) return; + // Cached ownership allows uninstalling without a network connection. + const previous = record ? { files: Object.fromEntries(record.files.map(file => [file, {}])) } + : await getPreviousMengsanManifest(); + const files = getManifestFileKeys(previous); + const pending = { + status: "uninstalling", version: record?.version || lib.config[mengsanVersionKey] || "legacy", + source: record?.source || getUpdateSource(), files, + }; + await saveMengsanRecord(pending); + ensureProgressNode(shuYing); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + setProgress(shuYing, { title: "卸载梦三模式", status: "删除并检查", file, current: i, total: files.length }); await addLog(shuYing, `卸载梦三文件:${file}`); await removeFile(`extension/术樱包/${file}`); - if (typeof game.checkFile == "function" - && await safeCheckFile(`extension/术樱包/${file}`) == 1) { - throw new Error(`梦三文件删除失败:${file}`); - } } - game.saveConfig("shuYing_mengsan_installed_version", "disabled"); - alert("梦三模式文件已卸载,请重启游戏生效。"); + await saveMengsanRecord({ status: "disabled", version: "disabled", files: [] }); + alert("梦三模式文件已卸载,存档已保留,请重启游戏生效。"); return; } + const rootManifest = await getManifest(); const moduleInfo = rootManifest.modules?.mengsan; - const sizeText = moduleInfo?.size_bytes - ? `,约 ${formatBytes(moduleInfo.size_bytes)}` : ""; - if (!confirm(`梦三模式尚未安装,是否下载并校验全部梦三资源${sizeText}?`)) return; + const sizeText = moduleInfo?.size_bytes ? `,约 ${formatBytes(moduleInfo.size_bytes)}` : ""; + const message = pendingInstall + ? `上次安装或更新未完成,是否重新下载并修复梦三模式${sizeText}?` + : `梦三模式尚未安装,是否下载并校验全部梦三资源${sizeText}?`; + if (!confirm(message)) return; const moduleManifest = await getModuleManifest({ name: rootManifest.tag_name, version: rootManifest.version, }, rootManifest, "mengsan"); + const previous = pendingInstall ? await getPreviousMengsanManifest(rootManifest) : null; + if (previous) mergeRemovedFiles(moduleManifest, getManifestFileKeys(previous)); const files = getManifestFileKeys(moduleManifest); ensureProgressNode(shuYing); setProgress(shuYing, { - title: "安装梦三模式", status: "准备下载", current: 0, - total: files.length * 2, + title: "安装梦三模式", status: "准备下载", current: 0, total: files.length * 2, }); - await updateAllFiles(shuYing, moduleManifest, files); - game.saveConfig("shuYing_mengsan_installed_version", rootManifest.version); - alert("梦三模式下载完成,请重启游戏生效。"); + await installMengsanFiles(shuYing, moduleManifest, files, previous); + alert("梦三模式安装完成,请重启游戏生效。"); } catch (error) { console.error(error); await addLog(shuYing, `梦三模块操作失败:${getErrorMessage(error)}`); - alert("梦三模式操作失败,请检查网络或 extension/术樱包/log.txt。"); + const status = lib.config[mengsanRecordKey]?.status; + const hint = status == "uninstalling" ? "卸载未完成,重启后梦三暂停加载,可再次点击此按钮继续卸载。" + : ["installing", "repair"].includes(status) ? "安装未完成,重启后梦三暂停加载,可再次点击此按钮修复。" : "模块文件未能完成操作。"; + alert(`${hint}\n${getErrorMessage(error)}\n请查看 extension/术樱包/log.txt。`); } finally { removeProgressNode(shuYing);