From 279f2d9b0ae78c81d26b0a1ca4307c90d6dca1e0 Mon Sep 17 00:00:00 2001 From: Art_Sakura <1754798088@qq.com> Date: Mon, 12 Oct 2026 15:10:31 +0800 Subject: [PATCH] =?UTF-8?q?fixed=20#=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=A4=A9=E4=B9=A6=E4=B9=B1=E6=96=97=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + character/skill.js | 15 +- .../shuying-skill-tianshu-review/brief.md | 65 +++++ .../specs/shuying-skill-correctness/spec.md | 103 ++++++++ .../verification.md | 84 ++++++ ...-official-description-review-2026-10-12.md | 79 ++++++ function.js | 31 +++ menu.js | 15 ++ .../sets/qingqingzijin/dynamicTranslate.js | 34 ++- pve/boss/sets/qingqingzijin/skill.js | 124 +++++---- pve/boss/sets/qingqingzijin/translate.js | 6 +- pve/boss/sets/tianshu/skill.js | 4 +- tianshu/bondSkills.js | 28 +- tianshu/config.js | 60 ++++- tianshu/directDamage.js | 28 ++ tianshu/extension.js | 240 +++++++++++++++++- tianshu/skillRecover.js | 61 +++++ tools/manifest_config.json | 2 + update.txt | 8 + 19 files changed, 888 insertions(+), 103 deletions(-) create mode 100644 docs/comet/changes/shuying-skill-tianshu-review/brief.md create mode 100644 docs/comet/changes/shuying-skill-tianshu-review/specs/shuying-skill-correctness/spec.md create mode 100644 docs/comet/changes/shuying-skill-tianshu-review/verification.md create mode 100644 docs/reviews/qingqingzijin-official-description-review-2026-10-12.md create mode 100644 tianshu/directDamage.js create mode 100644 tianshu/skillRecover.js diff --git a/.gitignore b/.gitignore index c11998e..16a8250 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,7 @@ __pycache__/ .DS_Store Thumbs.db Desktop.ini + +/.comet/ +/.comet/* +/.comet/config.yaml diff --git a/character/skill.js b/character/skill.js index a1bf01c..d0b9440 100644 --- a/character/skill.js +++ b/character/skill.js @@ -650,23 +650,10 @@ const skills = { subSkill: { init: { charlotte: true, - forced: true, - locked: false, audio: false, - trigger: { - player: "enterGame", - global: "phaseBefore", - }, - filter(event, player) { - if (!player.countCharge(true)) { - return false; - } - return event.name != "phase" || game.phaseNumber == 0; - }, - async content(event, trigger, player) { + init(player) { const num = get.info("fenwei_shuying").beginMarkCount; player.addCharge(num); - await game.delayx(); }, }, diff --git a/docs/comet/changes/shuying-skill-tianshu-review/brief.md b/docs/comet/changes/shuying-skill-tianshu-review/brief.md new file mode 100644 index 0000000..1e7bba3 --- /dev/null +++ b/docs/comet/changes/shuying-skill-tianshu-review/brief.md @@ -0,0 +1,65 @@ +# 目标 + +落实用户已经接受的术樱包技能审查结果及逐项确认的青青子衿规则修正;连环传导允许再次增伤,保留用户确认的增强版本。 + +# 范围 + +沿用用户选择的当前目录 D:/Work/noname-shuying,保留既有未提交修改。修复 tianshu/bondSkills.js 的依赖导入、江东基业分组、舌战他弃过滤,青青子衿的驭布重复群伤/花色、狼顾回合限制/他弃过滤,character 奋威动态初始化,以及天书 Boss 白仪/九天得牌过滤。 + +扩展修复青青子衿狼顾、妄尊、夺玺、引雷的难度规则,张角体力,以及核实报告“描述与实现还存在的范围差异”全部条目:疑神随机装备、反馈逐点发动与来源范围、狼顾来源范围、暴虐满血结算与选人范围、雷击选人范围、谏征使用者与目标范围。同步静态描述、动态描述和选择提示;远虑阴间明确描述为视为受伤。 + +## Source coverage + +来源边界为用户本轮编号决定及 docs/reviews/qingqingzijin-official-description-review-2026-10-12.md 中范围差异章节和张角体力;官网仅作为已核实规则依据,不把整篇活动公告、虚拟偶像或奖励纳入实现。 + +| 来源条目与位置 | 读取状态 | 需要保留的内容 | Spec 位置 | 验收 ID | 覆盖状态 | 理由或替代关系 | +| --- | --- | --- | --- | --- | --- | --- | +| 用户1,狼顾 | complete | 阴间黑色,其余黑桃,成功恢复次数,描述切换 | 狼顾难度 | A11 | covered | 当前决定 | +| 用户2,远虑 | complete | 阴间保留视为受伤,描述明确 | 增强与文本 | A19 | covered | 覆盖旧公告真实伤害 | +| 用户3–5,无双/神戟/战甲 | complete | 保留当前增强版本及数值 | 增强与文本 | A19 | covered | 覆盖旧公告基准 | +| 用户6,妄尊 | complete | 阴间造成伤害即反击,其余仍超过1点 | 妄尊难度 | A12 | covered | 当前决定 | +| 用户7,夺玺 | complete | 阴间仅自己摸2,困难双方各摸1 | 夺玺难度 | A13 | covered | 当前决定 | +| 用户8,引雷 | complete | 阴间不限回合,困难回合外 | 引雷难度 | A14 | covered | 当前决定 | +| 用户额外,张角体力 | complete | 普通/困难/阴间8/16/24,起手和技能不变 | 张角体力 | A15 | covered | 当前决定 | +| 范围差异:疑神 | complete | 从可获得装备中随机取牌 | 随机疑神 | A16 | covered | 用户授权报告全部条目 | +| 范围差异:反馈 | complete | 每点伤害独立机会,允许友方来源 | 反馈逐点 | A17 | covered | 用户授权报告全部条目 | +| 范围差异:狼顾 | complete | 获得其他角色牌均可触发 | 角色范围 | A18 | covered | 用户授权报告全部条目 | +| 范围差异:暴虐 | complete | 满血仍失去1体力,可选择任意角色 | 角色范围 | A18 | covered | 用户授权报告全部条目 | +| 范围差异:雷击 | complete | 可选择任意其他角色 | 角色范围 | A18 | covered | 用户授权报告全部条目 | +| 范围差异:谏征 | complete | 其他角色用杀即可,无额外阵营限制 | 角色范围 | A18 | covered | 用户授权报告全部条目 | + +# 非目标 + +排除梦三和停用原神。不添加显式 sourceSkill 到所有正常 subSkill,不修改允许连环再次增伤的规则,不重构无关代码,不改用户已有天书直伤/回复功能,不提交、推送或发布。 + +不回退远虑阴间虚拟伤害、无双增伤、神戟增强数值及战甲阴间摸3;不按2021公告删除用户确认的增强。 + +# 验收示例 + +- 羁绊的 filter、mod、async content 在没有开发者全局对象时,能通过模块导入获取引擎依赖。 +- 江东基业二三级对应子技能通过正确 group 挂载并执行,低等级不重复叠加。 +- 驭布对一张多目标杀只判定一次,成功时对各存活目标只造成一次伤害。 +- 驭布困难难度黑桃成功、梅花失败;阴间难度黑色成功、红色失败,动态描述与难度条件一致。 +- 狼顾失败后仅限制当前回合;黑桃成功时恢复次数,即使没有可弃手牌也不会错误占用次数。 +- 奋威开局及中途获得时各初始化2蓄力,同次持有不因入场/开局事件重复添加,不重置共享蓄力。 +- 舌战与狼顾仅从允许被发动者弃置的牌中随机选择。 +- 白仪与九天仅从允许被发动者获得的牌中随机选择。 +- 连环伤害允许再次增伤,既有牌伤增益不添加 notLink 排除。 +- 新增定向回归验证并运行相关现有天书测试,说明真实游戏/联机实测边界,保留无关改动。 +- 狼顾阴间梅花成功、红色失败,普通/困难仅黑桃成功;动态描述同步。 +- 妄尊阴间累计1点伤害即反击,困难1点不反击,未受伤时弃牌数仍为困难1/阴间2。 +- 夺玺阴间替换摸牌后仅自己摸2,困难双方各摸1;动态描述、提示同步。 +- 引雷困难自己回合不触发、阴间自己回合触发;描述同步回合条件。 +- 张角三难度体力和上限8/16/24,起始手牌6/8/10和追加技能保留。 +- 疑神随机获取各目标合法装备,正常难度分支只选一名敌人,阴间所有合格敌人各一张;禁止选择受保护装备。 +- 反馈受到多点伤害有逐点发动机会,可对友方来源发动,源牌耗尽后停止;自伤只获取自身装备。 +- 狼顾接受其他友方来源;暴虐满血也失去1体力并允许任意角色;雷击允许其他友方,谏征允许其他角色使用的杀,不增加阵营限制。 +- 远虑阴间保留unreal并明确描述;无双/神戟/战甲增强不回退,全部改动描述与提示一致。 + +# 已确认决定 + +用户接受审查报告并明确授权修复其中8项已确认问题;用户明确确认允许再次增伤。用户先前选择当前目录,继续沿用。此前只读审查已完成,其发现作为本次已确认修复清单;现在允许修改对应源码和必要测试。 + +用户最新明确规则:困难 hard 为黑桃,阴间 nightmare 为黑色;保留一张多目标杀只判定一次,默认非阴间保持黑桃。 + +2026-10-12 用户逐项明确授权修复上述规则及范围差异,要求同步文本;指定保留远虑/无双/神戟/战甲增强,并指定张角8/16/24。该指令构成本轮完整范围确认,无需再次询问同一选择。 diff --git a/docs/comet/changes/shuying-skill-tianshu-review/specs/shuying-skill-correctness/spec.md b/docs/comet/changes/shuying-skill-tianshu-review/specs/shuying-skill-correctness/spec.md new file mode 100644 index 0000000..1e103b5 --- /dev/null +++ b/docs/comet/changes/shuying-skill-tianshu-review/specs/shuying-skill-correctness/spec.md @@ -0,0 +1,103 @@ +# 术樱包技能修复 + +## Requirement: 羁绊依赖和分组 + +天书羁绊模块通过显式导入绑定引擎对象;各等级只挂载自己的正确子技能。 + +### Scenario: 非开发者环境的羁绊执行 +Acceptance: A1 +无全局 lib/get/game 时,已导入的羁绊 filter、mod、async content 使用模块绑定正常执行。 + +### Scenario: 江东升级后的效果 +Acceptance: A2 +二级、三级拥有正确 draw/discard/gain 子技能;升档后不同时叠加旧等级。 + +## Requirement: 驭布和狼顾 + +驭布对每次用杀执行一次判定,困难难度要求黑桃、阴间难度要求黑色;狼顾按回合限制次数,成功时恢复次数,随机他弃遵守保护。 + +### Scenario: 驭布多目标 +Acceptance: A3 +杀指定两个目标,只在首次指定目标事件判定一次;符合当前难度的判定成功后各存活目标受到一次伤害。 + +### Scenario: 驭布花色 +Acceptance: A4 +困难 hard 时黑桃成功、梅花与红色失败;阴间 nightmare 时黑桃与梅花成功、红色失败。动态描述困难显示黑桃、阴间显示黑色,默认非阴间保持黑桃。 + +### Scenario: 狼顾次数 +Acceptance: A5 +判定失败保留使用标记到当前全局回合结束;符合当前难度的判定成功移除标记,与原持牌角色是否有可弃手牌无关。 + +## Requirement: 动态获得和候选牌合法性 + +奋威初始化兼容中途获得,禁止开局/入场重复加初始值,保留其他技能已有共享蓄力。随机牌操作使用对应他弃/他取合法候选。 + +### Scenario: 奋威动态获得 +Acceptance: A6 +开局和中途获得均初始化2蓄力;入场和开局事件不会重复添加。已有共享蓄力不被重置。 + +### Scenario: 随机他弃 +Acceptance: A7 +舌战和狼顾目标混有可弃和受保护牌时,随机结果只属于允许被发动者弃置的集合。 + +### Scenario: 随机获得 +Acceptance: A8 +白仪和九天目标混有可取得和受保护牌时,随机结果只属于允许被发动者获得的集合。 + +## Requirement: 保持规则与验证 + +用户允许连环传导再次增伤。只修改审查确认问题,不触及梦三、不重构无关功能。 + +### Scenario: 连环再次增伤 +Acceptance: A9 +五虎等既有牌伤增益仍接受连环伤害并增加伤害值。 + +### Scenario: 回归验证 +Acceptance: A10 +在修改前证明定向问题复现,修复后相关验证通过,同时现有天书直伤、回复和换关轮次测试通过;报告是否进行真实游戏或联机验证。 + +## Requirement: 青青子衿难度与体力 + +普通normal对应官网休闲,困难hard对应官网普通,阴间nightmare对应官网困难。按用户逐项决定修复规则,保留明确指定的增强版本。 + +### Scenario: 狼顾难度 +Acceptance: A11 +普通、困难仅黑桃成功;阴间黑桃和梅花成功、红色失败。动态描述反映条件;成功恢复次数及合法他弃继续成立。 + +### Scenario: 妄尊难度 +Acceptance: A12 +阴间敌人在当前回合对自己造成过正伤害即可反击1点;普通、困难仍要求累计伤害超过1点。未受伤时普通、困难弃1,阴间弃2。描述与条件一致。 + +### Scenario: 夺玺难度 +Acceptance: A13 +其他角色摸牌阶段可以失去1点体力替换摸牌;困难自己与其各摸1,阴间仅自己摸2。玩家离场不再摸牌,选择提示及动态描述同步。 + +### Scenario: 引雷难度 +Acceptance: A14 +困难仅自己回合外失牌触发,阴间自己回合内外均可。普通不获得追加技能;随机横置未横置角色。描述显示对应回合条件。 + +### Scenario: 张角体力 +Acceptance: A15 +张角普通、困难、阴间的初始体力与上限分别为8、16、24。起始手牌6/8/10、基础技能与困难起追加引雷保留;其他BOSS配置不受影响。 + +## Requirement: 取牌方式和角色范围 + +随机获得从发动者合法候选中抽取;描述未限制阵营时,不把AI偏好作为硬性目标限制。 + +### Scenario: 随机疑神 +Acceptance: A16 +疑神替换回复后,普通/困难获取一名随机合格敌人的随机合法装备,阴间获取每名合格敌人的一张随机合法装备;不提供装备自选,也不获取受保护装备。 + +### Scenario: 反馈逐点 +Acceptance: A17 +受到N点伤害后有N次独立可选反馈机会,每次获取来源一张合法牌;来源包括友方,来源离场或没有合法牌时不发动。自伤只能取自身装备,不能重复获取手牌。 + +### Scenario: 角色范围 +Acceptance: A18 +狼顾获得任何其他角色牌可触发;暴虐满血时X为0仍失去1体力,不选择伤害目标,受伤时目标可以是任意存活角色;雷击可选任意其他角色;谏征接受任何其他角色的杀,只检查非原目标、攻击范围和手牌等实际条件。AI可以偏好敌方/保护友方,但不得限制用户可选范围。 + +## Requirement: 保留增强并同步文本 + +### Scenario: 增强与文本 +Acceptance: A19 +远虑阴间仍调用unreal并描述为视为受到过伤害;无双未响应增伤次数、神戟普通/困难/阴间额外摸2/3/4和额外杀1/2/2、战甲阴间摸3均保留。狼顾、妄尊、夺玺、引雷及所有本轮改动的静态/动态文本和选择提示与最终规则一致。 diff --git a/docs/comet/changes/shuying-skill-tianshu-review/verification.md b/docs/comet/changes/shuying-skill-tianshu-review/verification.md new file mode 100644 index 0000000..e3004ab --- /dev/null +++ b/docs/comet/changes/shuying-skill-tianshu-review/verification.md @@ -0,0 +1,84 @@ +--- +generated_from_state_version: 24 +--- + +# 验证 + +## 当前结果 + +- 结果: **验收通过,需要你确认** +- 验证情况: **已完成检查,但需要你确认验证结果** +- 目标周期: 4 +- 迭代: 1 +- 验证器尝试次数: 1 +- 完成时间: 2026-10-12T06:41:40.687Z +- 摘要: 独立只读Verifier已读完brief、完整Spec及A1-A19,逐项全部通过。复用绑定当前候选c9724c63-8c45-44ab-944c-4681834c1755的正式回归101次执行全通过(90独立测试,11重复fixture)和空白检查,不重复整套。核对本轮狼顾黑色、妄尊1点、夺玺自己摸2及AI、引雷不限回合、张角8/16/24,全部范围修复及保留增强/连环规则;追踪动态描述注册与数字getIndex引擎调度。11个非目标文件SHA256与基线完全一致。未编辑源码/测试/文档/gitignore、未执行git操作、记忆更新或发布,未实机/联机。 + +## 验收 + +| 编号 | 结果 | 来源 | 验收项 | 原因 | +| --- | --- | --- | --- | --- | +| A1 | passed | brief.md | 羁绊的 filter、mod、async content 在没有开发者全局对象时,能通过模块导入获取引擎依赖。 | tianshu/bondSkills.js:2显式模块导入,当前正式ESM用例验证无开发者lib/get/game全局时filter、mod和content执行。 | +| A2 | passed | brief.md | 江东基业二三级对应子技能通过正确 group 挂载并执行,低等级不重复叠加。 | bondSkills.js:114/186二三级group与subSkill对应;extension.js:946-989按来源精确换级并核对当前所有权;正式二三级分组用例通过。 | +| A3 | passed | brief.md | 驭布对一张多目标杀只判定一次,成功时对各存活目标只造成一次伤害。 | 青青子衿skill.js:qubu保留isFirstTarget,并在成功后遍历当前存活目标各一次;正式多目标用例通过。 | +| A4 | passed | brief.md | 驭布困难难度黑桃成功、梅花失败;阴间难度黑色成功、红色失败,动态描述与难度条件一致。 | qubu按nightmare黑色、其余黑桃判定;动态描述同分支;当前正式hard与nightmare花色及动态切换用例通过。 | +| A5 | passed | brief.md | 狼顾失败后仅限制当前回合;黑桃成功时恢复次数,即使没有可弃手牌也不会错误占用次数。 | langgu以global phaseAfter临时限制当前回合,成功先移除used再判断来源与合法手牌,空牌成功也恢复;正式用例通过。 | +| A6 | passed | brief.md | 奋威开局及中途获得时各初始化2蓄力,同次持有不因入场/开局事件重复添加,不重置共享蓄力。 | character/skill.js:654奋威分组init添加2蓄力而不重置共享值;当前定向通过,非目标哈希一致且复用前轮fenwei-engine-lifecycle正式PASS记录。 | +| A7 | passed | brief.md | 舌战与狼顾仅从允许被发动者弃置的牌中随机选择。 | 舌战两级与狼顾从getDiscardableCards(player,zone)抽取,并标记discarder;当前正式保护牌用例通过。 | +| A8 | passed | brief.md | 白仪与九天仅从允许被发动者获得的牌中随机选择。 | 白仪/九天从getGainableCards(player,zone)随机取牌,难度数量与后续逻辑保留;当前正式保护牌用例通过。 | +| A9 | passed | brief.md | 连环伤害允许再次增伤,既有牌伤增益不添加 notLink 排除。 | 既有五虎/天下无双/武安及戌狗增伤未增加notLink排除;当前正式五虎连环输入2增至3,确认允许再次增伤。 | +| A10 | passed | brief.md | 新增定向回归验证并运行相关现有天书测试,说明真实游戏/联机实测边界,保留无关改动。 | 复用当前候选Runtime 5文件回归101次执行全通过、0失败,含90个独立测试(回复fixture重复登记11项);正式空白检查退出0;11非目标SHA256一致。修复前40用例27失败为Builder开发期记录,未冒称Runtime红灯;未实机/联机。 | +| A11 | passed | brief.md | 狼顾阴间梅花成功、红色失败,普通/困难仅黑桃成功;动态描述同步。 | langgu普通/困难仅spade,阴间color black;成功次数与合法他弃保留,动态描述注册链完整;9花色与3描述及友方用例通过。 | +| A12 | passed | brief.md | 妄尊阴间累计1点伤害即反击,困难1点不反击,未受伤时弃牌数仍为困难1/阴间2。 | wangzun对当前回合源伤害累计,nightmare阈值>0,其余>1;0伤困难弃1/阴间弃2;正式0/1/2点分支和描述通过。 | +| A13 | passed | brief.md | 夺玺阴间替换摸牌后仅自己摸2,困难双方各摸1;动态描述、提示同步。 | duoxi替换摸牌后失去1体力,nightmare仅存活自己摸2,hard双方各1;提示、动态文本和AI采用自己摸牌分支;正式分支及离场用例通过。 | +| A14 | passed | brief.md | 引雷困难自己回合不触发、阴间自己回合触发;描述同步回合条件。 | yinlei normal拒绝,hard本人回合拒绝、nightmare不限制回合;过滤未横置存活角色并随机横置;动态文本同步,正式4种回合分支通过。 | +| A15 | passed | brief.md | 张角三难度体力和上限8/16/24,起始手牌6/8/10和追加技能保留。 | tianshu/config.js:121张角独立加成4/12/20叠加基础4得到8/16/24,hp/maxHp一致,起手6/8/10与hard起追加引雷保留;正式三难度及曹操不变断言通过。 | +| A16 | passed | brief.md | 疑神随机获取各目标合法装备,正常难度分支只选一名敌人,阴间所有合格敌人各一张;禁止选择受保护装备。 | yishen取消回复后从每目标getGainableCards(player,e)随机取牌;非阴间随机一敌人,阴间所有合格敌人各一;未开放装备自选;正式合法/保护牌用例通过。 | +| A17 | passed | brief.md | 反馈受到多点伤害有逐点发动机会,可对友方来源发动,源牌耗尽后停止;自伤只获取自身装备。 | fankui getIndex返回damage num,引擎gameEvent.js:498-507数字展开N项,content.js:3435每轮重筛来源存活及可得牌;来源不限阵营,自伤只e;正式多点耗尽及自伤用例通过。 | +| A18 | passed | brief.md | 狼顾接受其他友方来源;暴虐满血也失去1体力并允许任意角色;雷击允许其他友方,谏征允许其他角色使用的杀,不增加阵营限制。 | langgu其他来源不限阵营;baonue满血跳过摸牌/选人仍loseHp,受伤可选任意存活角色;leiji其他角色、zhuzheng其他杀使用者/原目标无阵营硬限制;当前正式范围用例通过。 | +| A19 | passed | brief.md | 远虑阴间保留unreal并明确描述;无双/神戟/战甲增强不回退,全部改动描述与提示一致。 | 远虑nightmare仍unreal且文本写视为伤害;无双增伤与1/2次、神戟摸2/3/4和杀1/2/2、战甲阴间摸3保留;静态基准/动态难度/相关提示一致,正式增强用例通过。 | + +## 检查 + +| 检查 | 命令 | 工作目录 | 状态 | 退出码 | 耗时 | +| --- | --- | --- | --- | ---: | ---: | +| 青青子衿及全部相关天书回归 | --test tools/test_qingqingzijin_rules.mjs tools/test_skill_review_fixes.mjs tools/test_tianshu_direct_damage.mjs tools/test_tianshu_skill_recover.mjs tools/test_tianshu_stage_turn.mjs | . | passed | 0 | 415 ms | +| 工作区差异空白检查 | diff --check | . | passed | 0 | 120 ms | + +### Builder 报告的证据 + +以下为 Builder 报告,不等同于 Runtime 检查凭据或独立验收结果。 + +- 青青子衿定向回归: passed — 41/41通过,修复前40用例13通过27失败。 +- 生产JS语法: passed — skill/dynamicTranslate/translate/config语法通过。 +- 非目标文件保护: passed — 11个基线哈希一致,其他已有改动保留。 +- 已知限制: 未进行真实游戏、浏览器或联机实测。 +- 已知限制: 测试mjs沿用仓库现有忽略规则,当前文件保存在工作区,未暂存或提交。 + +## 阻塞项 + +- **user**: The generic Skill bridge cannot prove an independent Verifier execution; user confirmation is required before Archive. — next: `await-user` + +## 风险与跳过的工作 + +- 未进行真实游戏、浏览器界面或联机验证;ESM桥接/局部桩及引擎源码证据不等同于实机验证。 +- 测试mjs沿用既有忽略规则,文件在工作区尚未暂存或提交;未执行发布。 +- 修复前失败计数为Builder开发期观察,并非本轮Runtime失败回执;当前正式结果为101次通过、90个独立测试。 + +## 之前的迭代 + +| 目标周期 | 迭代 | 尝试 | 结果 | 未解决项 | 摘要 | 完成时间 | +| ---: | ---: | ---: | --- | --- | --- | --- | +| 1 | 1 | 1 | pass | — | 只读审查验收通过(A1-A4),此结论表示审查交付满足范围,不表示待审源码全部合规。当前工作区及既有未提交修改作为审查对象,梦三模式和梦三测试均排除;本 Verifier 未修改产品源码、未修复、未提交、未发布。 已确认事实:活跃路径共有 140 个顶层技能声明(character 5、驱鬼逐邪 30、青青子衿 21、天书 Boss 21、十二生肖 12、天书 bondSkills 28、新天书 extension 规则 23),74 个 subSkill;2 个显式 sourceSkill 均为 wushuang_shuying 的子技能,另外 72 个由引擎补充。当前禁载的原神另有 6 根、8 子技能,未计入活跃统计。D:/Work/noname/resources/app/noname/game/index.js:8251-8270 的 finishSkill 先注册父名_子名并在 sourceSkill===undefined 时补父名;docs/lib-skill-format.md:258-286 同样说明自动归属。不能将省略显式 sourceSkill 本身判为违规,也不能将子技能注册等同于玩家已经拥有。 按严重程度排序的 8 项实现问题: [P1] 天书羁绊 ESM 没有引入引擎对象。D:/Work/noname-shuying/tianshu/bondSkills.js:1-2 无 import,但 :335-342 用 lib、:559 用 get、:742 用 get、:745 用 game。获得舌战、南蛮、奇策等羁绊后,在普通非 dev 环境执行对应 filter/mod/content 会引用未定义对象。真实 ESM 直接导入后,Skill07.filter 与 Skill06.mod 独立复现 ReferenceError: get is not defined,Skill03.content 复现 lib is not defined。initTianshu 的形参不会成为另一个 ESM 模块的词法变量;引擎 init/index.js:512 只在 dev/debug 路径调用 cheat.i,library/index.js:8891-8897 的全局暴露不能当生产依赖。 [P1] 江东基业二三级 group 错名。D:/Work/noname-shuying/tianshu/bondSkills.js:112-113、:184-186 写 Count2/Count3,而父技能定义为 Skill2/Skill3。独立展开全部 117 根及其子技能后,恰有这 5 个 group 引用找不到。正确的 Skill2_draw/discard、Skill3_draw/discard/gain 已由 finishSkill 注册,但错误 group 未将它们展开挂载到持有者。tianshu/config.js:327、:333 确实授予二三级父技能,故升级后对应摸牌/弃牌伤害/获得牌伤害效果失效。 [P1] 驭布多目标重复全目标群伤。D:/Work/noname-shuying/pve/boss/sets/qingqingzijin/skill.js:643-652 监听 useCardToPlayered 未限定 isFirstTarget,:662-665 每次都遍历 trigger.targets。引擎 content.js:9258-9297 对每个目标建立该事件,:9270 仅首个设 isFirstTarget。友方一张杀指定 N 个存活目标时可判定 N 次,每次成功都对全部目标伤害;N=2 且都成功时,每人额外受到 2 次伤害。 [P2] 驭布花色与描述不一致。同文件 :657 用 get.color(card)==black,因此梅花也成功;translate.js:36 写黑桃。建议明确规则后统一条件和文本。 [P2] 狼顾按轮限制而文本按回合。同文件 :240 检查 used,:259 用 roundStart 清除;translate.js:16 写每回合限一次。某回合判定失败后,在同一轮下一角色回合仍被锁,直至下轮;成功时 :263 手动清除是另一条路径,不能弥补失败路径。 [P2] 奋威中途奖励获得不初始化 2 点蓄力。D:/Work/noname-shuying/character/skill.js:591 定义 beginMarkCount:2,:650-670 仅 enterGame/开局 phaseBefore 添加初始值,父技能没有对应获得时 init。天书 :63 通过 addAdditionalSkills 中途授予已有角色;引擎 player.js:10979-11016 为普通技能增添,不触发 enterGame。正常已在场角色在非开局获得奋威时没有初始 2 点蓄力,直到其他蓄力条件发生;翻译 character/translate.js:14 标为 2/8。 [P2] 他弃候选过滤错误,可绕过 canBeDiscarded 保护。D:/Work/noname-shuying/tianshu/bondSkills.js:335-342、:390-397 的舌战使用 cardDiscardable(持有者自己弃牌规则)筛选别人弃牌;狼顾 qingqingzijin/skill.js:262-267 先 countDiscardableCards,再从全部 h 随机选牌,同样未保护候选集合。混有可弃和禁止被他人弃置牌时可选中受保护牌,或错误排除仅禁止自弃而允许他弃的牌。引擎 library/index.js:11151 与 :11171 区分两套 mod;player.js:5273 的 getDiscardableCards(player,zone) 才提供对应他弃候选。 [P2] 白仪/九天随机获得忽略 canBeGained 候选。D:/Work/noname-shuying/pve/boss/sets/tianshu/skill.js:617-619、:711-718 只先检查 countGainableCards>0,实际从全 he/h 随机抽取。目标同时有可获得和受保护牌时可以抽中受保护牌并直接 gain。建议使用 getGainableCards(player,zone),引擎 player.js:5285-5302 按 canBeGained 过滤。 另有 1 项潜在 P2,规则意图待确认:五虎、天下无双、武安君等牌伤增益及戌狗未排除连环传播。D:/Work/noname-shuying/tianshu/bondSkills.js:419-453、:471-518、:785-842;pve/boss/sets/shiershengxiao/skill.js:484-490。引擎 library/skill.js:1962-1973 将已增伤 num 连同 card/source 再传给下一目标,这些 damageBegin1 条件仍可通过并再次加伤。已确认源码行为,但尚无法确认设计要求是否刻意允许传播再次叠加;不能未经规则确认直接把缺少 notLink 认作普遍违规。建议以同一火杀、两个连环目标实测并确认预期。 天书所有权/直伤/回复证据:extension.js:49-72 只记录奖励来源,:731-812 按当前奖励技能与译名去重,:946-1003 按独立羁绊来源同步、保留其他来源并在永久失去后重算;getDirectDamageOrigin 与 getSkillRecoverOrigin 按父事件向上追踪并用 sourceSkillFor 归属,分别拒绝用牌/响应/嵌套伤害/技能 cost 及桃园次级回复。extension.js:1419-1486 在最终伤害结算登记并按最终 num 摸牌,三级无来源仍保留原施术者记录;skillRecover.js:30-63 包装原生回复,处理满血与溢出并防止二次结算。异步执行依据本地 AsyncCompiler.js:7-10 与 ArrayCompiler.js:28,原 async 函数被调用时只传事件、触发、玩家和结果,不会给 bondSkills 注入 get/lib/game。 正式验证:Runtime 检查 tianshu-review-tests 在当前候选执行 node --test tools/test_tianshu_direct_damage.mjs tools/test_tianshu_skill_recover.mjs tools/test_tianshu_stage_turn.mjs,退出 0,41 次测试执行全部通过、0 失败。skillRecover 测试导入 directDamage fixture 同时重复登记其 11 个测试,因此是 30 个独立测试、41 次执行;不能说有 41 个独立场景。正式日志 .comet/runtime/native/changes/shuying-skill-tianshu-review/logs/checks/0efa4fae-105e-4e92-ac43-eb454f8e2798-tianshu-review-tests.log。测试覆盖父链归属、最终伤害、无来源保留、所有权剥离重算、桃园满血/溢出/次级回复与换关轮次;采用 VM/桩,给 bond 技能注入全局对象,所以无法反证真实 ESM 导入缺陷。Builder 的 10 文件语法通过仅是开发期证据,不等于游戏运行。未做真实游戏、联机、dev 关闭浏览器联调;受保护卡随机结果、复杂事件父链以及连环规则意图仍需游戏复核。 建议优先修复 3 项 P1,再处理 5 项 P2;本只读任务未实施建议。以上审查与限制均已记录,A1-A4 通过不要求本次修复源码。 | 2026-10-12T03:43:33.267Z | +| 1 | 1 | 1 | recovery | — | 用户接受审查结果并明确要求修复报告中的8项已确认问题;用户确认连环传导允许再次增伤。将只读审查范围扩展为这些问题的最小修复与验证,沿用当前目录,保留既有改动。 | 2026-10-12T03:54:53.001Z | +| 2 | 1 | 1 | pass | — | 本轮独立验收A1–A10全部通过,五轴代码复核未发现需要阻断本次修复的问题。4个技能源码文件实际24行新增、40行删除:补羁绊引擎依赖导入及江东二三级分组,修正驭布首目标判定和黑桃条件、狼顾回合次数与成功空牌恢复,接入奋威动态init,修正舌战/狼顾他弃和白仪/九天他取候选。保留连环再次增伤,未触及梦三、停用原神或用户已有天书直伤/回复实现。 正式证据复用skill-fix-regressions和skill-diff-whitespace的passed结果(14新增用例及旧天书测试合计55次执行、44个独立用例),补充fenwei-engine-lifecycle正式passed,实际提取并调用当前引擎addSkill/addSkillTrigger/removeSkillTrigger/addCharge/getMaxCharge/expandSkills,验证初始值、去重、共享蓄力、失去后再获得和封顶。8个非目标文件SHA256与修复前baseline完全一致。红绿历史为Builder会话观察,未保存为Runtime红灯回执;Verifier对照before字节确认原条件与本次修正。 正确性方面已追踪用牌首目标事件队列、临时技能过期、card mods当前事件/发动者/牌主上下文与技能获得失去;可读性及架构遵循现有最小修改与引擎规范API,未新增通用抽象;安全性未引入网络、凭据或不可信输入通路;性能以一次判定替代每目标重复判定,其余只过滤有限牌集合。 额外考察但未纳入:固定羁绊技能旧hook未立即删除,但引擎执行前核对当前所有权,因此不构成低级叠加,属于原有管理流程;初始化在新技能持有期再加2且受共享容量限制符合引擎蓄力语义;白仪后段自弃仍使用cardDiscardable符合其“其随机弃置”描述;连环再次增伤是用户明确决定,不作为缺陷;统一导入中未使用ui/ai/_status沿用模块习惯,不为风格增加无关修改。没有真实游戏、浏览器界面或联机实测,本轮引擎检查仅为隔离方法验证。 | 2026-10-12T04:16:35.786Z | +| 2 | 1 | 1 | recovery | — | 用户核实驭布规则并明确要求改动:困难hard黑桃,阴间nightmare黑色;保留此前多目标一张杀只判定一次的修复,同步动态描述和难度分支回归验证。 | 2026-10-12T04:26:08.214Z | +| 3 | 1 | 1 | pass | — | 独立只读验收A1-A10全部通过。本轮相对前轮仅更新2个源码文件:pve/boss/sets/qingqingzijin/skill.js的驭布难度条件与dynamicTranslate.js的驭布动态描述;另增补tools/test_skill_review_fixes.mjs难度/描述用例。hard黑桃、nightmare黑色、默认非阴间黑桃;保留isFirstTarget每张多目标杀只判一次及存活目标一次伤害,动态描述经既有注册链实时读取难度。复用当前候选正式qubu-difficulty-regressions和qubu-diff-whitespace的passed记录,不重复整套:19定向用例与旧天书测试合计60次执行、49个独立用例。修改前17pass/2fail和修改后19pass的红绿对照为Builder会话观察,非Runtime红灯记录。12非目标文件SHA256独立复核均与本轮baseline一致,包括.gitignore/update.txt及前轮其他修复文件;复用前轮匹配的源码/引擎证据核对其余验收项。未修改源码、测试、忽略规则、索引或HEAD,未提交发布;未做真实游戏、浏览器界面或联机实测,ESM与局部桩结果不等同于实机验证。 | 2026-10-12T04:35:16.773Z | +| 3 | 1 | 1 | recovery | — | 用户逐项确认官网差异修复:保留远虑虚拟伤害及无双神戟战甲增强;修复狼顾妄尊夺玺引雷、张角体力和报告列出的范围差异,同步文本。 | 2026-10-12T06:23:10.262Z | +| 4 | 1 | 1 | pass | — | 独立只读Verifier已读完brief、完整Spec及A1-A19,逐项全部通过。复用绑定当前候选c9724c63-8c45-44ab-944c-4681834c1755的正式回归101次执行全通过(90独立测试,11重复fixture)和空白检查,不重复整套。核对本轮狼顾黑色、妄尊1点、夺玺自己摸2及AI、引雷不限回合、张角8/16/24,全部范围修复及保留增强/连环规则;追踪动态描述注册与数字getIndex引擎调度。11个非目标文件SHA256与基线完全一致。未编辑源码/测试/文档/gitignore、未执行git操作、记忆更新或发布,未实机/联机。 | 2026-10-12T06:41:40.687Z | + + + +## 结论 + +独立只读Verifier已读完brief、完整Spec及A1-A19,逐项全部通过。复用绑定当前候选c9724c63-8c45-44ab-944c-4681834c1755的正式回归101次执行全通过(90独立测试,11重复fixture)和空白检查,不重复整套。核对本轮狼顾黑色、妄尊1点、夺玺自己摸2及AI、引雷不限回合、张角8/16/24,全部范围修复及保留增强/连环规则;追踪动态描述注册与数字getIndex引擎调度。11个非目标文件SHA256与基线完全一致。未编辑源码/测试/文档/gitignore、未执行git操作、记忆更新或发布,未实机/联机。 diff --git a/docs/reviews/qingqingzijin-official-description-review-2026-10-12.md b/docs/reviews/qingqingzijin-official-description-review-2026-10-12.md new file mode 100644 index 0000000..4d152f0 --- /dev/null +++ b/docs/reviews/qingqingzijin-official-description-review-2026-10-12.md @@ -0,0 +1,79 @@ +# 青青子衿 BOSS 官网对照核实 + +核实日期:2026-10-12。依据用户指定的[三国杀官网公告](https://www.sanguosha.com/news/20210916_6804_0316),网页显示发布时间为 2021-09-23。这里只核实该公告,不推定它代表当前官方客户端规则。 + +更新:用户已逐项确认修复范围。现已修正狼顾、妄尊、夺玺、引雷、张角体力,以及下述范围差异全部条目,并同步描述和提示。远虑阴间虚拟伤害、无双/神戟/战甲增强按用户决定保留,远虑文本已明确“视为”。下文“当前源码”及“本轮没有修改”是修复前核实记录;最终验收结果见 `../comet/changes/shuying-skill-tianshu-review/verification.md`。 + +难度映射:官网休闲 → 术樱普通(normal);官网普通 → 术樱困难(hard);官网困难 → 术樱阴间(nightmare)。范围为六名 BOSS、22 个不同技能,排除梦三及网页的虚拟偶像技能。 + +本轮没有修改技能源码、配置、测试或 Comet 正式验收文件。上轮 Comet 的 60 次通过是已确认修复范围的验收,不能证明所有技能与本公告相符。 + +## 已确认的主要差异 + +| 技能 | 公告基准(按术樱难度表达) | 当前源码及描述 | 位置 | +| --- | --- | --- | --- | +| 狼顾 | 阴间判定接受黑色 | 所有难度仅接受黑桃;只有黑桃静态描述,未注册动态描述 | `skill.js:263`、`translate.js:16` | +| 远虑 | 困难、阴间均为真实伤害 | 阴间额外传入 `unreal`;动态描述多出“过”字 | `skill.js:296`、`dynamicTranslate.js:24` | +| 无双 | 无增伤条款 | 主技能另外增加未响应时伤害 +1,普通/困难每回合 1 次、阴间 2 次;描述包含此增强 | `skill.js:309`、`dynamicTranslate.js:32` | +| 神戟 | 各难度额外摸牌均为 2;困难额外杀为 1 | 困难额外摸 3、额外杀 2;阴间额外摸 4。动态描述同样采用这些数值 | `skill.js:448`、`skill.js:459`、`dynamicTranslate.js:37` | +| 战甲 | 阴间仍摸 2 | 阴间摸 3,动态描述也写 3 | `skill.js:533`、`dynamicTranslate.js:54` | +| 妄尊 | 阴间反击门槛为造成过伤害 | 所有难度均要求累计伤害 >1;阴间造成恰好 1 点时不反击。动态描述同样未切换门槛 | `skill.js:769`、`skill.js:800`、`dynamicTranslate.js:78` | +| 夺玺 | 阴间只由自己摸 2 | 阴间让自己与摸牌角色各摸 2;提示、动态描述也如此 | `skill.js:826`、`skill.js:862`、`dynamicTranslate.js:84` | +| 引雷 | 困难仅回合外;阴间不限回合 | 源码在所有难度排除自己的回合;静态描述又没有回合外条件,没有动态描述 | `skill.js:1158`、`translate.js:54` | + +这些是与指定公告的确定差异。是否保留包内增强属于产品决定,尤其无双;此前“允许再次增伤”的决定仍保留,本报告不据此擅自删除增强。 + +## 描述与实现还存在的范围差异 + +- **疑神**:描述与公告一致。源码 `skill.js:118–126` 使用可操作的 `gainPlayerCard` 选装备,未在代码中随机选装备;判定目标的困难分支随机选择的是有可获得装备的敌人。若按公告要求随机装备,需要更换选牌方式。 +- **反馈**:描述保留逐点触发含义;源码 `skill.js:158–174` 每个 `damageEnd` 只获得一张牌,未按 `trigger.num` 逐点处理,且只接受敌方来源。引擎伤害流程对一次多点伤害只有一个 `damageEnd`。这些限制未体现在描述中。 +- **狼顾**:除主要差异外,`skill.js:245、254` 把牌的原持有者限制为敌人;描述没有此限制。 +- **暴虐**:描述数值上限 3/4/5 正确。`skill.js:613` 在满血时完全跳过技能,因此不会执行最后的失去体力;`skill.js:625` 也把可伤害角色限制为敌人,描述未写该限制。 +- **雷击**:描述一致,`skill.js:1002–1005` 选人范围只含敌人。 +- **谏征**:描述正确切换黑色杀的接管规则;`skill.js:1063、1070` 还要求使用者为敌人,且已有目标含非敌人,描述没有这两项条件。 + +上述阵营限制可能是包内 AI/模式适配,但这是推断,不能作为与公告一致的证明。 + +## 22 个技能的覆盖结果 + +“核心一致”仅表示本轮未发现公告主要条款或描述的明确差异,不等于真实游戏、联机或所有交互已验证。 + +| BOSS | 技能 | 核实结果 | +| --- | --- | --- | +| 曹操 | 奸雄 | 核心一致 | +| 曹操 | 凌霸 | 核心一致,阴间伤害切换正确 | +| 曹操 | 疑神 | 描述一致,随机选装备方式不符 | +| 司马懿 | 反馈 | 描述一致,逐点触发与来源范围不符 | +| 司马懿 | 鬼才 | 核心一致 | +| 司马懿 | 狼顾 | 阴间判定与描述不符,另有限制来源阵营 | +| 司马懿 | 远虑 | 阴间使用虚拟伤害,另有描述错字 | +| 吕布 | 马术 | 引用引擎标准技能,核心一致 | +| 吕布 | 无双 | 包内额外增伤,无公告依据 | +| 吕布 | 神戟 | 困难及阴间数值不符 | +| 吕布 | 战甲 | 阴间摸牌数不符 | +| 董卓 | 酒池 | 核心一致 | +| 董卓 | 肉林 | 核心一致 | +| 董卓 | 暴虐 | 描述及上限正确,满血触发及选人范围不符 | +| 董卓 | 驭布 | 困难黑桃、阴间黑色;描述与修复后实现一致 | +| 袁术 | 庸肆 | 核心条款及描述一致 | +| 袁术 | 妄尊 | 阴间反击条件不符 | +| 袁术 | 夺玺 | 阴间摸牌对象不符 | +| 张角 | 鬼道 | 核心一致 | +| 张角 | 雷击 | 描述一致,选人范围不符 | +| 张角 | 谏征 | 难度描述一致,额外阵营限制未写明 | +| 张角 | 引雷 | 困难描述缺条件,阴间实现多条件 | + +## 顺带核实的难度配置 + +`tianshu/config.js:116–121` 中六名 BOSS 追加技能均从困难开始授予,与映射后的公告一致。起始手牌 6/8/10 一致。五名 BOSS 的体力 10/20/30 一致;张角公告为 8/16/24,当前原始体力 4 加共用模板 6/16/26,实际配置为 10/20/30。 + +张角相关位置:`pve/boss/sets/qingqingzijin/character.js:60`、`tianshu/config.js:56`、`tianshu/config.js:121`;体力生成链:`tianshu/extension.js:438–439`。 + +## 验证证据与限制 + +- 成功下载官方原始 HTML,逐表核对六名 BOSS 的三个难度;网页桌面版/移动版重复表格内容相同。 +- 对现有真实模块进行临时定向调用:确认神戟三难度数值、阴间战甲摸 3、狼顾梅花失败、妄尊恰好 1 点不反击、夺玺两方各摸 2、远虑传入 `unreal`、引雷自己回合不触发、暴虐满血不触发。 +- 引擎 `noname/library/element/content.js:11120` 在 `unreal` 时不扣体力,所以远虑差异影响实际体力,并非日志措辞差异。 +- 驭布沿用当前候选的难度与多目标回归证据。本轮未重复执行整个修复套件,也未进行游戏或联机实测。 + +所有未列明完整路径的技能文件,均位于 `D:/Work/noname-shuying/pve/boss/sets/qingqingzijin/`。 diff --git a/function.js b/function.js index 913ae71..761162c 100644 --- a/function.js +++ b/function.js @@ -1,3 +1,6 @@ +import { getDirectDamageSkillCount } from "./tianshu/directDamage.js"; +import { getSkillRecoverCount } from "./tianshu/skillRecover.js"; + export default function initFunction(lib, game, ui, get, ai, _status, shuYing = {}) { game.shuying_getDiscardNamesNum = function () { return Array.from(ui.discardPile.childNodes) @@ -386,6 +389,14 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => matchesCondition(item, currentSkills)); if (!condition.type && condition.not) return !matchesCondition(condition.not, currentSkills); if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) >= Math.max(1, Number(condition.count) || 1); + if (condition.type == "directDamage") { + return getDirectDamageSkillCount(player, currentSkills, lib) + >= Math.max(1, Number(condition.count) || 1); + } + if (condition.type == "skillRecover") { + return getSkillRecoverCount(player, currentSkills, lib) + >= Math.max(1, Number(condition.count) || 1); + } if (condition.type == "skill") return currentSkills.includes(condition.skill); if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name); if (condition.type == "skills") { @@ -427,6 +438,13 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = if (!condition.type && Array.isArray(condition.any)) return condition.any.some(item => isConditionRelevant(item, currentSkills)); if (!condition.type && condition.not) return false; if (condition.type == "keyword") return getKeywordCount(condition.keyword, currentSkills) > 0; + if (condition.type == "directDamage") { + return getDirectDamageSkillCount(player, currentSkills, lib) + > 0; + } + if (condition.type == "skillRecover") { + return getSkillRecoverCount(player, currentSkills, lib) > 0; + } if (condition.type == "skill") return currentSkills.includes(condition.skill); if (condition.type == "skillName") return currentSkills.some(skill => lib.translate[skill] == condition.name); if (condition.type == "skills") return [...(condition.all || []), ...(condition.any || [])].some(skill => currentSkills.includes(skill)); @@ -451,6 +469,19 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing = const need = Math.max(1, Number(condition.count) || 1); return `包含【${condition.keyword}】的技能 ${Math.min(count, need)}/${need}`; } + if (condition.type == "directDamage") { + const count = getDirectDamageSkillCount( + player, currentSkills, lib); + const need = Math.max(1, Number(condition.count) || 1); + return `已实际造成直伤的奖励技能 ${Math.min(count, need)}` + + `/${need};首次有效直伤后登记`; + } + if (condition.type == "skillRecover") { + const count = getSkillRecoverCount(player, currentSkills, lib); + const need = Math.max(1, Number(condition.count) || 1); + return `已实际回复体力的奖励技能 ${Math.min(count, need)}` + + `/${need};首次有效技能回复后登记`; + } if (condition.type == "skill") return `${currentSkills.includes(condition.skill) ? "已" : "未"}选择【${get.translation(condition.skill)}】`; if (condition.type == "skillName") return `${currentSkills.some(skill => lib.translate[skill] == condition.name) ? "已" : "未"}选择任意版本的【${condition.name}】`; if (condition.type == "skills") { diff --git a/menu.js b/menu.js index f62797f..066f0b6 100644 --- a/menu.js +++ b/menu.js @@ -72,6 +72,12 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { ].filter(Boolean))); } if (condition.type == "keyword") return condition.keyword ? [`含【${condition.keyword}】的技能`] : []; + if (condition.type == "directDamage") { + return ["直接造成技能伤害的过关技能"]; + } + if (condition.type == "skillRecover") { + return ["直接回复体力的过关技能"]; + } if (condition.type == "skill") return condition.skill ? [get.translation(condition.skill)] : []; if (condition.type == "skillName") return condition.name ? [condition.name] : []; if (condition.type == "skills") { @@ -93,6 +99,15 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) { return `武将译名包含【${characters.filter(Boolean).join("】或【")}】且取得上述过关技能后触发`; } if (condition.type == "keyword") return `按描述中包含【${condition.keyword}】的不同技能译名统计`; + if (condition.type == "directDamage") { + return "奖励技能首次直接造成有效伤害后登记,子技能归到主技能;" + + "按当前仍持有的不同技能译名统计,新等级从后续伤害生效"; + } + if (condition.type == "skillRecover") { + return "奖励技能首次实际回复体力后登记,子技能归到主技能;" + + "按当前仍持有的不同技能译名统计,新等级从后续回复生效;" + + "二级追加回复不会再次触发桃园,三级溢出只增加体力上限"; + } if (!condition.type && [...(condition.all || []), ...(condition.any || [])].some(item => item?.type == "skillNames")) { return "按不同译名统计;同译名的不同技能 ID 只计 1 种过关技能"; } diff --git a/pve/boss/sets/qingqingzijin/dynamicTranslate.js b/pve/boss/sets/qingqingzijin/dynamicTranslate.js index 0f48165..ce87cb0 100644 --- a/pve/boss/sets/qingqingzijin/dynamicTranslate.js +++ b/pve/boss/sets/qingqingzijin/dynamicTranslate.js @@ -17,11 +17,18 @@ const dynamicTranslates = { }, //司马懿 + langgu_shuying(player, skill) { + const condition = getTianshuDifficulty() == "nightmare" + ? "黑色" : "黑桃"; + + return "锁定技,每回合限一次,当你获得其他角色的牌时,进行一次判定:" + + `若结果为${condition},随机弃置其1张手牌,且视为此技能本回合未发动过。`; + }, yuanlv_shuying(player, skill) { const difficulty = getTianshuDifficulty(); if (difficulty == "nightmare") { - return "当你使用锦囊牌对敌方角色造成伤害时,你可以防止该伤害,改为摸一张牌且该敌方角色对你造成过1点伤害。"; + return "当你使用锦囊牌对敌方角色造成伤害时,你可以防止该伤害,改为摸一张牌且视为受到该敌方角色造成的1点伤害。"; } return "当你使用锦囊牌对敌方角色造成伤害时,你可以防止该伤害,改为摸一张牌且该敌方角色对你造成1点伤害。"; @@ -66,18 +73,29 @@ const dynamicTranslates = { return `锁定技,回合开始时,你摸X张牌并对至多X名角色造成1点伤害,然后你失去1点体力。X为你已损失体力且最大为${num}。` }, + qubu_shuying(player, skill) { + const condition = getTianshuDifficulty() == "nightmare" + ? "黑色" : "黑桃"; + + return "锁定技,当友方角色使用【杀】指定目标时,进行一次判定:" + + `若结果为${condition},你对此【杀】的所有目标造成1点伤害。`; + }, + //袁术 wangzun_shuying(player, skill) { const num = getTianshuDifficulty() == "nightmare" ? 2 : 1; + const condition = getTianshuDifficulty() == "nightmare" + ? "对你造成过伤害" : "对你造成的伤害数超过1点"; - return `锁定技,其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置${num}张牌;2.对你造成的伤害数超过1点,则你对其造成1点伤害。`; + return `锁定技,其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置${num}张牌;2.${condition},则你对其造成1点伤害。`; }, duoxi_shuying(player, skill) { - const num = - getTianshuDifficulty() == "nightmare" ? 2 : 1; + if (getTianshuDifficulty() == "nightmare") { + return "其他角色的摸牌阶段,你可以失去1点体力,改为你摸2张牌。"; + } - return `其他角色的摸牌阶段,你可以失去1点体力,改为你与其各摸${num}张牌。`; + return "其他角色的摸牌阶段,你可以失去1点体力,改为你与其各摸1张牌。"; }, //张角 @@ -88,6 +106,12 @@ const dynamicTranslates = { return "其他角色使用【杀】指定除你外的角色时,若你在其攻击范围内,你可以将一张手牌置于牌堆顶,取消所有目标,然后若此【杀】不为黑色,你成为目标。"; }, + yinlei_shuying(player, skill) { + const condition = getTianshuDifficulty() == "nightmare" + ? "失去牌" : "于回合外失去牌"; + + return `锁定技,当你${condition}时,随机横置一名角色。`; + }, }; export default dynamicTranslates; diff --git a/pve/boss/sets/qingqingzijin/skill.js b/pve/boss/sets/qingqingzijin/skill.js index 35b10a5..4e158a1 100644 --- a/pve/boss/sets/qingqingzijin/skill.js +++ b/pve/boss/sets/qingqingzijin/skill.js @@ -114,14 +114,19 @@ const skills = { if (difficulty == "nightmare") { for (const target of enemies) { + const card = target.getGainableCards(player, "e").randomGet(); + if (!card) continue; player.line(target); - await player.gainPlayerCard({ target, position: "e", forced: true }); + await player.gain(card, target, "give"); } } else { const target = enemies.randomGet(); + if (!target) return; + const card = target.getGainableCards(player, "e").randomGet(); + if (!card) return; player.line(target); - await player.gainPlayerCard({ target, position: "e", forced: true }); + await player.gain(card, target, "give"); } }, ai: { @@ -157,18 +162,20 @@ const skills = { audio: "ext:术樱包/pve/audio/skill/青青子衿:1", trigger: { player: "damageEnd" }, logTarget: "source", + getIndex(event) { + return event.num; + }, filter(event, player) { return ( event.num > 0 && event.source?.isIn() && - player.getEnemies(null, false).includes(event.source) && - event.source.hasGainableCards(player, "he") + event.source.hasGainableCards(player, event.source == player ? "e" : "he") ); }, async content(event, trigger, player) { await player.gainPlayerCard({ target: trigger.source, - position: "he", + position: trigger.source == player ? "e" : "he", forced: true, }); }, @@ -241,7 +248,6 @@ const skills = { return game.hasPlayer(current => { return ( current != player && - player.getEnemies(null, false).includes(current) && event.getl?.(current)?.cards2?.length ); }); @@ -250,20 +256,31 @@ const skills = { const source = game.findPlayer(current => { return ( current != player && - player.getEnemies(null, false).includes(current) && trigger.getl?.(current)?.cards2?.length ); }); if (!source) return; - player.addTempSkill("langgu_shuying_used", "roundStart"); + player.addTempSkill("langgu_shuying_used", { + global: "phaseAfter", + }); - const result = await player.judge(card => get.suit(card) == "spade" ? 2 : 0).forResult(); - if (result.bool && source.isIn() && source.countDiscardableCards(player, "h") > 0) { + const isNightmare = getTianshuDifficulty() == "nightmare"; + const result = await player.judge(card => { + const success = isNightmare + ? get.color(card) == "black" + : get.suit(card) == "spade"; + return success ? 2 : 0; + }).forResult(); + if (result.bool) { player.removeSkill("langgu_shuying_used"); + if (!source.isIn()) return; + const card = source.getDiscardableCards(player, "h").randomGet(); + if (!card) return; player.line(source); - const card = source.getCards("h").randomGet(); - await source.discard(card); + const discardEvent = source.discard(card); + discardEvent.discarder = player; + await discardEvent; } }, subSkill: { @@ -603,31 +620,30 @@ const skills = { trigger: { player: "phaseBegin" }, forced: true, locked: true, - filter(event, player) { - return player.getDamagedHp() > 0; - }, async content(event, trigger, player) { let maxNum = 3; if (getTianshuDifficulty() == "hard") maxNum = 4; if (getTianshuDifficulty() == "nightmare") maxNum = 5; const num = Math.min(maxNum, player.getDamagedHp()); - await player.draw(num); + if (num > 0) { + await player.draw(num); - const result = await player - .chooseTarget(`对至多${get.cnNumber(num)}名角色造成1点伤害`, [1, num], (card, player, target) => { - return target.isIn() && player.getEnemies(null, false).includes(target); - }) - .set("ai", target => { - const player = get.player(); - return get.damageEffect(target, player, player); - }) - .forResult(); + const result = await player + .chooseTarget(`对至多${get.cnNumber(num)}名角色造成1点伤害`, [1, num], (card, player, target) => { + return target.isIn(); + }) + .set("ai", target => { + const player = get.player(); + return get.damageEffect(target, player, player); + }) + .forResult(); - if (result.bool) { - for (const target of result.targets) { - player.line(target); - await target.damage(1, player); + if (result.bool) { + for (const target of result.targets) { + player.line(target); + await target.damage(1, player); + } } } @@ -645,6 +661,7 @@ const skills = { locked: true, filter(event, player) { return ( + event.isFirstTarget && event.card.name == "sha" && event.player?.isIn() && !player.getEnemies(null, false).includes(event.player) && @@ -652,8 +669,14 @@ const skills = { ); }, async content(event, trigger, player) { + const isNightmare = getTianshuDifficulty() == "nightmare"; const result = await player - .judge(card => get.color(card) == "black" ? 2 : -1) + .judge(card => { + const success = isNightmare + ? get.color(card) == "black" + : get.suit(card) == "spade"; + return success ? 2 : -1; + }) .set("judge2", result => result.bool) .forResult(); @@ -753,7 +776,7 @@ const skills = { return sum; }, 0); - if (damageNum > 1) return true; + if (damageNum > (getTianshuDifficulty() == "nightmare" ? 0 : 1)) return true; return ( damageNum == 0 && @@ -784,7 +807,7 @@ const skills = { if (num > 0) { await target.chooseToDiscard(num, "he", true); } - } else if (damageNum > 1 && target.isIn()) { + } else if (damageNum > (getTianshuDifficulty() == "nightmare" ? 0 : 1) && target.isIn()) { await target.damage(1, player); } }, @@ -807,11 +830,14 @@ const skills = { async cost(event, trigger, player) { const num = getTianshuDifficulty() == "nightmare" ? 2 : 1; + const drawText = getTianshuDifficulty() == "nightmare" + ? `你摸${get.cnNumber(num)}张牌` + : `你与其各摸${get.cnNumber(num)}张牌`; event.result = await player .chooseBool( get.prompt(event.skill, trigger.player), - `失去1点体力,将${get.translation(trigger.player)}的摸牌阶段改为你与其各摸${get.cnNumber(num)}张牌` + `失去1点体力,将${get.translation(trigger.player)}的摸牌阶段改为${drawText}` ) .set("ai", () => { const player = get.player(); @@ -823,10 +849,12 @@ const skills = { : 1; if (player.hp <= 1) return false; - const isEnemy = player .getEnemies(null, false) .includes(target); + if (getTianshuDifficulty() == "nightmare") { + return isEnemy && player.hp > 2; + } // 避免额外增加敌方摸牌,或削减友方原本的摸牌数 if (isEnemy && trigger.num < num) return false; @@ -846,7 +874,8 @@ const skills = { await player.loseHp(); - const targets = [player, target].filter( + const targets = (getTianshuDifficulty() == "nightmare" + ? [player] : [player, target]).filter( current => current.isIn() ); if (targets.length) { @@ -972,12 +1001,8 @@ const skills = { audio: "ext:术樱包/pve/audio/skill/青青子衿:1", trigger: { player: ["useCard", "respond"] }, filter(event, player) { - return ( - event.card.name == "shan" && - player - .getEnemies(null, false) - .some(target => target.isIn()) - ); + return event.card.name == "shan" + && game.hasPlayer(target => target != player && target.isIn()); }, line: "thunder", async cost(event, trigger, player) { @@ -985,12 +1010,7 @@ const skills = { .chooseTarget( get.prompt2(event.skill), (card, player, target) => { - return ( - target != player && - player - .getEnemies(null, false) - .includes(target) - ); + return target != player; } ) .set("ai", target => { @@ -1046,16 +1066,12 @@ const skills = { !event.targets?.length || event.targets.includes(player) || !source.inRange(player) || - player.countCards("h") == 0 || - !player.getEnemies(null, false).includes(source) + player.countCards("h") == 0 ) { return false; } - const enemies = player.getEnemies(null, false); - return event.targets.some(target => { - return target.isIn() && !enemies.includes(target); - }); + return true; }, async cost(event, trigger, player) { event.result = await player @@ -1144,7 +1160,7 @@ const skills = { filter(event, player) { if ( getTianshuDifficulty() == "normal" || - _status.currentPhase == player + (getTianshuDifficulty() != "nightmare" && _status.currentPhase == player) ) { return false; } diff --git a/pve/boss/sets/qingqingzijin/translate.js b/pve/boss/sets/qingqingzijin/translate.js index 55b1b08..c48a94a 100644 --- a/pve/boss/sets/qingqingzijin/translate.js +++ b/pve/boss/sets/qingqingzijin/translate.js @@ -13,7 +13,7 @@ const translates = { guicai_shuying: "鬼才", guicai_shuying_info: "当一名角色的判定牌生效前,你可以打出一张牌代替之。", langgu_shuying: "狼顾", - langgu_shuying_info: " 锁定技,每回合限一次,当你获得其他角色的牌时,进行一次判定:若结果为黑桃,随机弃置其1张手牌,且视为此技能本回合未发动过。", + langgu_shuying_info: "锁定技,每回合限一次,当你获得其他角色的牌时,进行一次判定:若结果为黑桃,随机弃置其1张手牌,且视为此技能本回合未发动过。", yuanlv_shuying: "远虑", yuanlv_shuying_info: "当你使用锦囊牌对敌方角色造成伤害时,你可以防止该伤害,改为摸一张牌且该敌方角色对你造成1点伤害。", @@ -39,7 +39,7 @@ const translates = { yongsi_shuying: "庸肆", yongsi_shuying_info: "锁定技,摸牌阶段,你改为摸X张牌(X为存活势力数);弃牌阶段,若你本回合:1.没有造成伤害,将手牌摸至当前体力值;2.造成伤害数超过1点,本回合手牌上限改为已损失体力值。", wangzun_shuying: "妄尊", - wangzun_shuying_info: "锁定技,其他角色其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置1张牌;2.对你造成伤害数超过1点,则你对其造成1点伤害。", + wangzun_shuying_info: "锁定技,其他敌方角色的结束阶段,若其本回合:1.没有对你造成伤害,则其弃置1张牌;2.对你造成伤害数超过1点,则你对其造成1点伤害。", duoxi_shuying: "夺玺", duoxi_shuying_info: "其他角色的摸牌阶段,你可以失去1点体力改为你与其各摸一张牌。", @@ -51,7 +51,7 @@ const translates = { zhuzheng_shuying: "谏征", zhuzheng_shuying_info: "其他角色使用【杀】指定除你外的角色时,若你在其攻击范围内,你可以将一张手牌置于牌堆顶,取消所有目标,然后若此【杀】不为黑色,你成为目标。", yinlei_shuying: "引雷", - yinlei_shuying_info: "锁定技,当你失去牌时,随机横置一名角色。", + yinlei_shuying_info: "锁定技,当你于回合外失去牌时,随机横置一名角色。", }; export default translates; diff --git a/pve/boss/sets/tianshu/skill.js b/pve/boss/sets/tianshu/skill.js index fee27b1..e9211c8 100644 --- a/pve/boss/sets/tianshu/skill.js +++ b/pve/boss/sets/tianshu/skill.js @@ -615,7 +615,7 @@ const skills = { } if (game.roundNumber < 3 && target.countGainableCards(player, "he") > 0) { - const cards = target.getCards("he").randomGets(Math.min(num, target.countCards("he"))); + const cards = target.getGainableCards(player, "he").randomGets(num); await player.gain(cards, target, "giveAuto"); } @@ -712,7 +712,7 @@ const skills = { const gained = []; for (const target of targets) { - const cards = target.getCards("h").randomGets(Math.min(num, target.countCards("h"))); + const cards = target.getGainableCards(player, "h").randomGets(num); if (cards.length) { player.line(target); await player.gain(cards, target, "giveAuto"); diff --git a/tianshu/bondSkills.js b/tianshu/bondSkills.js index a11dd00..b0ddf81 100644 --- a/tianshu/bondSkills.js +++ b/tianshu/bondSkills.js @@ -1,4 +1,6 @@ // 天书羁绊只通过技能表达具体效果;羁绊管理器负责决定当前应持有哪一级技能。 +import { lib, game, ui, get, ai, _status } from "../../../noname.js"; + const skill = { shuYing_Tianshu_BondSha1: { charlotte: true, @@ -109,8 +111,8 @@ const skill = { locked: true, popup: false, group: [ - "shuYing_Tianshu_Skill_02_Count2_draw", - "shuYing_Tianshu_Skill_02_Count2_discard", + "shuYing_Tianshu_Skill_02_Skill2_draw", + "shuYing_Tianshu_Skill_02_Skill2_discard", ], subSkill: { draw: { @@ -181,9 +183,9 @@ const skill = { locked: true, popup: false, group: [ - "shuYing_Tianshu_Skill_02_Count3_draw", - "shuYing_Tianshu_Skill_02_Count3_discard", - "shuYing_Tianshu_Skill_02_Count3_gain", + "shuYing_Tianshu_Skill_02_Skill3_draw", + "shuYing_Tianshu_Skill_02_Skill3_discard", + "shuYing_Tianshu_Skill_02_Skill3_gain", ], subSkill: { draw: { @@ -334,13 +336,7 @@ const skill = { async content(event, trigger, player) { const target = trigger.target; const cards = target - .getCards("he", card => { - return lib.filter.cardDiscardable( - card, - target, - event.name - ); - }) + .getDiscardableCards(player, "he") .randomGets(1); if (!cards.length) return; @@ -389,13 +385,7 @@ const skill = { } const cards = target - .getCards("he", card => { - return lib.filter.cardDiscardable( - card, - target, - event.name - ); - }) + .getDiscardableCards(player, "he") .randomGets(2); if (!cards.length) return; diff --git a/tianshu/config.js b/tianshu/config.js index f0400e5..a6cf4e5 100644 --- a/tianshu/config.js +++ b/tianshu/config.js @@ -118,7 +118,11 @@ const bossDifficulty = { lvbu_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("zhanjia_shuying")), dongzhuo_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("qubu_shuying")), yuanshu_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("duoxi_shuying")), - zhangjiao_boss_shuying: createBossConfig(bossDifficultyTemplates.qingqingzijin, addSkillFromHard("yinlei_shuying")), + zhangjiao_boss_shuying: createBossConfig({ + normal: createDifficultyConfig(6, 4), + hard: createDifficultyConfig(8, 12), + nightmare: createDifficultyConfig(10, 20), + }, addSkillFromHard("yinlei_shuying")), ...createBossGroup(["shaohao_boss_shuying", "xuannv_boss_shuying", "hanba_boss_shuying"], bossDifficultyTemplates.tianshuluandou), shuishengonggong_boss_shuying: createBossConfig(bossDifficultyTemplates.tianshuluandou, { @@ -210,6 +214,60 @@ const tianshuConfig = { banned: ["huoxin", "jueqing", "qinqing", "beige", "huashen", "drlt_zhiti", "olzhiti", "xinfu_pdgyingshi", "rebeige"], }, bonds: [ + { + id: "taoyuan", + name: "桃园", + condition: { type: "skillRecover" }, + levels: [ + { + level: 1, + count: 1, + skills: ["shuYing_Tianshu_BondTaoyuan1"], + text: "你通过技能回复体力时,回复值+1", + }, + { + level: 2, + count: 2, + skills: ["shuYing_Tianshu_BondTaoyuan2"], + text: "你通过技能回复体力时,回复值+1," + + "并可以令一名其他角色回复或失去1点体力", + }, + { + level: 3, + count: 3, + skills: ["shuYing_Tianshu_BondTaoyuan3"], + text: "你通过技能回复体力时,回复值+1," + + "并可以令一名其他角色回复或失去1点体力;" + + "按原体力上限结算回复后,将溢出回复转为受回复角色的体力上限", + }, + ], + }, + { + id: "directDamage", + name: "直伤", + condition: { type: "directDamage" }, + levels: [ + { + level: 1, + count: 1, + skills: ["shuYing_Tianshu_BondDirectDamage1"], + text: "你通过技能直接造成的伤害+1", + }, + { + level: 2, + count: 2, + skills: ["shuYing_Tianshu_BondDirectDamage2"], + text: "你通过技能直接造成的伤害+1,并摸等同于最终伤害值的牌", + }, + { + level: 3, + count: 3, + skills: ["shuYing_Tianshu_BondDirectDamage3"], + text: "你通过技能直接造成的伤害+1,并摸等同于最终伤害值的牌;" + + "你可以令此次伤害改为无来源,仍结算增伤和摸牌", + }, + ], + }, { id: "shafa", name: "杀伐", diff --git a/tianshu/directDamage.js b/tianshu/directDamage.js new file mode 100644 index 0000000..90dd28e --- /dev/null +++ b/tianshu/directDamage.js @@ -0,0 +1,28 @@ +export const directDamageStorage = "shuYing_tianshu_directDamageSkills"; + +export function getDirectDamageOrigin(event, lib, get) { + if (!event || event.num <= 0 || event.unreal) return null; + if (typeof event.notLink == "function" && !event.notLink()) return null; + let current = event.getParent?.(1, true); + const visited = new Set(); + while (current && !visited.has(current)) { + visited.add(current); + if (["useCard", "respond", "damage", "_lianhuan", "_lianhuan2"] + .includes(current.name) || current.name?.endsWith("_cost")) { + return null; + } + const skill = lib.skill[current.name] ? current.name : current.skill; + if (skill && lib.skill[skill]) { + return { skill: get.sourceSkillFor(skill), player: current.player }; + } + current = current.getParent?.(1, true); + } + return null; +} + +export function getDirectDamageSkillCount(player, skills, lib) { + const recorded = player?.storage?.[directDamageStorage]; + if (!Array.isArray(recorded)) return 0; + return new Set(skills.filter(skill => recorded.includes(skill)) + .map(skill => lib.translate[skill] || skill)).size; +} diff --git a/tianshu/extension.js b/tianshu/extension.js index 4092c45..1e0ab3d 100644 --- a/tianshu/extension.js +++ b/tianshu/extension.js @@ -1,5 +1,9 @@ import tianshuConfig from "./config.js"; import bondSkillPack from "./bondSkills.js"; +import { directDamageStorage, getDirectDamageOrigin, + getDirectDamageSkillCount } from "./directDamage.js"; +import { skillRecoverStorage, getSkillRecoverOrigin, getSkillRecoverCount, + wrapSkillRecoverEvent } from "./skillRecover.js"; import { lib, game, ui, get, ai, _status } from "../../../noname.js"; export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { @@ -22,6 +26,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { originalGameCheck: null, checkHookInstalled: false, bondHooksInstalled: false, + recoverHookInstalled: false, corpseChooseCleanup: null, corpseChooseEvent: null, initialBoss: null, @@ -274,10 +279,15 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { } }; // 新 Boss 入场并重排座位后,若当前行动者属于玩家方(含虚拟偶像)且实际下家为 Boss 或虚拟偶像,则启用旧轮次。 - const shouldUseLegacyBossTurn = current => { + const shouldUseLegacyBossTurn = (current, stageDeadPlayers) => { if (!lib.config[tianshuConfig.settings.legacyRoundConfigKey]) return false; if (!current || !current.isIn?.() || !isCapturedPlayerSide(current)) return false; - const next = current.next || current.getNext?.(); + let next = current.next || current.getNext?.(); + const visited = new Set([current]); + while (next && stageDeadPlayers?.has(next) && !visited.has(next)) { + visited.add(next); + next = next.next || next.getNext?.(); + } return Boolean(next && (isCurrentStageBoss(next) || isVirtualIdol(next))); }; // 从玩家方1号位沿上家方向查找当前关 Boss,作为旧轮次的行动者。 @@ -293,9 +303,9 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { } return null; }; - const getNextStageTurnPlayer = current => { + const getNextStageTurnPlayer = (current, stageDeadPlayers) => { const next = current?.next || null; - if (!shouldUseLegacyBossTurn(current)) return next; + if (!shouldUseLegacyBossTurn(current, stageDeadPlayers)) return next; return getLegacyBossTurnPlayer() || next; }; // 开局捕获玩家方成员,避免后续 game.boss 变化导致阵营误判。 @@ -738,6 +748,14 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { if (!condition.type && condition.not) return !matchesBondCondition(player, condition.not); const owned = getBondConditionSkills(player); + if (condition.type == "directDamage") { + return getDirectDamageSkillCount(player, owned, lib) + >= Math.max(1, Number(condition.count) || 1); + } + if (condition.type == "skillRecover") { + return getSkillRecoverCount(player, owned, lib) + >= Math.max(1, Number(condition.count) || 1); + } if (condition.type == "keyword") { if (!condition.keyword) return false; const marker = `【${condition.keyword}】`; @@ -812,6 +830,12 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { if (!condition.type && condition.not) return false; const skillName = lib.translate[skill] || ""; + if (condition.type == "directDamage") { + return getDirectDamageSkillCount(player, [skill], lib) > 0; + } + if (condition.type == "skillRecover") { + return getSkillRecoverCount(player, [skill], lib) > 0; + } if (condition.type == "keyword") { if (!condition.keyword) return false; const info = typeof get.skillInfoTranslation == "function" ? get.skillInfoTranslation(skill, player) : lib.translate[`${skill}_info`]; @@ -1088,6 +1112,9 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { game.over(!state.defending); return; } + const stageDeadPlayers = new Set(getPlayerSidePlayers({ + aliveOnly: false, includeVirtual: true, + }).filter(player => player.isDead())); await reviveVirtualIdolIfNeeded(); if (lib.config[tianshuConfig.settings.revivePlayersConfigKey]) { const deadPlayers = getPlayerSidePlayers({ aliveOnly: false, includeVirtual: false }).filter(player => player.isDead()); @@ -1116,7 +1143,8 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { state.stageIndex++; await spawnStage(state.stageIndex); // 必须在新 Boss 召唤并完成座位重排后,再按新座位链决定控制权归属。 - const nextTurnPlayer = getNextStageTurnPlayer(currentTurnPlayer); + const nextTurnPlayer = getNextStageTurnPlayer( + currentTurnPlayer, stageDeadPlayers); if (nextTurnPlayer) finishCurrentPhaseTo(nextTurnPlayer); }; // 处理 Boss 死亡事件:击杀奖励、隐藏尸体、判断是否清关。 @@ -1135,6 +1163,79 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { if (!stageCleared) return; await clearStage(); }; + const settleSkillRecover = async (event, record, overflow) => { + const { player, skill, level } = record; + const rewardSkill = getTianshuRewardSkills(player) + .find(current => get.sourceSkillFor(current) == skill); + if (rewardSkill) { + const recorded = player.storage[skillRecoverStorage] || []; + if (!recorded.includes(rewardSkill)) { + recorded.push(rewardSkill); + player.storage[skillRecoverStorage] = recorded; + game.broadcast((current, key, value) => { + current.storage[key] = value; + }, player, skillRecoverStorage, recorded); + player.syncStorage?.(skillRecoverStorage); + refreshTianshuBonds(player); + game.log(player, "登记桃园技能", + `【${get.translation(rewardSkill)}】`); + } + } + if (overflow > 0) game.log(player, "将", overflow, + "点溢出回复转为", event.player, "的体力上限"); + if (level < 2 || !player.isIn()) return; + const result = await player.chooseTarget( + "桃园:可以令一名其他角色回复或失去1点体力", + (card, current, target) => target != current && target.isIn()) + .set("ai", target => Math.abs(get.attitude(player, target))) + .forResult(); + if (!result.bool || !result.targets?.[0]?.isIn()) return; + const target = result.targets[0]; + const choice = await player.chooseControl("回复体力", "失去体力") + .set("prompt", `桃园:令${get.translation(target)}执行哪项?`) + .set("ai", () => get.attitude(player, target) > 0 ? 0 : 1) + .forResult(); + if (!target.isIn()) return; + player.line(target); + if (choice.control == "回复体力") { + const next = target.recover(1, player); + next.shuYing_tianshu_taoyuanSecondary = true; + await next; + } else if (choice.control == "失去体力") { + await target.loseHp(1); + } + }; + const installSkillRecoverHook = () => { + if (state.recoverHookInstalled) return; + const prototype = lib.element.Player?.prototype || lib.element.player; + const original = prototype.recover.shuYing_taoyuanBase + || prototype.recover; + const recover = function () { + const next = original.apply(this, arguments); + if (!state.running || _status.over || !this.isIn()) return next; + const origin = getSkillRecoverOrigin(next, lib, get); + const player = origin?.player; + if (!player?.isIn() || player != next.source + || !isCapturedPlayerSide(player) + || (isVirtualIdol(player) + && !canVirtualIdolReceiveRewards())) return next; + const level = player.storage.shuYing_tianshu_bondLevels + ?.taoyuan || 0; + const originalContent = next.content.original; + const nativeContent = typeof originalContent == "string" + ? lib.element.content[originalContent] + || lib.element.contents?.[originalContent] + : originalContent || next.content; + wrapSkillRecoverEvent(next, { + player, skill: origin.skill, level, + }, settleSkillRecover, nativeContent, + () => player.storage.shuYing_tianshu_bondLevels?.taoyuan || 0); + return next; + }; + recover.shuYing_taoyuanBase = original; + prototype.recover = recover; + state.recoverHookInstalled = true; + }; // gameStart 时启动天书乱斗流程,完成难度选择并等待 gameDrawAfter 召唤第一关。 const startRun = async (player = game.me) => { if (state.running) return; @@ -1159,6 +1260,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { installHiddenBossCorpseCheckHook(); spawnVirtualIdol(); capturePlayerSide(); + installSkillRecoverHook(); installTianshuBondHooks(); installBondOverviewButton(); state.pendingSpawn = true; @@ -1259,6 +1361,9 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { game.addGlobalSkill("shuYing_Tianshu_DefendingControl"); game.addGlobalSkill("shuYing_Tianshu_StageRoundHistory"); game.addGlobalSkill("shuYing_Tianshu_BondManager"); + game.addGlobalSkill("shuYing_Tianshu_DirectDamageCapture"); + game.addGlobalSkill("shuYing_Tianshu_DirectDamageSettle"); + game.addGlobalSkill("shuYing_Tianshu_SkillRecoverCapture"); }, }, shuYing_Tianshu_DefendingControl: { @@ -1297,6 +1402,113 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { game.getGlobalHistory().isRound = true; }, }, + shuYing_Tianshu_BondDirectDamage1: { charlotte: true, popup: false }, + shuYing_Tianshu_BondDirectDamage2: { charlotte: true, popup: false }, + shuYing_Tianshu_BondDirectDamage3: { charlotte: true, popup: false }, + shuYing_Tianshu_DirectDamageCapture: { + mode: ["boss"], + trigger: { global: "damageBegin1" }, + forced: true, + silent: true, + charlotte: true, + firstDo: true, + priority: 1000, + filter(event) { + const player = event.source; + if (!state.running || event.shuYing_tianshu_directDamage + || !player?.isIn() + || !isCapturedPlayerSide(player) + || (isVirtualIdol(player) + && !canVirtualIdolReceiveRewards())) return false; + const origin = getDirectDamageOrigin(event, lib, get); + return Boolean(origin && origin.player == player); + }, + async content(event, trigger) { + if (trigger.shuYing_tianshu_directDamage) return; + const origin = getDirectDamageOrigin(trigger, lib, get); + if (!origin) return; + const player = trigger.source; + const level = player.storage.shuYing_tianshu_bondLevels + ?.directDamage || 0; + trigger.shuYing_tianshu_directDamage = { + player, skill: origin.skill, level, + }; + if (!level) return; + trigger.num++; + if (level < 3) return; + const result = await player.chooseBool( + "直伤:是否令此次伤害改为无来源?", + "仍会增加1点伤害,并按最终伤害值摸牌。") + .set("ai", () => false).forResult(); + if (result.bool) { + delete trigger.source; + game.log(player, "令此次技能伤害改为无来源"); + } + }, + }, + shuYing_Tianshu_DirectDamageSettle: { + mode: ["boss"], + trigger: { global: "damage" }, + forced: true, + silent: true, + charlotte: true, + firstDo: true, + priority: 1000, + filter(event) { + return Boolean(state.running && event.num > 0 + && !event.unreal && event.shuYing_tianshu_directDamage + && !event.shuYing_tianshu_directDamage.settled); + }, + async content(event, trigger) { + const record = trigger.shuYing_tianshu_directDamage; + record.settled = true; + const { player, skill, level } = record; + const num = trigger.num; + const rewardSkill = getTianshuRewardSkills(player) + .find(current => get.sourceSkillFor(current) == skill); + if (rewardSkill) { + const recorded = player.storage[directDamageStorage] || []; + if (!recorded.includes(rewardSkill)) { + recorded.push(rewardSkill); + player.storage[directDamageStorage] = recorded; + game.broadcast((current, key, value) => { + current.storage[key] = value; + }, player, directDamageStorage, recorded); + player.syncStorage?.(directDamageStorage); + refreshTianshuBonds(player); + game.log(player, "登记直伤技能", + `【${get.translation(rewardSkill)}】`); + } + } + if (level >= 2 && player.isIn()) await player.draw(num); + }, + }, + shuYing_Tianshu_BondTaoyuan1: { charlotte: true, popup: false }, + shuYing_Tianshu_BondTaoyuan2: { charlotte: true, popup: false }, + shuYing_Tianshu_BondTaoyuan3: { charlotte: true, popup: false }, + shuYing_Tianshu_SkillRecoverCapture: { + mode: ["boss"], + trigger: { global: "recoverBegin" }, + forced: true, + silent: true, + charlotte: true, + firstDo: true, + priority: 1000, + filter(event) { + const record = event.shuYing_tianshu_skillRecover; + return Boolean(state.running && event.num > 0 && record + && !record.boosted + && !event.shuYing_tianshu_taoyuanSecondary); + }, + async content(event, trigger) { + const record = trigger.shuYing_tianshu_skillRecover; + if (record.boosted || trigger.shuYing_tianshu_taoyuanSecondary) { + return; + } + record.boosted = true; + if (record.level >= 1) trigger.num++; + }, + }, shuYing_Tianshu_BondManager: { mode: ["boss"], trigger: { global: "changeSkillsAfter" }, @@ -1397,6 +1609,24 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) { shuYing_Skill_Tianshu_intro5: { nobracket: true }, }; tianshu.translate = { + shuYing_Tianshu_BondTaoyuan1: "桃园·壹", + shuYing_Tianshu_BondTaoyuan1_info: "你通过技能回复体力时,回复值+1。", + shuYing_Tianshu_BondTaoyuan2: "桃园·贰", + shuYing_Tianshu_BondTaoyuan2_info: + "你通过技能回复体力时,回复值+1,并可以令一名其他角色回复或失去1点体力。", + shuYing_Tianshu_BondTaoyuan3: "桃园·叁", + shuYing_Tianshu_BondTaoyuan3_info: + "你通过技能回复体力时,回复值+1,并可以令一名其他角色回复或失去1点体力;" + + "按原体力上限结算回复后,将溢出回复转为受回复角色的体力上限。", + shuYing_Tianshu_BondDirectDamage1: "直伤·壹", + shuYing_Tianshu_BondDirectDamage1_info: "你通过技能直接造成的伤害+1。", + shuYing_Tianshu_BondDirectDamage2: "直伤·贰", + shuYing_Tianshu_BondDirectDamage2_info: + "你通过技能直接造成的伤害+1,并摸等同于最终伤害值的牌。", + shuYing_Tianshu_BondDirectDamage3: "直伤·叁", + shuYing_Tianshu_BondDirectDamage3_info: + "你通过技能直接造成的伤害+1,并摸等同于最终伤害值的牌;" + + "你可以令此次伤害改为无来源,仍结算增伤和摸牌。", tianshu_boss_shuYing: "天书乱斗", shuYing_Skill_Tianshu_Go: "天书乱斗", shuYing_Skill_Tianshu_intro1: " 第一关", diff --git a/tianshu/skillRecover.js b/tianshu/skillRecover.js new file mode 100644 index 0000000..2467667 --- /dev/null +++ b/tianshu/skillRecover.js @@ -0,0 +1,61 @@ +export const skillRecoverStorage = "shuYing_tianshu_skillRecoverSkills"; + +export function getSkillRecoverOrigin(event, lib, get) { + if (!event || event.num <= 0 || event.shuYing_tianshu_taoyuanSecondary) { + return null; + } + let current = event.getParent?.(1, true); + const visited = new Set(); + while (current && !visited.has(current)) { + visited.add(current); + if (["useCard", "respond", "recover", "damage"] + .includes(current.name) || current.name?.endsWith("_cost")) { + return null; + } + const skill = lib.skill[current.name] ? current.name : current.skill; + if (skill && lib.skill[skill]) { + return { skill: get.sourceSkillFor(skill), player: current.player }; + } + current = current.getParent?.(1, true); + } + return null; +} + +export function getSkillRecoverCount(player, skills, lib) { + const recorded = player?.storage?.[skillRecoverStorage]; + if (!Array.isArray(recorded)) return 0; + return new Set(skills.filter(skill => recorded.includes(skill)) + .map(skill => lib.translate[skill] || skill)).size; +} + +export function wrapSkillRecoverEvent(event, record, onSettle, nativeContent, + getLevel) { + const originalFilter = event.filterStop; + const originalContent = nativeContent + || (typeof event.content.original == "function" + ? event.content.original : event.content); + event.shuYing_tianshu_skillRecover = record; + event.filterStop = function () { + if (!record.boosted && getLevel) record.level = getLevel(); + if (record.level >= 3 && this.num > 0 && this.player.isHealthy() + && !this.shuYing_tianshu_taoyuanSecondary) return false; + return originalFilter?.call(this) || false; + }; + event.setContent(async current => { + if (record.settled) return; + if (current.shuYing_tianshu_taoyuanSecondary) { + await originalContent(current, current._trigger, current.player); + return; + } + const missing = Math.max(0, current.player.maxHp - current.player.hp); + const overflow = record.level >= 3 + ? Math.max(0, current.num - missing) : 0; + if (record.level >= 3) current.num = Math.min(current.num, missing); + await originalContent(current, current._trigger, current.player); + if (overflow > 0) await current.player.gainMaxHp(overflow); + record.settled = true; + if (current.num > 0 || overflow > 0) { + await onSettle(current, record, overflow); + } + }); +} diff --git a/tools/manifest_config.json b/tools/manifest_config.json index 801a1a1..c97138e 100644 --- a/tools/manifest_config.json +++ b/tools/manifest_config.json @@ -9,6 +9,8 @@ "diagnostics/storage.js", "tianshu/extension.js", "tianshu/config.js", + "tianshu/directDamage.js", + "tianshu/skillRecover.js", "pve/boss/index.js", "pve/boss/card.js", "pve/boss/character.js", diff --git a/update.txt b/update.txt index 07ff43a..9811cdb 100644 --- a/update.txt +++ b/update.txt @@ -1,3 +1,11 @@ +"-----< 2.0.2.4 改动 >-----", +"", + +"-----< 2.0.2.4 修复 >-----", +"修复天书乱斗部分羁绊失效的问题", +"修复天书乱斗部分BOSS技能异常的问题", +"" + "-----< 2.0.2.3 改动 >-----", "新增正式版、测试版更新通道切换。", "支持选择 Gitea 或 GitHub 下载源。",