fixed # 补全部分卡牌

fixed # 修复部分已知BUG
This commit is contained in:
2026-10-04 19:32:38 +08:00
parent c448da39c2
commit 94a2a6724b
163 changed files with 14008 additions and 1537 deletions
+12 -5
View File
@@ -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 '打包失败' }
+12
View File
@@ -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 '打包失败' }
+27 -1
View File
@@ -15,4 +15,30 @@ tools/*.mjs
/Slay the Spire 2
*.mjs
*.cjs
*.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
+36 -69
View File
@@ -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"
}
}
}
+4739
View File
File diff suppressed because it is too large Load Diff
+507
View File
@@ -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"
]
}
+6 -3
View File
@@ -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));
Binary file not shown.

After

Width:  |  Height:  |  Size: 661 KiB

+15 -1
View File
@@ -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量化压缩,拆包参考原件保留。
Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

+6 -16
View File
@@ -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;
+151 -147
View File
@@ -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;
},
});
}
+54 -9
View File
@@ -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;
}
+2 -1
View File
@@ -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);
}
+3 -3
View File
@@ -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);
+22 -18
View File
@@ -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,
+38 -11
View File
@@ -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;
+22
View File
@@ -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`。初始和衍生沿用中性配色,并保留各自级别名称;状态、诅咒、任务目前只输出级别,保留已有卡面视觉。
本次保留现有奖励随机概率、事件牌获取途径、添柴的全牌组随机规则,不依据稀有度新增排除规则或权重。巨石继续仅在战斗中生成。
核对边界:只做源码语法、字段覆盖和文件引用核对,不构成实机验收。
+17
View File
@@ -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、选择提示与点击区。
+2
View File
@@ -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)?.();
+8 -1
View File
@@ -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}:{}),
};
}
+23 -30
View File
@@ -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);
}
+9
View File
@@ -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);
}
+9
View File
@@ -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: "任务",
});
+8
View File
@@ -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。");
}
+2 -2
View File
@@ -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();
+87
View File
@@ -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;
};
}
+12
View File
@@ -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);
+2
View File
@@ -0,0 +1,2 @@
// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。
export * from "./packs/liubei/advanced-data.js";
+2
View File
@@ -0,0 +1,2 @@
// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。
export * from "./packs/liubei/data.js";
+32
View File
@@ -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;
+2
View File
@@ -0,0 +1,2 @@
// 兼容旧导入路径;刘备牌组的实际实现位于 packs/liubei。
export * from "./packs/liubei/runtime.js";
+24
View File
@@ -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);
},
};
}
+24 -697
View File
@@ -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 || [])])] };
}
+27
View File
@@ -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` 目前是牌组向这些入口注册本场效果的轻量桥接模块,不导入引擎或牌数据;状态受当前战斗会话约束,结束即清理。
适配规则:添柴使用所有可正常生成的刘备牌与无色牌;巨石仅由原始力量生成,不进入永久奖励。多人效果按梦三存活角色处理,群情激愤向全部存活角色的个人弃牌堆加副本;肉盾作用于梦三友方阵营,采用详情页的双倍/减半数值。自动打出的牌沿用原生合法目标与牌效果。
本次仅做源码语法、引用和图片文件核对;实际对局与视觉验收由用户执行。
打击牌判断:杀对应基础打击,计入完美打击,并能被地狱狂徒自动打出;重杀对应痛击,不属于打击牌。
File diff suppressed because it is too large Load Diff
+8
View File
@@ -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); }
}
+8
View File
@@ -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}`));
+268
View File
@@ -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
}
}
]);
+10
View File
@@ -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})]))});
+571
View File
@@ -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张加入你的手牌。这张牌在本回合可以免费打出。消耗。"
}
}
]);
+19
View File
@@ -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;
}
@@ -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}]);
+481
View File
@@ -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<s.powers.firstBlock){uses.add(current);return num*2;}
return num;
}
current=current.parent;
}
return num;
},
});
return s;
}
function totalExhaustions(token) {
let total=0;
for(const player of token.battle.players)total+=pileOf(player,token)?.exhaustTotal?.()||0;
return total;
}
function dataOf(card) {
const physical=card?.cards?.length===1 ? card.cards[0] : card;
const data=physical?.storage?.mengsanCard_shuying || card?.storage?.mengsanCard_shuying || {};
return {...data,name:card.name,suit:card.suit||data.suit||"spade",number:card.number||data.number||1,
nature:card.nature||data.nature||null,upgrade:cardUpgradeLevel(card),affixes:[...(data.affixes||[])]};
}
function generatedData(s,name,source=null,upgrade=0) {
const battle=s.token.battle,serial=(battle.generatedCardSerial||0)+1;
if(!safe(serial))throw new RangeError("生成牌序号越界");
battle.generatedCardSerial=serial;
const raw=source ? dataOf(source) : {suit:"spade",number:1,nature:null,affixes:[],upgrade};
return createCardData({...raw,id:`mengsan_generated_${s.token.session.id}_${serial}`,name,
upgrade:Math.min(raw.upgrade||0,cardUpgradeRule(name)?.maxLevel||0)}, {enemy:true});
}
const randomItem=(s,list)=>{
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;i<hitsCount && valid(player,s.token);i++){
const targets=r.all ? [...s.token.battle.players].filter(t=>valid(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;i<r.copies&&valid(player,s.token);i++)await copyToHand(s,chosen);
}
if(r.partyCopies)for(const participant of [...s.token.battle.players]){
if(!valid(player,s.token))return;
if(!valid(participant,s.token))continue;
const targetPile=pileOf(participant,s.token);if(targetPile)targetPile.addToDiscard(generatedData(s,spec.name,event.card));
}
if(r.topPlays){
const count=takeXCardUse(player,event,s.token.battle);
if(count!==null)for(let i=0;i<count+r.xBonus&&valid(player,s.token);i++){
const card=pile.drawTop();if(!card)break;await autoPlay(s,card,{fromDraw:true});
}
}
if(r.transformAttacks)for(const card of player.getCards("h").filter(isAttackCard)){
if(!valid(player,s.token))break;
const data=dataOf(card);pile.transformHand(card,{...data,name:"mengsan_ic_boulder",upgrade:r.boulderUpgrade||0});
}
if(r.stoke){
const candidates=player.getCards("h").slice();
const pool=generationPool(cardDefinitions,["liubei","colorless"]);
let count=0;
for(const card of candidates){if(!valid(player,s.token))return;if(await pile.exhaustFromHand(card))count++;}
for(let i=0;i<count&&valid(player,s.token);i++){
const name=randomItem(s,pool);if(!name)break;
await pile.addToHand(generatedData(s,name,null,r.generatedUpgrade||0));
}
}
if(r.power)for(const key of powerKeys)if(r[key]){
if(["bodyguard","autoStrike","barricade","corruption"].includes(key))s.powers[key]=1;
else add(s.powers,key,r[key]);
}
await flush(s);refresh(s);
}
function addMap(map,key,value){const next=(map.get(key)||0)+value;if(!safe(next))throw new RangeError("擒拿数值越界");map.set(key,next);}
async function onUse(use) {
const s=ensure(use.player);if(!s || seenUses.has(use))return;seenUses.add(use);
if(!isAttackCard(use.card))return;
add(s.turn,"attacks",1);
if(s.powers.freeAttack)s.powers.freeAttack--;
const source=originalOf(use),snapshot={card:{name:use.card.name,suit:use.card.suit,number:use.card.number,nature:use.card.nature},data:dataOf(use.card),original:source,targets:[...(use.targets||[])]};
if(s.turn.doubleTap&&!use.mengsanIcEcho_shuying){s.turn.doubleTap--;uses.set(use,snapshot);}
// 第三张的副本在原牌效果之前加入手牌,复制此刻的累积数据。
if(s.turn.attacks===3 && s.powers.juggle)for(let i=0;i<s.powers.juggle&&valid(s.player,s.token);i++)await copyToHand(s,source);
refresh(s);
}
async function onUseAfter(use) {
await flushAll();const s=stateOf(use.player),echo=uses.get(use);uses.delete(use);
if(s&&echo&&valid(s.player,s.token))await autoPlay(s,echo.original,{echo,targets:echo.targets});
refresh(s);
}
async function turnBegin(player) {
const s=ensure(player);if(!s)return;
s.turn=Object.create(null);s.freeCards=new WeakSet();s.autoCount=0;
if(s.powers.plating)s.powers.plating=Math.max(0,s.powers.plating-1);
const pile=pileOf(player,s.token);
if(!pile)return;
for(let i=0;i<(s.powers.retrieveAttack||0)&&valid(player,s.token);i++){
const card=randomItem(s,pile.discardCards().filter(isAttackCard));if(!card)break;
if(await pile.gainToHand(card)){
if(!valid(player,s.token))break;
const data=dataOf(card);if(canUpgradeCard(data))card.storage.mengsanCard_shuying={...data,upgrade:data.upgrade+1};
}
}
refresh(s);
}
async function turnEnd(player) {
const s=ensure(player);if(!s)return;
if(s.powers.plating){await player.changeHujia(s.powers.plating);if(!valid(player,s.token))return;}
const pile=pileOf(player,s.token);if(!pile)return;
const howls=pile.exhaustedCards().filter(c=>rule(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};
}
+52
View File
@@ -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 })])));
+15
View File
@@ -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] };
}
}
+910
View File
@@ -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) };
}
+140
View File
@@ -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 }),
});
+12
View File
@@ -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",
]);
+71
View File
@@ -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));
+320
View File
@@ -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("<br>");
} },
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;
} };
}
+99
View File
@@ -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"}),
});
+4
View File
@@ -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]);
+6
View File
@@ -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])));
+594
View File
@@ -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.length<n){const item=rand(s,bag);out.push(item);bag.splice(bag.indexOf(item),1);}
return out;
}
function pool(s,type=null,onlyColorless=false) {
const ownDecks=Object.values(cardDefinitions).filter(c=>c.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;i<count && valid(player,s.token);i++){
const selected=id==="volley"?[rand(s,enemies(s))].filter(Boolean):r.all?enemies(s):[target];
for(const t of selected){actual+=await hit(s,t,amount,event.card,true);if(!valid(player,s.token))return;}
}
// Detail page: the equal splash directly deals non-attack damage; no strength/weak/vulnerable reapplication.
if(id==="omnislice")for(const t of enemies(s).filter(t=>t!==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<r.generate&&valid(player,s.token);i++)await generateHand(s,rand(s,pool(s,null,true)));
if(id==="discovery")await chooseGenerated(s);
if(id==="abundance")await chooseGenerated(s,{type:"power",upgrade:r.generatedUpgrade||0});
if(id==="mad_science-chaos")await generateHand(s,rand(s,pool(s)),{free:true});
if(r.generateZero){
const names=pool(s).filter(name=>{
// Jackpot+ upgrades the result, but its pool still requires a zero-cost base card.
return cardPackCosts[name]===0;
});
for(let i=0;i<r.generateZero&&valid(player,s.token);i++)await generateHand(s,rand(s,names),{upgrade:r.generatedUpgrade||0});
}
if(r.generateDrawAttacks)for(let i=0;i<r.generateDrawAttacks&&valid(player,s.token);i++){
const name=rand(s,pool(s,"attack"));if(!name)break;
const c=pile?.addToDraw(generated(s,name),Math.floor(random(s.token.run)*((pile?.drawCards().length||0)+1)));
if(c)s.freeBattle.add(c);
}
if(r.autoDraw)for(let i=0;i<r.autoDraw&&valid(player,s.token);i++){const c=rand(s,pile?.drawCards()||[]);if(!c)break;await autoPlay(s,c);}
if(r.autoDiscard)for(const c of sample(s,(pile?.discardCards()||[]).filter(isAttackCard),r.autoDiscard)){if(!valid(player,s.token))return;await autoPlay(s,c);}
if(r.replay){const c=rand(s,(pile?.drawCards()||[]).filter(c=>!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<s.powers.calamity && valid(s.player,s.token);i++)await generateHand(s,rand(s,pool(s,"attack")));
}
if(isActiveCardUse(use,s.player))add(s.turn,"plays",1);
if(s.powers.panache){
add(s.turn,"panachePlays",1);
if(s.turn.panachePlays%5===0)await hitAll(s,s.powers.panache);
}
if(s.turn.choking)for(const [target,n] of s.turn.choking){if(valid(s.player,s.token)&&valid(target,s.token))await target.loseHp(n);}
if(!use.mengsanSharedEcho_shuying){
const replay=replays.get(entry.snapshot.original);
if(replay?.session===s.token.session)for(let i=0;i<replay.count;i++)entry.echoes.push(entry.snapshot.targets);
if(isAttackCard(use.card)){
const consumed=[];
for(const tag of b.sharedTags||[])if(tag.owner!==s.player && s.player.isFriendOf(tag.owner) && valid(tag.target,s.token)){
entry.echoes.push([tag.target]);consumed.push(tag);
}
b.sharedTags=(b.sharedTags||[]).filter(t=>!consumed.includes(t));
}
}
if(s.powers.nostalgia && (s.turn.nostalgia||0)<s.powers.nostalgia && !use.mengsanSharedEcho_shuying &&
["attack","skill"].includes(cardDefinitions[use.card?.name]?.cardType)){
add(s.turn,"nostalgia",1);entry.returnTop=true;
}
}
async function onUseAfter(use) {
const entry=uses.get(use);if(!entry)return;
uses.delete(use);const {s}=entry,b=s.token.battle;
try {
if(valid(s.player,s.token) && !use._cancelled){
for(const targets of entry.echoes){if(!valid(s.player,s.token))break;await autoPlay(s,entry.snapshot.original,{echo:entry.snapshot,targets});}
if(entry.returnTop && valid(s.player,s.token))pileOf(s)?.moveDiscardToTop(entry.snapshot.original);
if(entry.ballHandoff && valid(s.player,s.token)){
const card=entry.snapshot.original,source=pileOf(s),recipients=friends(s).filter(p=>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<s.powers.entropy && valid(player,s.token);i++){
const [c]=await choose(s,player.getCards("h").filter(c=>!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;i<s.powers.mayhem&&valid(player,s.token);i++){
const c=pileOf(s)?.drawTop();if(!c)break;await autoPlay(s,c);
}
}
async function handEnd(phase) {
if(seenEnds.has(phase))return;seenEnds.add(phase);
const s=ensure(phase.player);if(!s)return;
// Snapshot before ethereal removal and ordinary discards; resolve each held copy separately.
const hand=s.player.getCards("h").slice(),count=hand.length;
for(const c of hand){
if(!valid(s.player,s.token))return;
if(!s.player.getCards("h").includes(c))continue;
const spec=sharedByName[c.name],r=rule(c);if(!spec||!r||spec.id==="infection")continue;
const damage=spec.id==="wither"?witherDamage(c):r.endDamage;
if(damage){const e=s.player.damage(damage,"nosource");e.mengsanSharedHandDamage_shuying=true;await e;}
if(!valid(s.player,s.token))return;
if(r.endLoseHp)await s.player.loseHp(r.endLoseHp);
if(!valid(s.player,s.token))return;
if(spec.id==="regret"&&count)await s.player.loseHp(count);
if(!valid(s.player,s.token))return;
if(spec.id==="debt")debitGold(s,10);
if(spec.id==="shame"){s.pendingDebuffs||={};add(s.pendingDebuffs,"frail",1);}
if(spec.id==="doubt"){s.pendingDebuffs||={};add(s.pendingDebuffs,"weak",1);}
}
for(const c of hand){if(!valid(s.player,s.token))return;if(rule(c)?.ethereal)await pileOf(s)?.exhaustFromHand(c);}
refresh(s);
}
async function turnEnd(player) {
const s=stateOf(player);if(!s)return;
if(s.powers.plating)await player.changeHujia(s.powers.plating);
for(const bomb of s.bombs.slice()){
if(!valid(player,s.token))return;
bomb.turns--;if(bomb.turns<=0){s.bombs.splice(s.bombs.indexOf(bomb),1);await hitAll(s,bomb.damage);}
}
}
function clearTurn(player) {
const b=getBattle();if(!b?.session.active)return;
for(const p of b.players){const s=stateOf(p);if(!s)continue;if(p===player)s.negativeStrength=0;s.turn.choking=null;s.freeTurn=new WeakSet();}
const s=stateOf(player);if(s?.powers.noCardBlock)s.powers.noCardBlock--;
b.sharedAttackRecords=[];
}
function limited(player,card,event) {
const s=ensure(player);if(!s || !isActiveCardUse(event,player))return false;
const use=nearestUse(event);if(use?.mengsanSharedEcho_shuying||use?.mengsanIcEcho_shuying)return false;
const held=player.getCards("h"),id=sharedByName[card?.name]?.id;
if(id!=="enthralled"&&held.some(c=>sharedByName[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;
}
},
};
}
+179
View File
@@ -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
}
}
]);
+174 -20
View File
@@ -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<battle.exhaustPile.length;index++){
const card=current.get(battle.exhaustPile[index].id);
if(card)battle.exhaustPile[index]=snapshot(card);
}
refresh();
},
// 自动打出不会触发抽牌、洗牌或堆顶牌不足的失败规则。
beginAutoPlay(card, fromExhaust = false) {
if (released || !session.active || !owned.has(card)) return false;
if (fromExhaust) {
if (!card.storage?.mengsanExhausted_shuying) return false;
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;
} 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;
+104
View File
@@ -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();
},
};
}
+7
View File
@@ -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",
});
+51
View File
@@ -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;
}
+31
View File
@@ -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;
}
+16
View File
@@ -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;n<count;n++)if(!upgradeRandomCard(run.player.deck,()=>nextRandom(run)))break;
}
+13
View File
@@ -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);

Some files were not shown because too many files have changed in this diff Show More