fixed # 修复部分天书乱斗的问题

This commit is contained in:
2026-10-12 15:10:31 +08:00
parent 0205bbebc6
commit 279f2d9b0a
19 changed files with 888 additions and 103 deletions
+4
View File
@@ -40,3 +40,7 @@ __pycache__/
.DS_Store
Thumbs.db
Desktop.ini
/.comet/
/.comet/*
/.comet/config.yaml
+1 -14
View File
@@ -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();
},
},
@@ -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。该指令构成本轮完整范围确认,无需再次询问同一选择。
@@ -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均保留。狼顾、妄尊、夺玺、引雷及所有本轮改动的静态/动态文本和选择提示与最终规则一致。
File diff suppressed because one or more lines are too long
@@ -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/`。
+31
View File
@@ -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") {
+15
View File
@@ -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 种过关技能";
}
@@ -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;
+70 -54
View File
@@ -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;
}
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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");
+9 -19
View File
@@ -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;
+59 -1
View File
@@ -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: "杀伐",
+28
View File
@@ -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;
}
+235 -5
View File
@@ -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: "&nbsp;第一关",
+61
View File
@@ -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);
}
});
}
+2
View File
@@ -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",
+8
View File
@@ -1,3 +1,11 @@
"-----< 2.0.2.4 改动 >-----",
"",
"-----< 2.0.2.4 修复 >-----",
"修复天书乱斗部分羁绊失效的问题",
"修复天书乱斗部分BOSS技能异常的问题",
""
"-----< 2.0.2.3 改动 >-----",
"新增正式版、测试版更新通道切换。",
"支持选择 Gitea 或 GitHub 下载源。",