mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-11-15 17:29:59 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c923a818d | ||
|
|
fe3ebcc0e8 | ||
|
|
2a91f43675 | ||
|
|
dce49106a0 | ||
|
|
a8942c158e | ||
|
|
b9ebbfac7e |
@@ -15,11 +15,7 @@ tools/*.mjs
|
||||
/Slay the Spire 2
|
||||
|
||||
*.mjs
|
||||
!tools/test_skill_teardown.mjs
|
||||
!tools/test_battle_lifecycle.mjs
|
||||
*.cjs
|
||||
!mengsan/tools/check-sts2-relics.mjs
|
||||
!mengsan/tools/write-sts2-relics-report.mjs
|
||||
|
||||
/.update_tmp/
|
||||
/.planning/
|
||||
|
||||
Vendored
+497
-392
File diff suppressed because it is too large
Load Diff
Vendored
+708
-698
File diff suppressed because it is too large
Load Diff
Vendored
+2
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "2.0.2.3-rc.3",
|
||||
"version": "2.0.2.3-rc.5",
|
||||
"module_id": "mengsan",
|
||||
"snapshot": "legacy",
|
||||
"files": [
|
||||
@@ -804,6 +804,7 @@
|
||||
"mengsan/ui/map/renderer.js",
|
||||
"mengsan/ui/navigation.js",
|
||||
"mengsan/ui/relic-icon.js",
|
||||
"mengsan/ui/relic-strip.js",
|
||||
"mengsan/ui/relics.js",
|
||||
"mengsan/ui/reward-package.js",
|
||||
"mengsan/ui/reward-ui.js",
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
schema: comet.native.v4
|
||||
name: shuying-skill-tianshu-review
|
||||
language: zh-CN
|
||||
phase: archive
|
||||
status: done
|
||||
state_version: 28
|
||||
brief: brief.md
|
||||
document_constraints_version: 2
|
||||
shape_confirmation_hash: 73614dd2595713e137f716f16fb11eed1c7d59096b3fd243d53ff1d97cedd7d3
|
||||
spec_changes:
|
||||
- capability: shuying-skill-correctness
|
||||
operation: create
|
||||
source: specs/shuying-skill-correctness/spec.md
|
||||
workspace:
|
||||
isolation: current
|
||||
change_branch: main
|
||||
target_branch: main
|
||||
finish: null
|
||||
loop:
|
||||
stage: done
|
||||
goal_cycle: 4
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 0
|
||||
no_progress_count: 0
|
||||
execution_failure_count: 0
|
||||
previous_unresolved_ids: []
|
||||
next_action: null
|
||||
acceptance:
|
||||
- id: A1
|
||||
source: brief.md
|
||||
text: 羁绊的 filter、mod、async content 在没有开发者全局对象时,能通过模块导入获取引擎依赖。
|
||||
result: passed
|
||||
reason:
|
||||
text: tianshu/bondSkills.js:2显式模块导入,当前正式ESM用例验证无开发者lib/get/game全局时filter、mod和content执行。
|
||||
truncated: false
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: 江东基业二三级对应子技能通过正确 group 挂载并执行,低等级不重复叠加。
|
||||
result: passed
|
||||
reason:
|
||||
text: bondSkills.js:114/186二三级group与subSkill对应;extension.js:946-989按来源精确换级并核对当前所有权;正式二三级分组用例通过。
|
||||
truncated: false
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: 驭布对一张多目标杀只判定一次,成功时对各存活目标只造成一次伤害。
|
||||
result: passed
|
||||
reason:
|
||||
text: 青青子衿skill.js:qubu保留isFirstTarget,并在成功后遍历当前存活目标各一次;正式多目标用例通过。
|
||||
truncated: false
|
||||
- id: A4
|
||||
source: brief.md
|
||||
text: 驭布困难难度黑桃成功、梅花失败;阴间难度黑色成功、红色失败,动态描述与难度条件一致。
|
||||
result: passed
|
||||
reason:
|
||||
text: qubu按nightmare黑色、其余黑桃判定;动态描述同分支;当前正式hard与nightmare花色及动态切换用例通过。
|
||||
truncated: false
|
||||
- id: A5
|
||||
source: brief.md
|
||||
text: 狼顾失败后仅限制当前回合;黑桃成功时恢复次数,即使没有可弃手牌也不会错误占用次数。
|
||||
result: passed
|
||||
reason:
|
||||
text: langgu以global phaseAfter临时限制当前回合,成功先移除used再判断来源与合法手牌,空牌成功也恢复;正式用例通过。
|
||||
truncated: false
|
||||
- id: A6
|
||||
source: brief.md
|
||||
text: 奋威开局及中途获得时各初始化2蓄力,同次持有不因入场/开局事件重复添加,不重置共享蓄力。
|
||||
result: passed
|
||||
reason:
|
||||
text: character/skill.js:654奋威分组init添加2蓄力而不重置共享值;当前定向通过,非目标哈希一致且复用前轮fenwei-engine-lifecycle正式PASS记录。
|
||||
truncated: false
|
||||
- id: A7
|
||||
source: brief.md
|
||||
text: 舌战与狼顾仅从允许被发动者弃置的牌中随机选择。
|
||||
result: passed
|
||||
reason:
|
||||
text: 舌战两级与狼顾从getDiscardableCards(player,zone)抽取,并标记discarder;当前正式保护牌用例通过。
|
||||
truncated: false
|
||||
- id: A8
|
||||
source: brief.md
|
||||
text: 白仪与九天仅从允许被发动者获得的牌中随机选择。
|
||||
result: passed
|
||||
reason:
|
||||
text: 白仪/九天从getGainableCards(player,zone)随机取牌,难度数量与后续逻辑保留;当前正式保护牌用例通过。
|
||||
truncated: false
|
||||
- id: A9
|
||||
source: brief.md
|
||||
text: 连环伤害允许再次增伤,既有牌伤增益不添加 notLink 排除。
|
||||
result: passed
|
||||
reason:
|
||||
text: 既有五虎/天下无双/武安及戌狗增伤未增加notLink排除;当前正式五虎连环输入2增至3,确认允许再次增伤。
|
||||
truncated: false
|
||||
- id: A10
|
||||
source: brief.md
|
||||
text: 新增定向回归验证并运行相关现有天书测试,说明真实游戏/联机实测边界,保留无关改动。
|
||||
result: passed
|
||||
reason:
|
||||
text: 复用当前候选Runtime
|
||||
5文件回归101次执行全通过、0失败,含90个独立测试(回复fixture重复登记11项);正式空白检查退出0;11非目标SHA256一致。修复前40用例27失败为Builder开发期记录,未冒称Runtime红灯;未实机/联机。
|
||||
truncated: false
|
||||
- id: A11
|
||||
source: brief.md
|
||||
text: 狼顾阴间梅花成功、红色失败,普通/困难仅黑桃成功;动态描述同步。
|
||||
result: passed
|
||||
reason:
|
||||
text: langgu普通/困难仅spade,阴间color black;成功次数与合法他弃保留,动态描述注册链完整;9花色与3描述及友方用例通过。
|
||||
truncated: false
|
||||
- id: A12
|
||||
source: brief.md
|
||||
text: 妄尊阴间累计1点伤害即反击,困难1点不反击,未受伤时弃牌数仍为困难1/阴间2。
|
||||
result: passed
|
||||
reason:
|
||||
text: wangzun对当前回合源伤害累计,nightmare阈值>0,其余>1;0伤困难弃1/阴间弃2;正式0/1/2点分支和描述通过。
|
||||
truncated: false
|
||||
- id: A13
|
||||
source: brief.md
|
||||
text: 夺玺阴间替换摸牌后仅自己摸2,困难双方各摸1;动态描述、提示同步。
|
||||
result: passed
|
||||
reason:
|
||||
text: duoxi替换摸牌后失去1体力,nightmare仅存活自己摸2,hard双方各1;提示、动态文本和AI采用自己摸牌分支;正式分支及离场用例通过。
|
||||
truncated: false
|
||||
- id: A14
|
||||
source: brief.md
|
||||
text: 引雷困难自己回合不触发、阴间自己回合触发;描述同步回合条件。
|
||||
result: passed
|
||||
reason:
|
||||
text: yinlei
|
||||
normal拒绝,hard本人回合拒绝、nightmare不限制回合;过滤未横置存活角色并随机横置;动态文本同步,正式4种回合分支通过。
|
||||
truncated: false
|
||||
- id: A15
|
||||
source: brief.md
|
||||
text: 张角三难度体力和上限8/16/24,起始手牌6/8/10和追加技能保留。
|
||||
result: passed
|
||||
reason:
|
||||
text: tianshu/config.js:121张角独立加成4/12/20叠加基础4得到8/16/24,hp/maxHp一致,起手6/8/10与hard起追加引雷保留;正式三难度及曹操不变断言通过。
|
||||
truncated: false
|
||||
- id: A16
|
||||
source: brief.md
|
||||
text: 疑神随机获取各目标合法装备,正常难度分支只选一名敌人,阴间所有合格敌人各一张;禁止选择受保护装备。
|
||||
result: passed
|
||||
reason:
|
||||
text: yishen取消回复后从每目标getGainableCards(player,e)随机取牌;非阴间随机一敌人,阴间所有合格敌人各一;未开放装备自选;正式合法/保护牌用例通过。
|
||||
truncated: false
|
||||
- id: A17
|
||||
source: brief.md
|
||||
text: 反馈受到多点伤害有逐点发动机会,可对友方来源发动,源牌耗尽后停止;自伤只获取自身装备。
|
||||
result: passed
|
||||
reason:
|
||||
text: fankui getIndex返回damage
|
||||
num,引擎gameEvent.js:498-507数字展开N项,content.js:3435每轮重筛来源存活及可得牌;来源不限阵营,自伤只e;正式多点耗尽及自伤用例通过。
|
||||
truncated: false
|
||||
- id: A18
|
||||
source: brief.md
|
||||
text: 狼顾接受其他友方来源;暴虐满血也失去1体力并允许任意角色;雷击允许其他友方,谏征允许其他角色使用的杀,不增加阵营限制。
|
||||
result: passed
|
||||
reason:
|
||||
text: langgu其他来源不限阵营;baonue满血跳过摸牌/选人仍loseHp,受伤可选任意存活角色;leiji其他角色、zhuzheng其他杀使用者/原目标无阵营硬限制;当前正式范围用例通过。
|
||||
truncated: false
|
||||
- id: A19
|
||||
source: brief.md
|
||||
text: 远虑阴间保留unreal并明确描述;无双/神戟/战甲增强不回退,全部改动描述与提示一致。
|
||||
result: passed
|
||||
reason:
|
||||
text: 远虑nightmare仍unreal且文本写视为伤害;无双增伤与1/2次、神戟摸2/3/4和杀1/2/2、战甲阴间摸3保留;静态基准/动态难度/相关提示一致,正式增强用例通过。
|
||||
truncated: false
|
||||
builder_handoff:
|
||||
candidate_id: c9724c63-8c45-44ab-944c-4681834c1755
|
||||
identity_provider: skill-coordinated
|
||||
builder_execution_ref: skill-coordinated:builder:9b9bc03f-d1c9-4af7-93f4-3aceeb630392
|
||||
iteration: 1
|
||||
summary:
|
||||
text: 修复用户确认的青青子衿难度/体力/范围差异,同步文本;保留远虑虚拟伤害与无双神戟战甲增强。新增41项定向验证通过,修复前40项中27项复现失败;4个生产文件语法检查通过,11个非目标文件哈希未变。原A1-A10证据继续适用。未进行真实游戏或联机验证,未提交发布。
|
||||
truncated: false
|
||||
addressed_acceptance_ids:
|
||||
- A1
|
||||
- A2
|
||||
- A3
|
||||
- A4
|
||||
- A5
|
||||
- A6
|
||||
- A7
|
||||
- A8
|
||||
- A9
|
||||
- A10
|
||||
- A11
|
||||
- A12
|
||||
- A13
|
||||
- A14
|
||||
- A15
|
||||
- A16
|
||||
- A17
|
||||
- A18
|
||||
- A19
|
||||
acceptance_review:
|
||||
- id: A1
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tianshu/bondSkills.js:2
|
||||
- tools/test_skill_review_fixes.mjs:90
|
||||
note: 显式导入引擎对象,无开发者全局对象的ESM调用filter/mod/content通过。
|
||||
- id: A2
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tianshu/bondSkills.js:114,186
|
||||
- tools/test_skill_review_fixes.mjs:104
|
||||
note: 修正5个group名,二/三级摸牌与他弃伤害、三级得牌反伤可通过组触发。
|
||||
- id: A3
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- pve/boss/sets/qingqingzijin/skill.js:654
|
||||
- tools/test_skill_review_fixes.mjs:159
|
||||
note: 保留isFirstTarget,不同难度均每张两目标杀只有一次判定、各目标一次额外伤害。
|
||||
- id: A4
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- pve/boss/sets/qingqingzijin/skill.js:662-672
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js:69-75
|
||||
- tools/test_skill_review_fixes.mjs:136-195
|
||||
note: hard要求spade,nightmare要求black;六组难度花色场景及动态描述随hard/nightmare切换均通过。
|
||||
- id: A5
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- pve/boss/sets/qingqingzijin/skill.js:241
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- tools/test_skill_review_fixes.mjs
|
||||
note: 狼顾当前回合限制、成功恢复次数和合法他弃保留,并由新难度测试覆盖。
|
||||
- id: A6
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- character/skill.js:654
|
||||
- tools/test_skill_review_fixes.mjs:216
|
||||
- D:/Work/noname/resources/app/noname/library/element/player.js:10748
|
||||
note: 使用既有init子技能的引擎init回调添加2蓄力;同次持有由initedSkills避免重复,不重置共享charge。
|
||||
- id: A7
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tianshu/bondSkills.js:339,388
|
||||
- pve/boss/sets/qingqingzijin/skill.js:267
|
||||
- tools/test_skill_review_fixes.mjs:211,239
|
||||
note: 舌战和狼顾使用getDiscardableCards发动者上下文,保留随机操作并注明discarder。
|
||||
- id: A8
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- pve/boss/sets/tianshu/skill.js:618,715
|
||||
- tools/test_skill_review_fixes.mjs:251
|
||||
note: 白仪和九天使用getGainableCards合法候选,保留难度取牌数与其余后续效果。
|
||||
- id: A9
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_skill_review_fixes.mjs:263
|
||||
- 既有五虎/天下无双/武安/戌狗增伤源码未变
|
||||
note: 用户明确允许连环再次增伤;本次未加notLink限制,专门用例输入2得到3。
|
||||
- id: A10
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- 本轮定向19用例:修改前17pass2fail,修改后19pass
|
||||
- 12个非目标文件与本轮快照SHA一致
|
||||
- 实现与动态描述语法及git diff --check通过
|
||||
note: 真实ESM测试覆盖新增难度分支和动态描述;已有其他修复保留,最终组合计划运行60执行。没有真实游戏/联机实测。
|
||||
- id: A11
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 狼顾阴间判定黑色、普通困难黑桃;新增动态描述并保持成功恢复及合法他弃。
|
||||
- id: A12
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 妄尊阴间正伤害即可反击,普通困难超过1点;弃牌数不变,描述同步。
|
||||
- id: A13
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 夺玺阴间仅自己摸2,困难双方各1;提示及AI和离场检查同步。
|
||||
- id: A14
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 引雷阴间回合内外均触发,困难仅回合外;新增动态描述。
|
||||
- id: A15
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tianshu/config.js:121
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
note: 张角独立模板额外体力4/12/20,加原始4得到8/16/24,起手与技能保留。
|
||||
- id: A16
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 疑神从getGainableCards合法装备随机取牌,阴间每名合格敌人各一张,取消自选。
|
||||
- id: A17
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 反馈getIndex返回伤害点数,支持友方来源,逐次检查合法牌;自伤仅取装备。
|
||||
- id: A18
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 取消狼顾来源、暴虐目标、雷击目标、谏征使用者及目标的额外阵营限制;暴虐满血仍失去1体力。
|
||||
- id: A19
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- tools/test_qingqingzijin_rules.mjs
|
||||
- pve/boss/sets/qingqingzijin/skill.js
|
||||
- pve/boss/sets/qingqingzijin/dynamicTranslate.js
|
||||
note: 远虑unreal保留且文本明确视为;无双神戟战甲增强与连环规则保留;文本提示同步。
|
||||
checks:
|
||||
- name:
|
||||
text: 青青子衿定向回归
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: 41/41通过,修复前40用例13通过27失败。
|
||||
truncated: false
|
||||
- name:
|
||||
text: 生产JS语法
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: skill/dynamicTranslate/translate/config语法通过。
|
||||
truncated: false
|
||||
- name:
|
||||
text: 非目标文件保护
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: 11个基线哈希一致,其他已有改动保留。
|
||||
truncated: false
|
||||
checks_truncated: false
|
||||
known_limits:
|
||||
- text: 未进行真实游戏、浏览器或联机实测。
|
||||
truncated: false
|
||||
- text: 测试mjs沿用仓库现有忽略规则,当前文件保存在工作区,未暂存或提交。
|
||||
truncated: false
|
||||
known_limits_truncated: false
|
||||
review: null
|
||||
submitted_at: 2026-10-12T06:36:05.406Z
|
||||
blockers: []
|
||||
verification:
|
||||
candidate_id: c9724c63-8c45-44ab-944c-4681834c1755
|
||||
identity_provider: skill-coordinated
|
||||
verifier_execution_ref: skill-coordinated:verifier:778df9a8-a7ae-4d96-ba17-7fd7da47daf7
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
assurance: skill-coordinated
|
||||
verdict: pass
|
||||
checks:
|
||||
- id: boss-rules-regressions
|
||||
name:
|
||||
text: 青青子衿及全部相关天书回归
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: --test
|
||||
truncated: false
|
||||
- text: tools/test_qingqingzijin_rules.mjs
|
||||
truncated: false
|
||||
- text: tools/test_skill_review_fixes.mjs
|
||||
truncated: false
|
||||
- text: tools/test_tianshu_direct_damage.mjs
|
||||
truncated: false
|
||||
- text: tools/test_tianshu_skill_recover.mjs
|
||||
truncated: false
|
||||
- text: tools/test_tianshu_stage_turn.mjs
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 415
|
||||
- id: boss-rules-whitespace
|
||||
name:
|
||||
text: 工作区差异空白检查
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: diff
|
||||
truncated: false
|
||||
- text: --check
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 120
|
||||
summary:
|
||||
text: 独立只读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操作、记忆更新或发布,未实机/联机。
|
||||
truncated: false
|
||||
risks:
|
||||
- text: 未进行真实游戏、浏览器界面或联机验证;ESM桥接/局部桩及引擎源码证据不等同于实机验证。
|
||||
truncated: false
|
||||
- text: 测试mjs沿用既有忽略规则,文件在工作区尚未暂存或提交;未执行发布。
|
||||
truncated: false
|
||||
- text: 修复前失败计数为Builder开发期观察,并非本轮Runtime失败回执;当前正式结果为101次通过、90个独立测试。
|
||||
truncated: false
|
||||
risks_truncated: false
|
||||
completed_at: 2026-10-12T06:41:40.687Z
|
||||
history:
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: |-
|
||||
只读审查验收通过(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 通过不要求本次修复源码。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T03:43:33.267Z
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户接受审查结果并明确要求修复报告中的8项已确认问题;用户确认连环传导允许再次增伤。将只读审查范围扩展为这些问题的最小修复与验证,沿用当前目录,保留既有改动。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T03:54:53.001Z
|
||||
- goal_cycle: 2
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: |-
|
||||
本轮独立验收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沿用模块习惯,不为风格增加无关修改。没有真实游戏、浏览器界面或联机实测,本轮引擎检查仅为隔离方法验证。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T04:16:35.786Z
|
||||
- goal_cycle: 2
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户核实驭布规则并明确要求改动:困难hard黑桃,阴间nightmare黑色;保留此前多目标一张杀只判定一次的修复,同步动态描述和难度分支回归验证。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T04:26:08.214Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收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与局部桩结果不等同于实机验证。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T04:35:16.773Z
|
||||
- goal_cycle: 3
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: recovery
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 用户逐项确认官网差异修复:保留远虑虚拟伤害及无双神戟战甲增强;修复狼顾妄尊夺玺引雷、张角体力和报告列出的范围差异,同步文本。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T06:23:10.262Z
|
||||
- goal_cycle: 4
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读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操作、记忆更新或发布,未实机/联机。
|
||||
truncated: false
|
||||
completed_at: 2026-10-12T06:41:40.687Z
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
last_dropped_at: null
|
||||
outcome_counts:
|
||||
pass: 0
|
||||
fail: 0
|
||||
blocked: 0
|
||||
execution-error: 0
|
||||
recovery: 0
|
||||
verification_result: pass
|
||||
verification_report: verification.md
|
||||
archived: true
|
||||
created_at: 2026-10-13T01:31:51.574Z
|
||||
+4
-4
@@ -1,13 +1,13 @@
|
||||
---
|
||||
generated_from_state_version: 24
|
||||
generated_from_state_version: 28
|
||||
---
|
||||
|
||||
# 验证
|
||||
|
||||
## 当前结果
|
||||
|
||||
- 结果: **验收通过,需要你确认**
|
||||
- 验证情况: **已完成检查,但需要你确认验证结果**
|
||||
- 结果: **已归档**
|
||||
- 验证情况: **已完成检查,验证结果已确认**
|
||||
- 目标周期: 4
|
||||
- 迭代: 1
|
||||
- 验证器尝试次数: 1
|
||||
@@ -57,7 +57,7 @@ generated_from_state_version: 24
|
||||
|
||||
## 阻塞项
|
||||
|
||||
- **user**: The generic Skill bridge cannot prove an independent Verifier execution; user confirmation is required before Archive. — next: `await-user`
|
||||
_无。_
|
||||
|
||||
## 风险与跳过的工作
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Outcome
|
||||
|
||||
在梦三战斗手牌区最左侧常驻展示持有遗物的图片列表,长按条目显示遗物描述。
|
||||
|
||||
# Scope
|
||||
|
||||
复用现有持有遗物数据和图片资源,调整战斗遗物界面及必要的手牌布局。
|
||||
图片本身就是列表条目,不增加独立文字列表。图片点击区域48×48 CSS像素,
|
||||
条目间距至少8px,与手牌保留至少12px间隔。遗物过多时局部滚动。
|
||||
长按500ms打开名称、描述和当前状态;滑动、取消和销毁会取消待触发长按。
|
||||
描述浮层支持明确关闭,限制在屏幕可见范围内;提供键盘打开与关闭方式。
|
||||
无图片或加载失败的遗物显示名称替代。保留现有详情弹窗入口作为补充。
|
||||
|
||||
# Non-goals
|
||||
|
||||
不修改遗物效果、获取规则、奖励池或存档,不改动其他游戏模式。
|
||||
不处理其他变更、不发布版本、不自动提交或推送。
|
||||
|
||||
# Acceptance examples
|
||||
|
||||
- 战斗开始后,手牌区最左侧展示当前持有遗物;每件遗物仅有一个图片条目,缺图显示名称。
|
||||
- 点击区域48×48px,条目间距至少8px,与手牌间隔至少12px;窄屏和遗物较多时可滚动且不重叠手牌。
|
||||
- 长按500ms显示对应名称、描述和当前状态;提前松开、滑动或取消不误触;键盘也可打开并关闭详情。
|
||||
- 遗物界面不触发手牌选择、拖动或出牌;既有手牌交互和遗物详情入口仍可用。
|
||||
- 战斗结束或会话销毁后移除列表、浮层、监听器和定时器,恢复被占用的布局空间;再次进入不重复挂载。
|
||||
|
||||
# Decisions
|
||||
|
||||
用户选择当前目录,实现遗物图片列表与长按描述,并明确要求使用UI/UX技能。
|
||||
UI/UX检索支持相邻触摸目标至少8px间距和按下反馈。
|
||||
|
||||
# Verification expectations
|
||||
|
||||
执行针对长按取消、数据展示和销毁清理的定向检查及语法检查;独立验证所有验收项。
|
||||
真实引擎和设备上的触摸、缩放与手牌回归未执行时明确标记边界,不宣称实机通过。
|
||||
@@ -0,0 +1,275 @@
|
||||
schema: comet.native.v4
|
||||
name: mengsan-hand-relics
|
||||
language: en
|
||||
phase: archive
|
||||
status: done
|
||||
state_version: 8
|
||||
brief: brief.md
|
||||
document_constraints_version: 2
|
||||
shape_confirmation_hash: 27831d4a33d10b9652c80f8548467e488fc93bea537bcd845ab109bf372ecaaa
|
||||
spec_changes:
|
||||
- capability: mengsan-hand-relics
|
||||
operation: create
|
||||
source: specs/mengsan-hand-relics/spec.md
|
||||
workspace:
|
||||
isolation: current
|
||||
change_branch: main
|
||||
target_branch: main
|
||||
finish: null
|
||||
loop:
|
||||
stage: done
|
||||
goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 0
|
||||
no_progress_count: 0
|
||||
execution_failure_count: 0
|
||||
previous_unresolved_ids: []
|
||||
next_action: null
|
||||
acceptance:
|
||||
- id: A1
|
||||
source: brief.md
|
||||
text: 战斗开始后,手牌区最左侧展示当前持有遗物;每件遗物仅有一个图片条目,缺图显示名称。
|
||||
result: passed
|
||||
reason:
|
||||
text: mountRelics复用heldRelics和createRelicIcon,逐件图片按钮;Runtime
|
||||
hand-relic-browser确认12条、缺图/加载失败名称替代与刷新。
|
||||
truncated: false
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: 点击区域48×48px,条目间距至少8px,与手牌间隔至少12px;窄屏和遗物较多时可滚动且不重叠手牌。
|
||||
result: passed
|
||||
reason:
|
||||
text: CSS 48x48、gap8、64px条带与88px布局预留;Runtime
|
||||
hand-relic-browser在360/640/1280px确认尺寸、间距及局部滚动。
|
||||
truncated: false
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: 长按500ms显示对应名称、描述和当前状态;提前松开、滑动或取消不误触;键盘也可打开并关闭详情。
|
||||
result: passed
|
||||
reason:
|
||||
text: 500ms定时器、提前松开/移动阈值/scroll/pointercancel/blur取消经源码和组件Runtime检查;hand-relic-dialog补充名称描述状态、屏内modal、Enter/Space打开、Escape/按钮关闭和焦点回归。
|
||||
truncated: false
|
||||
- id: A4
|
||||
source: brief.md
|
||||
text: 遗物界面不触发手牌选择、拖动或出牌;既有手牌交互和遗物详情入口仍可用。
|
||||
result: passed
|
||||
reason:
|
||||
text: strip阻止交互冒泡,手牌入口仅接受属于手牌zone的.card;保留全量详情按钮,Runtime集成检查验证单项/全量入口及pointerdown不向手牌root冒泡。布局仅减可用宽度,未改手牌业务。
|
||||
truncated: false
|
||||
- id: A5
|
||||
source: brief.md
|
||||
text: 战斗结束或会话销毁后移除列表、浮层、监听器和定时器,恢复被占用的布局空间;再次进入不重复挂载。
|
||||
result: passed
|
||||
reason:
|
||||
text: ownResource注册dispose,session逆序释放;strip销毁取消定时器、移除全部注册监听和DOM、恢复CSS变量。Runtime组件与集成检查验证销毁中断长按、重复dispose、modal清理及重挂载唯一条带。
|
||||
truncated: false
|
||||
builder_handoff:
|
||||
candidate_id: 4411b609-0d11-4d73-8fa7-fa402cbb1f88
|
||||
identity_provider: skill-coordinated
|
||||
builder_execution_ref: skill-coordinated:builder:79d2c398-bd34-4cfa-b146-a31feb185d5c
|
||||
iteration: 1
|
||||
summary:
|
||||
text: 手牌左侧遗物图片列表、500ms长按详情、滚动与生命周期清理已实现
|
||||
truncated: false
|
||||
addressed_acceptance_ids:
|
||||
- A1
|
||||
- A2
|
||||
- A3
|
||||
- A4
|
||||
- A5
|
||||
acceptance_review:
|
||||
- id: A1
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- mengsan/ui/relic-strip.js:63-89
|
||||
- mengsan/ui/relics.js:5-91
|
||||
- mengsan/tools/check-hand-relics.mjs
|
||||
note: 复用持有遗物和图片,缺图名称替代
|
||||
- id: A2
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- mengsan/ui/style.css:1379-1434
|
||||
- mengsan/ui/hand/style.css:6-19
|
||||
- mengsan/tools/check-hand-relics.mjs
|
||||
note: 88px预留与64px列表形成24px间距,48px触摸目标和8px行间距
|
||||
- id: A3
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- mengsan/ui/relic-strip.js:13-46
|
||||
- mengsan/ui/relics.js:49-87
|
||||
- mengsan/tools/check-hand-relics.mjs
|
||||
note: 长按打开对应详情;取消、滚动和键盘支持
|
||||
- id: A4
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- mengsan/ui/relic-strip.js:48-58
|
||||
- mengsan/ui/hand/style.css:6-19
|
||||
- mengsan/tools/check-hand-relics.mjs
|
||||
note: 事件阻止冒泡,手牌只调整容器可用宽度,保留全量详情入口
|
||||
- id: A5
|
||||
status: implemented-with-evidence
|
||||
evidence:
|
||||
- mengsan/ui/relic-strip.js:92-106
|
||||
- mengsan/ui/relics.js:88-94
|
||||
- mengsan/tools/check-hand-relics.mjs
|
||||
note: session资源释放调用dispose,移除监听定时器并恢复变量
|
||||
checks:
|
||||
- name:
|
||||
text: focused-browser
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: Edge测试360/640/1280px间距、缺图、长按及取消、键盘、刷新和清理通过
|
||||
truncated: false
|
||||
- name:
|
||||
text: syntax
|
||||
truncated: false
|
||||
result: passed
|
||||
note:
|
||||
text: node --check relics.js/relic-strip.js通过
|
||||
truncated: false
|
||||
checks_truncated: false
|
||||
known_limits:
|
||||
- text: 未在真实无名杀游戏或物理触摸设备上验收;浏览器为隔离组件检查。
|
||||
truncated: false
|
||||
known_limits_truncated: false
|
||||
review: null
|
||||
submitted_at: 2026-10-13T09:54:07.717Z
|
||||
blockers: []
|
||||
verification:
|
||||
candidate_id: 4411b609-0d11-4d73-8fa7-fa402cbb1f88
|
||||
identity_provider: skill-coordinated
|
||||
verifier_execution_ref: skill-coordinated:verifier:77a0df51-2a3e-4976-ba8a-c55403e89206
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
assurance: skill-coordinated
|
||||
verdict: pass
|
||||
checks:
|
||||
- id: hand-relic-browser
|
||||
name:
|
||||
text: hand-relic-browser
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: mengsan/tools/check-hand-relics.mjs
|
||||
truncated: false
|
||||
- text: C:/Users/111/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright
|
||||
truncated: false
|
||||
- text: C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 3919
|
||||
- id: hand-relic-dialog
|
||||
name:
|
||||
text: hand-relic-dialog
|
||||
truncated: false
|
||||
argv_display:
|
||||
- text: -e
|
||||
truncated: false
|
||||
- text: "const fs=require('fs'); const assert=require('assert/strict'); const
|
||||
{chromium}=require('C:/Users/111/.cache/codex-runtimes/codex-primar\
|
||||
y-runtime/dependencies/node/node_modules/playwright');
|
||||
(async()=>{const browser=await
|
||||
chromium.launch({headless:true,executablePath:'C:/Program Files
|
||||
(x86)/Microsoft/Edge/Application/msedge.exe'});try{const page=await
|
||||
browser.newPage({viewport:{width:360,height:640}});await
|
||||
page.setContent('<div id=\"root\"
|
||||
style=\"position:relative;height:156px\"><div id=\"hand\"
|
||||
class=\"mengsan-hand-scroll-shuying\"
|
||||
style=\"position:absolute\"></div></div>');await
|
||||
page.addStyleTag({content:fs.readFileSync('mengsan/ui/style.css','u\
|
||||
tf8')+fs.readFileSync('mengsan/ui/hand/style.css','utf8')});const
|
||||
source=fs.readFileSync('mengsan/ui/relic-strip.js','utf8')+'\\n'+fs\
|
||||
.readFileSync('mengsan/ui/relics.js','utf8').replace(/^import
|
||||
.*;$/gm,'');await page.evaluate(async source=>{const prelude='const
|
||||
ui=window.ui; const heldRelics=run=>run.relics; const
|
||||
createRelicIcon=()=>null;';window.ui={me:document.querySelector('#r\
|
||||
oot'),window:document.body};window.run={relics:[{id:'a',name:'甲',de\
|
||||
scription:'描述甲',pool:'常规',tier:'普通'},{id:'b',name:'乙',description:'\
|
||||
描述乙',pool:'常规',tier:'普通'}]};window.session={active:true,ownResource\
|
||||
:(key,dispose)=>window.cleanup=dispose};window.handEvents=0;ui.me.a\
|
||||
ddEventListener('pointerdown',()=>window.handEvents++);const
|
||||
module=await import(URL.createObjectURL(new
|
||||
Blob([prelude+source],{type:'text/javascript'})));window.mount=modu\
|
||||
le.mountRelics;window.instance=mount(session,run,{status:()=>
|
||||
'状态示例'});},source);const
|
||||
items=page.locator('.mengsan-hand-relics-shuying button');await
|
||||
items.first().focus();await
|
||||
page.keyboard.press('Enter');assert.equal(await
|
||||
page.locator('dialog').count(),1);assert.equal(await
|
||||
page.locator('dialog section').count(),1);assert.ok((await
|
||||
page.locator('dialog').innerText()).includes('描述甲'));assert.ok((awa\
|
||||
it page.locator('dialog').innerText()).includes('状态示例'));const
|
||||
rect=await
|
||||
page.locator('dialog').boundingBox();assert.ok(rect.x>=0&&rect.y>=0\
|
||||
&&rect.x+rect.width<=360&&rect.y+rect.height<=640);await
|
||||
page.keyboard.press('Escape');assert.equal(await
|
||||
page.locator('dialog').count(),0);assert.equal(await
|
||||
items.first().evaluate(e=>document.activeElement===e),true);await
|
||||
page.locator('.mengsan-battle-relics-button-shuying').click();asser\
|
||||
t.equal(await page.locator('dialog section').count(),2);await
|
||||
page.getByRole('button',{name:'关闭',exact:true}).click();await
|
||||
items.first().click();assert.equal(await
|
||||
page.evaluate(()=>handEvents),0);await items.first().focus();await
|
||||
page.keyboard.press('Space');assert.equal(await
|
||||
page.locator('dialog').count(),1);await
|
||||
page.evaluate(()=>cleanup());assert.equal(await
|
||||
page.locator('dialog,.mengsan-hand-relics-shuying,.mengsan-battle-r\
|
||||
elics-button-shuying').count(),0);await
|
||||
page.evaluate(()=>{instance=mount(session,run,{status:()=>
|
||||
'状态示例'});});assert.equal(await
|
||||
page.locator('.mengsan-hand-relics-shuying').count(),1);await
|
||||
page.evaluate(()=>cleanup());console.log('PASS: integrated relic
|
||||
dialog selected/all contents, status, viewport bounds,
|
||||
Enter/Space/Escape, focus return, pointer bubbling isolation,
|
||||
session cleanup and remount');}finally{await
|
||||
browser.close();}})().catch(e=>{console.error(e);process.exitCode=1\
|
||||
});"
|
||||
truncated: false
|
||||
argv_truncated: false
|
||||
cwd_ref: .
|
||||
status: passed
|
||||
exit_code: 0
|
||||
duration_ms: 1298
|
||||
summary:
|
||||
text: 独立只读验收A1–A5通过;复用绑定当前候选hand-relic-browser并补充hand-relic-dialog成功。
|
||||
truncated: false
|
||||
risks:
|
||||
- text: 验收采用隔离 Edge 浏览器组件及遗物详情集成、源码审查;没有运行完整无名杀引擎、物理触摸设备或真实缩放/手牌出牌回归,不代表实机验收通过。
|
||||
truncated: false
|
||||
- text: check-hand-relics.mjs被现有*.mjs忽略规则排除,当前Runtime使用本地文件;后续如提交或迁移必须确保检查文件随交付提供。
|
||||
truncated: false
|
||||
- text: 图索引所涉源码metadata_changed及CSS部分解析覆盖已直接读源码核实,结论以当前候选源码和Runtime证据为准。
|
||||
truncated: false
|
||||
- text: 旧全量check-sts2-relics.mjs因源图集burning_blood.png缺失ENOENT未通过,不能将该全量检查计为通过;此次UI定向Runtime检查不受影响。
|
||||
truncated: false
|
||||
risks_truncated: false
|
||||
completed_at: 2026-10-13T09:58:45.226Z
|
||||
history:
|
||||
- goal_cycle: 1
|
||||
iteration: 1
|
||||
attempt: 1
|
||||
outcome: pass
|
||||
unresolved_ids: []
|
||||
summary:
|
||||
text: 独立只读验收A1–A5通过;复用绑定当前候选hand-relic-browser并补充hand-relic-dialog成功。
|
||||
truncated: false
|
||||
completed_at: 2026-10-13T09:58:45.226Z
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
last_dropped_at: null
|
||||
outcome_counts:
|
||||
pass: 0
|
||||
fail: 0
|
||||
blocked: 0
|
||||
execution-error: 0
|
||||
recovery: 0
|
||||
verification_result: pass
|
||||
verification_report: verification.md
|
||||
archived: true
|
||||
created_at: 2026-10-13T09:47:29.973Z
|
||||
@@ -0,0 +1,27 @@
|
||||
# Battle hand relic list
|
||||
|
||||
梦三战斗手牌区左侧展示征程当前持有的遗物,复用既有图片、名称、描述和战斗状态。
|
||||
列表条目为图片按钮;无图片或图片加载失败以名称替代,具有可访问名称。
|
||||
现有遗物详情入口保留,不改变遗物业务行为。
|
||||
|
||||
### Scenario: 持有遗物展示
|
||||
Acceptance: A1
|
||||
进入战斗后左侧列表逐件展示持有遗物,图片与条目是一体,不另设文字列表。
|
||||
|
||||
### Scenario: 间距和溢出
|
||||
Acceptance: A2
|
||||
点击区域48×48px,条目间距至少8px,与手牌间隔至少12px。
|
||||
窄屏和遗物较多时在列表内滚动,不覆盖手牌。
|
||||
|
||||
### Scenario: 长按描述
|
||||
Acceptance: A3
|
||||
持续按下500ms打开名称、描述和当前状态;提前松开、滚动、取消不会误打开。
|
||||
浮层有关闭方式,位于可见屏幕内;键盘可打开并关闭详情。
|
||||
|
||||
### Scenario: 手牌兼容
|
||||
Acceptance: A4
|
||||
操作遗物不触发手牌选择、拖动或出牌,保留既有遗物详情入口和手牌交互。
|
||||
|
||||
### Scenario: 生命周期
|
||||
Acceptance: A5
|
||||
退出战斗或销毁会话清理列表、浮层、监听器及长按定时器,恢复布局空间;重新进入不重复挂载。
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
generated_from_state_version: 8
|
||||
---
|
||||
|
||||
# Verification
|
||||
|
||||
## Current result
|
||||
|
||||
- Result: **Archived**
|
||||
- Verification status: **Checks completed; result confirmed**
|
||||
- Goal cycle: 1
|
||||
- Iteration: 1
|
||||
- Verifier attempt: 1
|
||||
- Completed: 2026-10-13T09:58:45.226Z
|
||||
- Summary: 独立只读验收A1–A5通过;复用绑定当前候选hand-relic-browser并补充hand-relic-dialog成功。
|
||||
|
||||
## Acceptance
|
||||
|
||||
| ID | Result | Source | Criterion | Reason |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| A1 | passed | brief.md | 战斗开始后,手牌区最左侧展示当前持有遗物;每件遗物仅有一个图片条目,缺图显示名称。 | mountRelics复用heldRelics和createRelicIcon,逐件图片按钮;Runtime hand-relic-browser确认12条、缺图/加载失败名称替代与刷新。 |
|
||||
| A2 | passed | brief.md | 点击区域48×48px,条目间距至少8px,与手牌间隔至少12px;窄屏和遗物较多时可滚动且不重叠手牌。 | CSS 48x48、gap8、64px条带与88px布局预留;Runtime hand-relic-browser在360/640/1280px确认尺寸、间距及局部滚动。 |
|
||||
| A3 | passed | brief.md | 长按500ms显示对应名称、描述和当前状态;提前松开、滑动或取消不误触;键盘也可打开并关闭详情。 | 500ms定时器、提前松开/移动阈值/scroll/pointercancel/blur取消经源码和组件Runtime检查;hand-relic-dialog补充名称描述状态、屏内modal、Enter/Space打开、Escape/按钮关闭和焦点回归。 |
|
||||
| A4 | passed | brief.md | 遗物界面不触发手牌选择、拖动或出牌;既有手牌交互和遗物详情入口仍可用。 | strip阻止交互冒泡,手牌入口仅接受属于手牌zone的.card;保留全量详情按钮,Runtime集成检查验证单项/全量入口及pointerdown不向手牌root冒泡。布局仅减可用宽度,未改手牌业务。 |
|
||||
| A5 | passed | brief.md | 战斗结束或会话销毁后移除列表、浮层、监听器和定时器,恢复被占用的布局空间;再次进入不重复挂载。 | ownResource注册dispose,session逆序释放;strip销毁取消定时器、移除全部注册监听和DOM、恢复CSS变量。Runtime组件与集成检查验证销毁中断长按、重复dispose、modal清理及重挂载唯一条带。 |
|
||||
|
||||
## Checks
|
||||
|
||||
| Check | Command | Working directory | Status | Exit | Duration |
|
||||
| --- | --- | --- | --- | ---: | ---: |
|
||||
| hand-relic-browser | mengsan/tools/check-hand-relics.mjs C:/Users/111/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe | . | passed | 0 | 3919 ms |
|
||||
| hand-relic-dialog | -e const fs=require('fs'); const assert=require('assert/strict'); const {chromium}=require('C:/Users/111/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright'); (async()=>{const browser=await chromium.launch({headless:true,executablePath:'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'});try{const page=await browser.newPage({viewport:{width:360,height:640}});await page.setContent('<div id="root" style="position:relative;height:156px"><div id="hand" class="mengsan-hand-scroll-shuying" style="position:absolute"></div></div>');await page.addStyleTag({content:fs.readFileSync('mengsan/ui/style.css','utf8')+fs.readFileSync('mengsan/ui/hand/style.css','utf8')});const source=fs.readFileSync('mengsan/ui/relic-strip.js','utf8')+'\n'+fs.readFileSync('mengsan/ui/relics.js','utf8').replace(/^import .*;$/gm,'');await page.evaluate(async source=>{const prelude='const ui=window.ui; const heldRelics=run=>run.relics; const createRelicIcon=()=>null;';window.ui={me:document.querySelector('#root'),window:document.body};window.run={relics:[{id:'a',name:'甲',description:'描述甲',pool:'常规',tier:'普通'},{id:'b',name:'乙',description:'描述乙',pool:'常规',tier:'普通'}]};window.session={active:true,ownResource:(key,dispose)=>window.cleanup=dispose};window.handEvents=0;ui.me.addEventListener('pointerdown',()=>window.handEvents++);const module=await import(URL.createObjectURL(new Blob([prelude+source],{type:'text/javascript'})));window.mount=module.mountRelics;window.instance=mount(session,run,{status:()=> '状态示例'});},source);const items=page.locator('.mengsan-hand-relics-shuying button');await items.first().focus();await page.keyboard.press('Enter');assert.equal(await page.locator('dialog').count(),1);assert.equal(await page.locator('dialog section').count(),1);assert.ok((await page.locator('dialog').innerText()).includes('描述甲'));assert.ok((await page.locator('dialog').innerText()).includes('状态示例'));const rect=await page.locator('dialog').boundingBox();assert.ok(rect.x>=0&&rect.y>=0&&rect.x+rect.width<=360&&rect.y+rect.height<=640);await page.keyboard.press('Escape');assert.equal(await page.locator('dialog').count(),0);assert.equal(await items.first().evaluate(e=>document.activeElement===e),true);await page.locator('.mengsan-battle-relics-button-shuying').click();assert.equal(await page.locator('dialog section').count(),2);await page.getByRole('button',{name:'关闭',exact:true}).click();await items.first().click();assert.equal(await page.evaluate(()=>handEvents),0);await items.first().focus();await page.keyboard.press('Space');assert.equal(await page.locator('dialog').count(),1);await page.evaluate(()=>cleanup());assert.equal(await page.locator('dialog,.mengsan-hand-relics-shuying,.mengsan-battle-relics-button-shuying').count(),0);await page.evaluate(()=>{instance=mount(session,run,{status:()=> '状态示例'});});assert.equal(await page.locator('.mengsan-hand-relics-shuying').count(),1);await page.evaluate(()=>cleanup());console.log('PASS: integrated relic dialog selected/all contents, status, viewport bounds, Enter/Space/Escape, focus return, pointer bubbling isolation, session cleanup and remount');}finally{await browser.close();}})().catch(e=>{console.error(e);process.exitCode=1}); | . | passed | 0 | 1298 ms |
|
||||
|
||||
### Builder-reported evidence
|
||||
|
||||
These are Builder reports, not Runtime check receipts or independent verification results.
|
||||
|
||||
- focused-browser: passed — Edge测试360/640/1280px间距、缺图、长按及取消、键盘、刷新和清理通过
|
||||
- syntax: passed — node --check relics.js/relic-strip.js通过
|
||||
- Known limitation: 未在真实无名杀游戏或物理触摸设备上验收;浏览器为隔离组件检查。
|
||||
|
||||
## Blockers
|
||||
|
||||
_None._
|
||||
|
||||
## Risks and skipped work
|
||||
|
||||
- 验收采用隔离 Edge 浏览器组件及遗物详情集成、源码审查;没有运行完整无名杀引擎、物理触摸设备或真实缩放/手牌出牌回归,不代表实机验收通过。
|
||||
- check-hand-relics.mjs被现有*.mjs忽略规则排除,当前Runtime使用本地文件;后续如提交或迁移必须确保检查文件随交付提供。
|
||||
- 图索引所涉源码metadata_changed及CSS部分解析覆盖已直接读源码核实,结论以当前候选源码和Runtime证据为准。
|
||||
- 旧全量check-sts2-relics.mjs因源图集burning_blood.png缺失ENOENT未通过,不能将该全量检查计为通过;此次UI定向Runtime检查不受影响。
|
||||
|
||||
## Previous iterations
|
||||
|
||||
| Goal cycle | Iteration | Attempt | Outcome | Unresolved | Summary | Completed |
|
||||
| ---: | ---: | ---: | --- | --- | --- | --- |
|
||||
| 1 | 1 | 1 | pass | — | 独立只读验收A1–A5通过;复用绑定当前候选hand-relic-browser并补充hand-relic-dialog成功。 | 2026-10-13T09:58:45.226Z |
|
||||
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
独立只读验收A1–A5通过;复用绑定当前候选hand-relic-browser并补充hand-relic-dialog成功。
|
||||
@@ -1,146 +0,0 @@
|
||||
# Recovery authorized by the user on 2026-10-13.
|
||||
# CLI-generated Shape state; historical version 24 evidence is backed up in
|
||||
# .planning/comet-state-recovery-20261013. All acceptance requires revalidation.
|
||||
schema: comet.native.v4
|
||||
name: shuying-skill-tianshu-review
|
||||
language: zh-CN
|
||||
phase: shape
|
||||
status: await-user
|
||||
state_version: 25
|
||||
brief: brief.md
|
||||
document_constraints_version: 2
|
||||
shape_confirmation_hash: 73614dd2595713e137f716f16fb11eed1c7d59096b3fd243d53ff1d97cedd7d3
|
||||
spec_changes:
|
||||
- capability: shuying-skill-correctness
|
||||
operation: create
|
||||
source: specs/shuying-skill-correctness/spec.md
|
||||
workspace:
|
||||
isolation: current
|
||||
change_branch: null
|
||||
target_branch: null
|
||||
finish: null
|
||||
loop:
|
||||
stage: await-user
|
||||
goal_cycle: 1
|
||||
iteration: 0
|
||||
attempt: 0
|
||||
retry_epoch: 0
|
||||
failed_iteration_count: 0
|
||||
no_progress_count: 0
|
||||
execution_failure_count: 0
|
||||
previous_unresolved_ids: []
|
||||
next_action: confirm-shape
|
||||
acceptance:
|
||||
- id: A1
|
||||
source: brief.md
|
||||
text: 羁绊的 filter、mod、async content 在没有开发者全局对象时,能通过模块导入获取引擎依赖。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A2
|
||||
source: brief.md
|
||||
text: 江东基业二三级对应子技能通过正确 group 挂载并执行,低等级不重复叠加。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A3
|
||||
source: brief.md
|
||||
text: 驭布对一张多目标杀只判定一次,成功时对各存活目标只造成一次伤害。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A4
|
||||
source: brief.md
|
||||
text: 驭布困难难度黑桃成功、梅花失败;阴间难度黑色成功、红色失败,动态描述与难度条件一致。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A5
|
||||
source: brief.md
|
||||
text: 狼顾失败后仅限制当前回合;黑桃成功时恢复次数,即使没有可弃手牌也不会错误占用次数。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A6
|
||||
source: brief.md
|
||||
text: 奋威开局及中途获得时各初始化2蓄力,同次持有不因入场/开局事件重复添加,不重置共享蓄力。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A7
|
||||
source: brief.md
|
||||
text: 舌战与狼顾仅从允许被发动者弃置的牌中随机选择。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A8
|
||||
source: brief.md
|
||||
text: 白仪与九天仅从允许被发动者获得的牌中随机选择。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A9
|
||||
source: brief.md
|
||||
text: 连环伤害允许再次增伤,既有牌伤增益不添加 notLink 排除。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A10
|
||||
source: brief.md
|
||||
text: 新增定向回归验证并运行相关现有天书测试,说明真实游戏/联机实测边界,保留无关改动。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A11
|
||||
source: brief.md
|
||||
text: 狼顾阴间梅花成功、红色失败,普通/困难仅黑桃成功;动态描述同步。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A12
|
||||
source: brief.md
|
||||
text: 妄尊阴间累计1点伤害即反击,困难1点不反击,未受伤时弃牌数仍为困难1/阴间2。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A13
|
||||
source: brief.md
|
||||
text: 夺玺阴间替换摸牌后仅自己摸2,困难双方各摸1;动态描述、提示同步。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A14
|
||||
source: brief.md
|
||||
text: 引雷困难自己回合不触发、阴间自己回合触发;描述同步回合条件。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A15
|
||||
source: brief.md
|
||||
text: 张角三难度体力和上限8/16/24,起始手牌6/8/10和追加技能保留。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A16
|
||||
source: brief.md
|
||||
text: 疑神随机获取各目标合法装备,正常难度分支只选一名敌人,阴间所有合格敌人各一张;禁止选择受保护装备。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A17
|
||||
source: brief.md
|
||||
text: 反馈受到多点伤害有逐点发动机会,可对友方来源发动,源牌耗尽后停止;自伤只获取自身装备。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A18
|
||||
source: brief.md
|
||||
text: 狼顾接受其他友方来源;暴虐满血也失去1体力并允许任意角色;雷击允许其他友方,谏征允许其他角色使用的杀,不增加阵营限制。
|
||||
result: pending
|
||||
reason: null
|
||||
- id: A19
|
||||
source: brief.md
|
||||
text: 远虑阴间保留unreal并明确描述;无双/神戟/战甲增强不回退,全部改动描述与提示一致。
|
||||
result: pending
|
||||
reason: null
|
||||
builder_handoff: null
|
||||
blockers: []
|
||||
verification: null
|
||||
history: []
|
||||
history_overflow:
|
||||
dropped_entries: 0
|
||||
first_dropped_at: null
|
||||
last_dropped_at: null
|
||||
outcome_counts:
|
||||
pass: 0
|
||||
fail: 0
|
||||
blocked: 0
|
||||
execution-error: 0
|
||||
recovery: 0
|
||||
verification_result: pending
|
||||
verification_report: null
|
||||
archived: false
|
||||
created_at: 2026-10-13T01:31:51.574Z
|
||||
@@ -0,0 +1,27 @@
|
||||
# Battle hand relic list
|
||||
|
||||
梦三战斗手牌区左侧展示征程当前持有的遗物,复用既有图片、名称、描述和战斗状态。
|
||||
列表条目为图片按钮;无图片或图片加载失败以名称替代,具有可访问名称。
|
||||
现有遗物详情入口保留,不改变遗物业务行为。
|
||||
|
||||
### Scenario: 持有遗物展示
|
||||
Acceptance: A1
|
||||
进入战斗后左侧列表逐件展示持有遗物,图片与条目是一体,不另设文字列表。
|
||||
|
||||
### Scenario: 间距和溢出
|
||||
Acceptance: A2
|
||||
点击区域48×48px,条目间距至少8px,与手牌间隔至少12px。
|
||||
窄屏和遗物较多时在列表内滚动,不覆盖手牌。
|
||||
|
||||
### Scenario: 长按描述
|
||||
Acceptance: A3
|
||||
持续按下500ms打开名称、描述和当前状态;提前松开、滚动、取消不会误打开。
|
||||
浮层有关闭方式,位于可见屏幕内;键盘可打开并关闭详情。
|
||||
|
||||
### Scenario: 手牌兼容
|
||||
Acceptance: A4
|
||||
操作遗物不触发手牌选择、拖动或出牌,保留既有遗物详情入口和手牌交互。
|
||||
|
||||
### Scenario: 生命周期
|
||||
Acceptance: A5
|
||||
退出战斗或销毁会话清理列表、浮层、监听器及长按定时器,恢复布局空间;重新进入不重复挂载。
|
||||
@@ -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均保留。狼顾、妄尊、夺玺、引雷及所有本轮改动的静态/动态文本和选择提示与最终规则一致。
|
||||
+1
-11
@@ -6,7 +6,7 @@ import { configureDiagnostics } from "./diagnostics/index.js";
|
||||
|
||||
export const type = "extension";
|
||||
export let shuYing = new Object();
|
||||
const shuYingLocalVersion = "2.0.2.3-rc.3";
|
||||
const shuYingLocalVersion = "2.0.2.3-rc.5";
|
||||
|
||||
shuYing.name = "术樱包";
|
||||
shuYing.editable = false;
|
||||
@@ -291,16 +291,6 @@ shuYing.help = () => {
|
||||
|
||||
};
|
||||
|
||||
//本地资源修复
|
||||
shuYing.RepairBug = () => {
|
||||
updater.repairCoreFiles(shuYing);
|
||||
};
|
||||
|
||||
//重新下载重要js文件
|
||||
shuYing.RepairBugGo = () => {
|
||||
updater.repairCoreFiles(shuYing);
|
||||
};
|
||||
|
||||
shuYing.Diuse_downText = function () {
|
||||
time = setTimeout(() => {
|
||||
}, 1000)
|
||||
|
||||
+14
@@ -1,5 +1,6 @@
|
||||
import { getDirectDamageSkillCount } from "./tianshu/directDamage.js";
|
||||
import { getSkillRecoverCount } from "./tianshu/skillRecover.js";
|
||||
import { getSkillCardCount } from "./tianshu/skillCard.js";
|
||||
|
||||
export default function initFunction(lib, game, ui, get, ai, _status, shuYing = {}) {
|
||||
game.shuying_getDiscardNamesNum = function () {
|
||||
@@ -397,6 +398,10 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
return getSkillRecoverCount(player, currentSkills, lib)
|
||||
>= Math.max(1, Number(condition.count) || 1);
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
return getSkillCardCount(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") {
|
||||
@@ -445,6 +450,9 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
if (condition.type == "skillRecover") {
|
||||
return getSkillRecoverCount(player, currentSkills, lib) > 0;
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
return getSkillCardCount(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));
|
||||
@@ -482,6 +490,12 @@ export default function initFunction(lib, game, ui, get, ai, _status, shuYing =
|
||||
return `已实际回复体力的奖励技能 ${Math.min(count, need)}`
|
||||
+ `/${need};首次有效技能回复后登记`;
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
const count = getSkillCardCount(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") {
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
}
|
||||
.mengsan-hand-scroll-shuying {
|
||||
overflow: visible !important;
|
||||
left: 0 !important;
|
||||
left: var(--mengsan-relic-space, 0px) !important;
|
||||
top: 0 !important;
|
||||
bottom: 0 !important;
|
||||
width: 100% !important;
|
||||
width: calc(100% - var(--mengsan-relic-space, 0px)) !important;
|
||||
height: 100% !important;
|
||||
padding: 0 !important;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
export function mountRelicStrip(root, getRelics, open, createIcon,
|
||||
document, window) {
|
||||
const strip = document.createElement("div");
|
||||
strip.className = "mengsan-hand-relics-shuying";
|
||||
strip.setAttribute("aria-label", "持有遗物,长按查看描述");
|
||||
const before = root.style.getPropertyValue("--mengsan-relic-space");
|
||||
const priority = root.style.getPropertyPriority("--mengsan-relic-space");
|
||||
let pending = null, timer = null, signature = null, disposed = false;
|
||||
const cancel = () => {
|
||||
if (timer !== null) window.clearTimeout(timer);
|
||||
pending?.button.classList.remove("is-pressed");
|
||||
timer = null;
|
||||
pending = null;
|
||||
};
|
||||
const stop = event => event.stopPropagation();
|
||||
const down = event => {
|
||||
stop(event);
|
||||
cancel();
|
||||
const button = event.target.closest?.("button");
|
||||
if (!button || !strip.contains(button) || event.button !== 0) return;
|
||||
pending = { button, id: event.pointerId,
|
||||
x: event.clientX, y: event.clientY };
|
||||
button.classList.add("is-pressed");
|
||||
timer = window.setTimeout(() => {
|
||||
const relic = getRelics().find(item => item.id === button.dataset.relicId);
|
||||
cancel();
|
||||
if (!disposed && relic) open(relic);
|
||||
}, 500);
|
||||
};
|
||||
const move = event => {
|
||||
if (event.pointerId !== pending?.id) return;
|
||||
if (Math.hypot(event.clientX - pending.x,
|
||||
event.clientY - pending.y) > 8) cancel();
|
||||
};
|
||||
const up = event => {
|
||||
if (event.pointerId === pending?.id) cancel();
|
||||
};
|
||||
const click = event => {
|
||||
stop(event);
|
||||
event.preventDefault();
|
||||
if (event.detail !== 0) return;
|
||||
const button = event.target.closest?.("button");
|
||||
const relic = getRelics().find(item => item.id === button?.dataset.relicId);
|
||||
if (relic) open(relic);
|
||||
};
|
||||
const context = event => { stop(event); event.preventDefault(); };
|
||||
const listeners = [
|
||||
[strip, "pointerdown", down], [strip, "pointermove", stop],
|
||||
[strip, "pointerup", stop], [strip, "click", click],
|
||||
[strip, "keydown", stop], [strip, "keyup", stop],
|
||||
[strip, "contextmenu", context], [strip, "scroll", cancel],
|
||||
[strip, "wheel", stop], [strip, "touchstart", stop],
|
||||
[strip, "touchmove", stop], [strip, "touchend", stop],
|
||||
[window, "pointermove", move, true],
|
||||
[window, "pointerup", up, true],
|
||||
[window, "pointercancel", up, true], [window, "blur", cancel],
|
||||
];
|
||||
for (const args of listeners) args[0].addEventListener(...args.slice(1));
|
||||
root.appendChild(strip);
|
||||
const refresh = () => {
|
||||
if (disposed) return;
|
||||
const relics = getRelics();
|
||||
const next = JSON.stringify(relics.map(item => [item.id, item.image, item.name]));
|
||||
if (next === signature) return;
|
||||
signature = next;
|
||||
cancel();
|
||||
strip.replaceChildren();
|
||||
strip.hidden = relics.length === 0;
|
||||
root.style.setProperty("--mengsan-relic-space", relics.length ? "88px" : "0px");
|
||||
for (const relic of relics) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.dataset.relicId = relic.id;
|
||||
button.setAttribute("aria-label", `${relic.name},长按查看描述`);
|
||||
button.setAttribute("aria-haspopup", "dialog");
|
||||
const fallback = document.createElement("span");
|
||||
fallback.textContent = relic.name;
|
||||
const icon = createIcon(relic.image);
|
||||
if (icon) {
|
||||
fallback.hidden = true;
|
||||
icon.addEventListener("error", () => { fallback.hidden = false; },
|
||||
{ once: true });
|
||||
button.appendChild(icon);
|
||||
}
|
||||
button.appendChild(fallback);
|
||||
strip.appendChild(button);
|
||||
}
|
||||
};
|
||||
refresh();
|
||||
return { refresh, dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
cancel();
|
||||
for (const args of listeners) args[0].removeEventListener(...args.slice(1));
|
||||
strip.remove();
|
||||
const value = root.style.getPropertyValue("--mengsan-relic-space");
|
||||
if (value === "88px" || value === "0px") {
|
||||
if (before) root.style.setProperty("--mengsan-relic-space", before, priority);
|
||||
else root.style.removeProperty("--mengsan-relic-space");
|
||||
}
|
||||
} };
|
||||
}
|
||||
+15
-7
@@ -1,10 +1,11 @@
|
||||
import { ui } from "../../../../noname.js";
|
||||
import { heldRelics } from "../relics/definitions.js";
|
||||
import { createRelicIcon } from "./relic-icon.js";
|
||||
import { mountRelicStrip } from "./relic-strip.js";
|
||||
|
||||
export function mountRelics(session, run, battleRelics) {
|
||||
let dialog = null, list = null, previousFocus = null, disposed = false;
|
||||
const relics = heldRelics(run);
|
||||
let relics = heldRelics(run), selected = null, strip = null;
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "mengsan-battle-piles-button-shuying " +
|
||||
@@ -21,6 +22,9 @@ export function mountRelics(session, run, battleRelics) {
|
||||
if (previousFocus?.isConnected) previousFocus.focus();
|
||||
};
|
||||
const refresh = () => {
|
||||
relics = heldRelics(run);
|
||||
button.textContent = `遗物 ${relics.length}`;
|
||||
strip?.refresh();
|
||||
if (!list) return;
|
||||
list.replaceChildren();
|
||||
if (!relics.length) {
|
||||
@@ -28,7 +32,7 @@ export function mountRelics(session, run, battleRelics) {
|
||||
empty.textContent = "本次征程尚未获得遗物。";
|
||||
list.appendChild(empty);
|
||||
}
|
||||
for (const relic of relics) {
|
||||
for (const relic of selected ? relics.filter(r => r.id === selected) : relics) {
|
||||
const row = document.createElement("section");
|
||||
const icon=createRelicIcon(relic.image);if(icon)row.appendChild(icon);
|
||||
const name = document.createElement("h3");
|
||||
@@ -41,18 +45,19 @@ export function mountRelics(session, run, battleRelics) {
|
||||
list.appendChild(row);
|
||||
}
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
const open = relic => {
|
||||
if (disposed || !session.active || dialog) return;
|
||||
selected = relic?.id || null;
|
||||
previousFocus = document.activeElement;
|
||||
dialog = document.createElement("dialog");
|
||||
const currentDialog = dialog;
|
||||
dialog.className = "mengsan-deck-dialog-shuying " +
|
||||
"mengsan-relic-dialog-shuying";
|
||||
dialog.setAttribute("aria-label", "遗物(道具)");
|
||||
dialog.setAttribute("aria-label", relic?.name || "遗物(道具)");
|
||||
const header = document.createElement("header");
|
||||
header.className = "ms-deck-header";
|
||||
const title = document.createElement("h2");
|
||||
title.textContent = "遗物(道具)";
|
||||
title.textContent = relic?.name || "遗物(道具)";
|
||||
const dismiss = document.createElement("button");
|
||||
dismiss.type = "button";
|
||||
dismiss.className = "ms-deck-button";
|
||||
@@ -73,10 +78,13 @@ export function mountRelics(session, run, battleRelics) {
|
||||
document.body.appendChild(dialog);
|
||||
try { dialog.showModal(); dismiss.focus(); }
|
||||
catch (error) { close(); throw error; }
|
||||
});
|
||||
};
|
||||
button.addEventListener("click", () => open());
|
||||
if (ui.me) strip = mountRelicStrip(ui.me, () => heldRelics(run),
|
||||
open, createRelicIcon, document, window);
|
||||
const dispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true; close(); button.remove();
|
||||
disposed = true; close(); strip?.dispose(); button.remove();
|
||||
};
|
||||
session.ownResource(button, dispose);
|
||||
return { dispose, refresh };
|
||||
|
||||
@@ -1376,6 +1376,62 @@ body.mengsan-battle-ui-shuying :is(
|
||||
object-fit: contain;
|
||||
}
|
||||
.mengsan-battle-relics-button-shuying { top: 162px; }
|
||||
.mengsan-hand-relics-shuying {
|
||||
position: absolute !important;
|
||||
left: 0;
|
||||
bottom: 8px;
|
||||
width: 64px;
|
||||
max-height: calc(100% - 16px);
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
touch-action: pan-y;
|
||||
pointer-events: auto;
|
||||
z-index: 260;
|
||||
background: #234844e6;
|
||||
border-radius: 8px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d8ba83 #234844;
|
||||
}
|
||||
.mengsan-hand-relics-shuying[hidden] { display: none; }
|
||||
.mengsan-hand-relics-shuying > button {
|
||||
position: relative !important;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
padding: 0;
|
||||
border: 1px solid #d8ba8380;
|
||||
border-radius: 6px;
|
||||
background: #17332f;
|
||||
color: #fff1cb;
|
||||
cursor: pointer;
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
.mengsan-hand-relics-shuying > button:is(.is-pressed, :focus-visible) {
|
||||
outline: 2px solid #d8ba83;
|
||||
outline-offset: -2px;
|
||||
background: #3b6258;
|
||||
}
|
||||
.mengsan-hand-relics-shuying > button > img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
pointer-events: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
.mengsan-hand-relics-shuying > button > span {
|
||||
font: 12px/1.4 "Microsoft YaHei", sans-serif;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.mengsan-relic-dialog-shuying { max-width: 620px; }
|
||||
.mengsan-relic-list-shuying { overflow-y: auto; padding: 12px 0; }
|
||||
.mengsan-relic-list-shuying section {
|
||||
|
||||
@@ -78,6 +78,9 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
|
||||
if (condition.type == "skillRecover") {
|
||||
return ["直接回复体力的过关技能"];
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
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") {
|
||||
@@ -108,6 +111,11 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
|
||||
+ "按当前仍持有的不同技能译名统计,新等级从后续回复生效;"
|
||||
+ "二级追加回复不会再次触发桃园,三级溢出只增加体力上限";
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
return "奖励技能首次实际使用或打出视为牌、转化牌后登记,子技能归到主技能;"
|
||||
+ "按当前仍持有的不同技能译名统计,新等级从后续用牌生效;"
|
||||
+ "响应打出只登记;目标调整至少保留1个,三级对全部指定目标再次结算";
|
||||
}
|
||||
if (!condition.type && [...(condition.all || []), ...(condition.any || [])].some(item => item?.type == "skillNames")) {
|
||||
return "按不同译名统计;同译名的不同技能 ID 只计 1 种过关技能";
|
||||
}
|
||||
@@ -472,25 +480,12 @@ export default function initShuYingMenu({ lib, game, ui, shuYing, updater }) {
|
||||
};
|
||||
void shuYing.updateMengsanModuleMenu();
|
||||
|
||||
menu.repairBug = {
|
||||
name: "本地资源修复",
|
||||
clear: true,
|
||||
onclick() {
|
||||
if (confirm("点击确定会检测本地资源,并尝试修复。") && shuYing.m_bIsDownload) {
|
||||
shuYing.m_bIsDownload = false;
|
||||
shuYing.RepairBug();
|
||||
} else if (!shuYing.m_bIsDownload) {
|
||||
alert("有其他文件正在下载,请稍后再试吧。");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
menu.filling = {
|
||||
name: "查漏补缺 <--补齐丢失资源 建议更新后必点",
|
||||
name: "查漏补缺",
|
||||
clear: true,
|
||||
onclick() {
|
||||
if (
|
||||
confirm("点击确定会检测本地丢失资源,并开始修补;若是武将立绘则是静态,若需动态请手动下载(请注意流量使用情况!务必在WIFI状态下修补)") &&
|
||||
confirm("检查当前已安装版本的核心文件、图片和配音,修复缺失或损坏的文件。立绘为静态资源,动态立绘需手动下载。请注意流量使用,建议在 WiFi 下执行。") &&
|
||||
shuYing.m_bIsDownload
|
||||
) {
|
||||
shuYing.m_bIsDownload = false;
|
||||
|
||||
@@ -214,6 +214,35 @@ const tianshuConfig = {
|
||||
banned: ["huoxin", "jueqing", "qinqing", "beige", "huashen", "drlt_zhiti", "olzhiti", "xinfu_pdgyingshi", "rebeige"],
|
||||
},
|
||||
bonds: [
|
||||
{
|
||||
id: "yinpai",
|
||||
name: "印牌",
|
||||
condition: { type: "skillCard" },
|
||||
levels: [
|
||||
{
|
||||
level: 1,
|
||||
count: 1,
|
||||
skills: ["shuYing_Tianshu_BondYinpai1"],
|
||||
text: "你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,"
|
||||
+ "至少保留1个目标",
|
||||
},
|
||||
{
|
||||
level: 2,
|
||||
count: 2,
|
||||
skills: ["shuYing_Tianshu_BondYinpai2"],
|
||||
text: "你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,"
|
||||
+ "至少保留1个目标,且此牌无法被响应",
|
||||
},
|
||||
{
|
||||
level: 3,
|
||||
count: 3,
|
||||
skills: ["shuYing_Tianshu_BondYinpai3"],
|
||||
text: "你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,"
|
||||
+ "至少保留1个目标,且此牌无法被响应,"
|
||||
+ "并对全部指定目标额外结算一次,不重新选择目标",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "taoyuan",
|
||||
name: "桃园",
|
||||
|
||||
@@ -7,6 +7,9 @@ export function getDirectDamageOrigin(event, lib, get) {
|
||||
const visited = new Set();
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
if (["card", "precard", "postcard"].includes(current.type)) {
|
||||
return null;
|
||||
}
|
||||
if (["useCard", "respond", "damage", "_lianhuan", "_lianhuan2"]
|
||||
.includes(current.name) || current.name?.endsWith("_cost")) {
|
||||
return null;
|
||||
|
||||
+128
-6
@@ -4,6 +4,8 @@ import { directDamageStorage, getDirectDamageOrigin,
|
||||
getDirectDamageSkillCount } from "./directDamage.js";
|
||||
import { skillRecoverStorage, getSkillRecoverOrigin, getSkillRecoverCount,
|
||||
wrapSkillRecoverEvent } from "./skillRecover.js";
|
||||
import { skillCardStorage, getSkillCardOrigin, getSkillCardCount,
|
||||
getSkillCardTargetOptions, applySkillCardEffects } from "./skillCard.js";
|
||||
import { lib, game, ui, get, ai, _status } from "../../../noname.js";
|
||||
|
||||
export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
@@ -756,6 +758,10 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
return getSkillRecoverCount(player, owned, lib)
|
||||
>= Math.max(1, Number(condition.count) || 1);
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
return getSkillCardCount(player, owned, lib)
|
||||
>= Math.max(1, Number(condition.count) || 1);
|
||||
}
|
||||
if (condition.type == "keyword") {
|
||||
if (!condition.keyword) return false;
|
||||
const marker = `【${condition.keyword}】`;
|
||||
@@ -836,6 +842,9 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
if (condition.type == "skillRecover") {
|
||||
return getSkillRecoverCount(player, [skill], lib) > 0;
|
||||
}
|
||||
if (condition.type == "skillCard") {
|
||||
return getSkillCardCount(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`];
|
||||
@@ -1364,6 +1373,7 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
game.addGlobalSkill("shuYing_Tianshu_DirectDamageCapture");
|
||||
game.addGlobalSkill("shuYing_Tianshu_DirectDamageSettle");
|
||||
game.addGlobalSkill("shuYing_Tianshu_SkillRecoverCapture");
|
||||
game.addGlobalSkill("shuYing_Tianshu_SkillCardCapture");
|
||||
},
|
||||
},
|
||||
shuYing_Tianshu_DefendingControl: {
|
||||
@@ -1402,9 +1412,15 @@ 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_BondDirectDamage1: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondDirectDamage2: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondDirectDamage3: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_DirectDamageCapture: {
|
||||
mode: ["boss"],
|
||||
trigger: { global: "damageBegin1" },
|
||||
@@ -1483,9 +1499,15 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
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_BondTaoyuan1: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondTaoyuan2: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondTaoyuan3: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_SkillRecoverCapture: {
|
||||
mode: ["boss"],
|
||||
trigger: { global: "recoverBegin" },
|
||||
@@ -1509,6 +1531,95 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
if (record.level >= 1) trigger.num++;
|
||||
},
|
||||
},
|
||||
shuYing_Tianshu_BondYinpai1: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondYinpai2: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_BondYinpai3: {
|
||||
charlotte: true, popup: false, nopop: true,
|
||||
},
|
||||
shuYing_Tianshu_SkillCardCapture: {
|
||||
mode: ["boss"],
|
||||
trigger: { global: ["useCard2", "respond"] },
|
||||
forced: true,
|
||||
silent: true,
|
||||
charlotte: true,
|
||||
firstDo: true,
|
||||
priority: 1000,
|
||||
filter(event) {
|
||||
const player = event.player;
|
||||
if (!state.running || event.shuYing_tianshu_yinpaiHandled
|
||||
|| !player?.isIn() || !isCapturedPlayerSide(player)
|
||||
|| (isVirtualIdol(player)
|
||||
&& !canVirtualIdolReceiveRewards())) return false;
|
||||
return Boolean(getSkillCardOrigin(event, lib, get));
|
||||
},
|
||||
async content(event, trigger) {
|
||||
if (trigger.shuYing_tianshu_yinpaiHandled) return;
|
||||
const origin = getSkillCardOrigin(trigger, lib, get);
|
||||
if (!origin) return;
|
||||
trigger.shuYing_tianshu_yinpaiHandled = true;
|
||||
const player = trigger.player;
|
||||
const level = player.storage.shuYing_tianshu_bondLevels
|
||||
?.yinpai || 0;
|
||||
const rewardSkill = getTianshuRewardSkills(player)
|
||||
.find(current => get.sourceSkillFor(current)
|
||||
== origin.skill);
|
||||
if (rewardSkill) {
|
||||
const recorded = player.storage[skillCardStorage] || [];
|
||||
if (!recorded.includes(rewardSkill)) {
|
||||
recorded.push(rewardSkill);
|
||||
player.storage[skillCardStorage] = recorded;
|
||||
game.broadcast((current, key, value) => {
|
||||
current.storage[key] = value;
|
||||
}, player, skillCardStorage, recorded);
|
||||
player.syncStorage?.(skillCardStorage);
|
||||
refreshTianshuBonds(player);
|
||||
game.log(player, "登记印牌技能",
|
||||
`【${get.translation(rewardSkill)}】`);
|
||||
}
|
||||
}
|
||||
if (!level || trigger.name != "useCard") return;
|
||||
const options = getSkillCardTargetOptions(
|
||||
trigger, lib, game, get);
|
||||
const candidates = options.add.concat(options.remove);
|
||||
if (candidates.length) {
|
||||
const result = await player.chooseTarget(
|
||||
"印牌:增加或减少1个合法目标(至少保留1个)",
|
||||
(card, current, target) => target.isIn()
|
||||
&& candidates.includes(target))
|
||||
.set("ai", target => {
|
||||
const effect = get.effect(target, trigger.card,
|
||||
player, player);
|
||||
return options.remove.includes(target)
|
||||
? -effect : effect;
|
||||
}).forResult();
|
||||
const target = result.targets?.[0];
|
||||
if (result.bool && target?.isIn()) {
|
||||
const valid = getSkillCardTargetOptions(
|
||||
trigger, lib, game, get);
|
||||
if (options.remove.includes(target)
|
||||
&& valid.remove.includes(target)) {
|
||||
trigger.targets.splice(
|
||||
trigger.targets.indexOf(target), 1);
|
||||
game.log(player, "移除", trigger.card,
|
||||
"的目标", target);
|
||||
} else if (options.add.includes(target)
|
||||
&& valid.add.includes(target)) {
|
||||
trigger.targets.push(target);
|
||||
game.log(player, "为", trigger.card,
|
||||
"增加目标", target);
|
||||
}
|
||||
player.line(target);
|
||||
}
|
||||
}
|
||||
applySkillCardEffects(trigger, level, game.players);
|
||||
if (level >= 3) game.log(trigger.card,
|
||||
"对全部指定目标额外结算一次");
|
||||
},
|
||||
},
|
||||
shuYing_Tianshu_BondManager: {
|
||||
mode: ["boss"],
|
||||
trigger: { global: "changeSkillsAfter" },
|
||||
@@ -1609,6 +1720,17 @@ export default function initTianshu(lib, game, ui, get, ai, _status, shuYing) {
|
||||
shuYing_Skill_Tianshu_intro5: { nobracket: true },
|
||||
};
|
||||
tianshu.translate = {
|
||||
shuYing_Tianshu_BondYinpai1: "印牌·壹",
|
||||
shuYing_Tianshu_BondYinpai1_info:
|
||||
"你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,至少保留1个目标。",
|
||||
shuYing_Tianshu_BondYinpai2: "印牌·贰",
|
||||
shuYing_Tianshu_BondYinpai2_info:
|
||||
"你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,"
|
||||
+ "至少保留1个目标,且此牌无法被响应。",
|
||||
shuYing_Tianshu_BondYinpai3: "印牌·叁",
|
||||
shuYing_Tianshu_BondYinpai3_info:
|
||||
"你使用技能视为牌或转化牌时,可以增加或减少1个合法目标,"
|
||||
+ "至少保留1个目标,且此牌无法被响应,并对全部指定目标额外结算一次。",
|
||||
shuYing_Tianshu_BondTaoyuan1: "桃园·壹",
|
||||
shuYing_Tianshu_BondTaoyuan1_info: "你通过技能回复体力时,回复值+1。",
|
||||
shuYing_Tianshu_BondTaoyuan2: "桃园·贰",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
export const skillCardStorage = "shuYing_tianshu_skillCardSkills";
|
||||
|
||||
export function getSkillCardOrigin(event, lib, get) {
|
||||
if (!["useCard", "respond"].includes(event?.name) || !event.card) {
|
||||
return null;
|
||||
}
|
||||
const virtual = get.is.virtualCard(event.card)
|
||||
&& (!event.card.isCard || !event.cards?.length);
|
||||
if (!virtual && !get.is.convertedCard(event.card)) return null;
|
||||
if (event.skill && lib.skill[event.skill]) {
|
||||
return { skill: get.sourceSkillFor(event.skill), player: event.player };
|
||||
}
|
||||
let current = event.getParent?.(1, true);
|
||||
const visited = new Set();
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
if (["useCard", "respond"].includes(current.name)
|
||||
|| ["card", "precard", "postcard"].includes(current.type)
|
||||
|| current.name?.endsWith("_cost")) return null;
|
||||
const skill = lib.skill[current.name] ? current.name : current.skill;
|
||||
if (skill && lib.skill[skill]) {
|
||||
if (current.player != event.player) return null;
|
||||
return { skill: get.sourceSkillFor(skill), player: current.player };
|
||||
}
|
||||
current = current.getParent?.(1, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSkillCardCount(player, skills, lib) {
|
||||
const recorded = player?.storage?.[skillCardStorage];
|
||||
if (!Array.isArray(recorded)) return 0;
|
||||
return new Set(skills.filter(skill => recorded.includes(skill))
|
||||
.map(skill => lib.translate[skill] || skill)).size;
|
||||
}
|
||||
|
||||
export function getSkillCardTargetOptions(event, lib, game, get) {
|
||||
const options = { add: [], remove: [] };
|
||||
const info = get.info(event.card);
|
||||
if (!info || !event.targets?.length || info.notarget || info.toself
|
||||
|| info.complexTarget || info.singleCard
|
||||
|| info.filterAddedTarget) return options;
|
||||
if (event.targets.length > 1) {
|
||||
options.remove = event.targets.filter(target => target.isIn());
|
||||
}
|
||||
if (info.allowMultiple !== false) {
|
||||
options.add = game.players.filter(target => target.isIn()
|
||||
&& !event.targets.includes(target)
|
||||
&& lib.filter.targetEnabled2(event.card, event.player, target)
|
||||
&& lib.filter.targetInRange(event.card, event.player, target));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function applySkillCardEffects(event, level, players) {
|
||||
if (level >= 2) {
|
||||
if (!Array.isArray(event.directHit)) event.directHit = [];
|
||||
for (const player of players) {
|
||||
if (!event.directHit.includes(player)) event.directHit.push(player);
|
||||
}
|
||||
event.nowuxie = true;
|
||||
}
|
||||
if (level >= 3) event.effectCount = (Number(event.effectCount) || 1) + 1;
|
||||
}
|
||||
@@ -8,6 +8,9 @@ export function getSkillRecoverOrigin(event, lib, get) {
|
||||
const visited = new Set();
|
||||
while (current && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
if (["card", "precard", "postcard"].includes(current.type)) {
|
||||
return null;
|
||||
}
|
||||
if (["useCard", "respond", "recover", "damage"]
|
||||
.includes(current.name) || current.name?.endsWith("_cost")) {
|
||||
return null;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"tianshu/config.js",
|
||||
"tianshu/directDamage.js",
|
||||
"tianshu/skillRecover.js",
|
||||
"tianshu/skillCard.js",
|
||||
"pve/boss/index.js",
|
||||
"pve/boss/card.js",
|
||||
"pve/boss/character.js",
|
||||
|
||||
@@ -6,6 +6,7 @@ const githubRawBase = "https://raw.githubusercontent.com/Shu-Ying/noname-shuying
|
||||
const coreTempDir = "extension/术樱包/.update_tmp";
|
||||
let fileMutationUncertain = false;
|
||||
const verifiedCacheKey = "shuYing_update_verified_files";
|
||||
const installedManifestKey = "shuYing_update_installation";
|
||||
let verifiedFiles = new Map();
|
||||
let sessionVerified = new Set();
|
||||
let verificationContext = null;
|
||||
@@ -1012,13 +1013,6 @@ async function removeManifestOldPaths(shuYing, manifest) {
|
||||
await removeManifestDirectories(shuYing, manifest);
|
||||
}
|
||||
|
||||
async function downloadCoreFiles(shuYing, manifest) {
|
||||
manifest = manifest || await getManifest();
|
||||
const files = getManifestCoreFiles(manifest);
|
||||
if (!files.length) throw new Error("校验清单缺少 core 核心文件列表");
|
||||
await updateAllFiles(shuYing, manifest, files);
|
||||
}
|
||||
|
||||
async function getChangedFiles(manifest, shuYing) {
|
||||
if (typeof game.readFile != "function") throw new Error("当前环境不支持校验本地文件");
|
||||
const files = getManifestFileKeys(manifest);
|
||||
@@ -1039,6 +1033,49 @@ async function getChangedFiles(manifest, shuYing) {
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
|
||||
function installationManifest(manifest) {
|
||||
const { version, algorithm, core, assets, files, modules, module_id,
|
||||
tag_name, source } = manifest;
|
||||
return { version, algorithm, core, assets, files, modules, module_id,
|
||||
tag_name, source, remove: [], removeDirectories: [] };
|
||||
}
|
||||
|
||||
function installedBaseline(record, version, moduleId = "") {
|
||||
if (record?.status != "installed" || !record.manifest
|
||||
|| record.manifest.version != version) return null;
|
||||
try {
|
||||
return validateManifest(record.manifest, version, moduleId);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
async function getUpdateFiles(manifest, baseline, shuYing) {
|
||||
if (!baseline) {
|
||||
await addLog(shuYing, "没有可信安装清单,执行一次完整检查");
|
||||
return getChangedFiles(manifest, shuYing);
|
||||
}
|
||||
const changed = getManifestFileKeys(manifest).filter(file => {
|
||||
const previous = baseline.files[file];
|
||||
return !previous || previous.sha256 != manifest.files[file].sha256
|
||||
|| previous.size != manifest.files[file].size;
|
||||
});
|
||||
const core = getManifestCoreFiles(manifest);
|
||||
const invalidCore = await getChangedFiles({ ...manifest,
|
||||
files: Object.fromEntries(core.map(file => [file, manifest.files[file]])),
|
||||
}, shuYing);
|
||||
await addLog(shuYing, `清单差异:${changed.length} 个文件;未变化资源不检查`);
|
||||
return uniqueManifestList([...changed, ...invalidCore]);
|
||||
}
|
||||
|
||||
async function saveMainInstallation(manifest) {
|
||||
await saveUpdaterConfig({
|
||||
[installedManifestKey]: { schema: 1, status: "installed",
|
||||
manifest: installationManifest(manifest) },
|
||||
shuYing_local_version: manifest.version,
|
||||
shuYing_online_version: manifest.version,
|
||||
});
|
||||
}
|
||||
|
||||
function makeLegacyModuleManifest(manifest, moduleId) {
|
||||
const prefix = `${moduleId}/`;
|
||||
const files = Object.fromEntries(Object.entries(manifest.files || {})
|
||||
@@ -1091,6 +1128,7 @@ async function getLegacyModuleManifest(moduleId, rootManifest) {
|
||||
|
||||
const mengsanRecordKey = "shuYing_mengsan_installation";
|
||||
const mengsanVersionKey = "shuYing_mengsan_installed_version";
|
||||
const mengsanPendingUpdateKey = "shuYing_mengsan_update_pending";
|
||||
|
||||
function getMengsanRecord() {
|
||||
const record = lib.config[mengsanRecordKey];
|
||||
@@ -1107,7 +1145,11 @@ function getMengsanRecord() {
|
||||
|
||||
async function saveMengsanRecord(record) {
|
||||
const version = record.status == "disabled" ? "disabled" : record.version || "legacy";
|
||||
await saveUpdaterConfig({ [mengsanRecordKey]: record, [mengsanVersionKey]: version });
|
||||
const values = { [mengsanRecordKey]: record, [mengsanVersionKey]: version };
|
||||
if (["installed", "disabled"].includes(record.status)) {
|
||||
values[mengsanPendingUpdateKey] = null;
|
||||
}
|
||||
await saveUpdaterConfig(values);
|
||||
}
|
||||
|
||||
async function getMengsanModuleStatus() {
|
||||
@@ -1208,7 +1250,8 @@ async function getPreviousMengsanManifest(rootManifest = null) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
async function installMengsanFiles(shuYing, manifest, files, previousManifest = null) {
|
||||
async function installMengsanFiles(shuYing, manifest, files, previousManifest = null,
|
||||
verifyAll = true) {
|
||||
assertMutationReady();
|
||||
const previousRecord = getMengsanRecord();
|
||||
const currentFiles = getManifestFileKeys(manifest);
|
||||
@@ -1224,10 +1267,14 @@ async function installMengsanFiles(shuYing, manifest, files, previousManifest =
|
||||
try {
|
||||
await updateAllFiles(shuYing, manifest, files, operationRoot);
|
||||
filesApplied = true;
|
||||
const invalid = await getChangedFiles(manifest, shuYing);
|
||||
const verification = verifyAll ? manifest : { ...manifest,
|
||||
files: Object.fromEntries(files.map(file => [file, manifest.files[file]])),
|
||||
};
|
||||
const invalid = await getChangedFiles(verification, shuYing);
|
||||
if (invalid.length) throw new Error(`梦三模块校验失败:${invalid.slice(0, 5).join("、")}`);
|
||||
await saveMengsanRecord({ status: "installed", version: manifest.version,
|
||||
source: manifest.source, files: currentFiles });
|
||||
source: manifest.source, files: currentFiles,
|
||||
manifest: installationManifest(manifest) });
|
||||
}
|
||||
catch (error) {
|
||||
let recovery = { ...pending, status: "repair", recoveryDirectory: error.recoveryDirectory || null };
|
||||
@@ -1452,12 +1499,38 @@ async function checkVersion(shuYing, options = {}) {
|
||||
const onlineVersion = await getOnlineVersion(options.channel, options.source);
|
||||
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
||||
const manifest = onlineVersion.manifest;
|
||||
const installation = lib.config[installedManifestKey];
|
||||
const moduleRecord = getMengsanRecord();
|
||||
const modulePending = !!lib.config[mengsanPendingUpdateKey]
|
||||
|| (moduleRecord && ["installing", "repair"].includes(moduleRecord.status));
|
||||
const moduleVersion = moduleRecord?.version || lib.config[mengsanVersionKey];
|
||||
const moduleEnabled = !["disabled", "uninstalling"].includes(moduleRecord?.status)
|
||||
&& moduleVersion != "disabled";
|
||||
const moduleNeedsUpdate = manifest.modules?.mengsan && moduleEnabled
|
||||
&& (modulePending || (parseVersion(moduleVersion || "")
|
||||
&& moduleVersion != manifest.version));
|
||||
if (!options.switchChannel && parseVersion(localVersion)
|
||||
&& compareVersions(onlineVersion.online_version, localVersion) < 0) {
|
||||
throw new Error("目标更新源的版本落后于本地版本,请稍后重试");
|
||||
}
|
||||
let previousManifest = null;
|
||||
if (parseVersion(localVersion) && localVersion != manifest.version) {
|
||||
if (localVersion == manifest.version
|
||||
&& installation?.status != "updating"
|
||||
&& !moduleNeedsUpdate) {
|
||||
game.saveConfig("shuYing_online_version", manifest.version);
|
||||
await addLog(shuYing, "版本一致,跳过本地资源检查;资源异常请使用查漏补缺");
|
||||
alert("本地版本为最新版");
|
||||
return manifest.version;
|
||||
}
|
||||
const message = localVersion == manifest.version
|
||||
? "有未完成的更新,是否继续同步版本?"
|
||||
: `检测到最新版本为:${manifest.version}, 本地版本为:${localVersion},是否更新?`;
|
||||
if (!options.switchChannel && !confirm(message)) return null;
|
||||
|
||||
const baseline = installation?.schema == 1
|
||||
? installedBaseline(installation, localVersion) : null;
|
||||
let previousManifest = baseline;
|
||||
if (!previousManifest && parseVersion(localVersion)
|
||||
&& localVersion != manifest.version) {
|
||||
previousManifest = await getManifestFromTag({
|
||||
name: `v${localVersion}`, version: localVersion,
|
||||
}, false, manifest.source).catch(error => {
|
||||
@@ -1469,6 +1542,9 @@ async function checkVersion(shuYing, options = {}) {
|
||||
const installedMengsan = await isMengsanInstalled(
|
||||
localVersion, previousManifest, manifest
|
||||
);
|
||||
if (installedMengsan && manifest.modules?.mengsan) {
|
||||
await saveUpdaterConfig({ [mengsanPendingUpdateKey]: manifest.version });
|
||||
}
|
||||
if (previousManifest) {
|
||||
const retainedModuleFiles = manifest.modules?.mengsan
|
||||
? getManifestFileKeys(previousManifest)
|
||||
@@ -1481,8 +1557,11 @@ async function checkVersion(shuYing, options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
const changed = await getChangedFiles(manifest, shuYing);
|
||||
const obsolete = await getObsoleteFiles(manifest);
|
||||
const updateMain = localVersion != manifest.version
|
||||
|| installation?.status == "updating";
|
||||
const changed = updateMain
|
||||
? await getUpdateFiles(manifest, baseline, shuYing) : [];
|
||||
const obsolete = updateMain ? await getObsoleteFiles(manifest) : [];
|
||||
let mengsanManifest = null;
|
||||
let mengsanChanged = [];
|
||||
let mengsanObsolete = [];
|
||||
@@ -1500,7 +1579,12 @@ async function checkVersion(shuYing, options = {}) {
|
||||
getManifestFileKeys(previousMengsan)
|
||||
);
|
||||
}
|
||||
mengsanChanged = await getChangedFiles(mengsanManifest, shuYing);
|
||||
const moduleBaseline = installedBaseline(
|
||||
moduleRecord, moduleRecord?.version, "mengsan"
|
||||
);
|
||||
mengsanChanged = await getUpdateFiles(
|
||||
mengsanManifest, moduleBaseline, shuYing
|
||||
);
|
||||
mengsanObsolete = await getObsoleteFiles(mengsanManifest);
|
||||
}
|
||||
catch (error) {
|
||||
@@ -1512,35 +1596,23 @@ async function checkVersion(shuYing, options = {}) {
|
||||
const allObsoleteCount = obsolete.length + mengsanObsolete.length;
|
||||
await addLog(shuYing, `更新源:${manifest.source};标签:${manifest.tag_name};待更新文件:${allChangedCount};待删除文件:${allObsoleteCount}`);
|
||||
|
||||
if (localVersion == onlineVersion.online_version
|
||||
&& !allChangedCount && !allObsoleteCount && !mengsanUpdateError) {
|
||||
if (mengsanManifest && getMengsanRecord()?.status != "installed") {
|
||||
await installMengsanFiles(shuYing, mengsanManifest, [], previousMengsan);
|
||||
}
|
||||
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
|
||||
alert("本地版本为最新版");
|
||||
return onlineVersion.online_version;
|
||||
if (updateMain) {
|
||||
await saveUpdaterConfig({ [installedManifestKey]: {
|
||||
schema: 1, status: "updating", manifest: baseline,
|
||||
} });
|
||||
await updateAllFiles(shuYing, manifest, changed);
|
||||
await saveMainInstallation(manifest);
|
||||
}
|
||||
|
||||
const message = localVersion == onlineVersion.online_version
|
||||
? `当前版本有 ${allChangedCount} 个文件需修复、${allObsoleteCount} 个旧文件需删除,是否继续?`
|
||||
: `检测到最新版本为:${onlineVersion.online_version}, 本地版本为:${localVersion},是否更新?`;
|
||||
if (!options.switchChannel && !confirm(message)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await updateAllFiles(shuYing, manifest, changed);
|
||||
if (mengsanManifest && !mengsanUpdateError) {
|
||||
try {
|
||||
await installMengsanFiles(shuYing, mengsanManifest, mengsanChanged, previousMengsan);
|
||||
await installMengsanFiles(shuYing, mengsanManifest,
|
||||
mengsanChanged, previousMengsan, false);
|
||||
}
|
||||
catch (error) {
|
||||
mengsanUpdateError = error;
|
||||
await addLog(shuYing, `梦三模块更新失败:${getErrorMessage(error)}`);
|
||||
}
|
||||
}
|
||||
game.saveConfig("shuYing_local_version", onlineVersion.online_version);
|
||||
game.saveConfig("shuYing_online_version", onlineVersion.online_version);
|
||||
await addLog(shuYing, "版本更新完成");
|
||||
alert(mengsanUpdateError
|
||||
? "主包更新完成;梦三模块未完成更新,可稍后重试。"
|
||||
@@ -1575,35 +1647,25 @@ async function repairMissingFiles(shuYing) {
|
||||
await addLog(shuYing, "查漏补缺开始");
|
||||
ensureProgressNode(shuYing);
|
||||
setProgress(shuYing, { title: "查漏补缺", status: "获取校验清单", current: 0, total: 1 });
|
||||
const manifest = await getManifest();
|
||||
const files = getManifestDownloadFiles(manifest);
|
||||
const localVersion = lib.config.shuYing_local_version || "0.0.0.0";
|
||||
let previousManifest = null;
|
||||
if (parseVersion(localVersion) && localVersion != manifest.version) {
|
||||
previousManifest = await getManifestFromTag({
|
||||
name: `v${localVersion}`, version: localVersion,
|
||||
}, false, manifest.source).catch(() => null);
|
||||
if (previousManifest) {
|
||||
const retainedModuleFiles = manifest.modules?.mengsan
|
||||
? getManifestFileKeys(previousManifest)
|
||||
.filter(file => file.startsWith("mengsan/"))
|
||||
: [];
|
||||
mergeRemovedFiles(
|
||||
manifest,
|
||||
getManifestFileKeys(previousManifest),
|
||||
retainedModuleFiles
|
||||
);
|
||||
}
|
||||
if (!parseVersion(localVersion)) {
|
||||
throw new Error("无法确认本地版本,请先执行版本检测完成安装");
|
||||
}
|
||||
const manifest = await getManifestFromTag({
|
||||
name: `v${localVersion}`, version: localVersion,
|
||||
}, false);
|
||||
const files = getManifestDownloadFiles(manifest);
|
||||
const installedMengsan = await isMengsanInstalled(
|
||||
localVersion, previousManifest, manifest
|
||||
localVersion, null, manifest
|
||||
);
|
||||
let mengsanManifest = null;
|
||||
let previousMengsan = null;
|
||||
if (installedMengsan && manifest.modules?.mengsan) {
|
||||
mengsanManifest = await getModuleManifest({
|
||||
name: manifest.tag_name, version: manifest.version,
|
||||
}, manifest, "mengsan");
|
||||
if (installedMengsan) {
|
||||
const record = getMengsanRecord();
|
||||
const moduleVersion = record?.version || lib.config[mengsanVersionKey];
|
||||
mengsanManifest = await getInstalledModuleManifest("mengsan",
|
||||
parseVersion(moduleVersion || "") ? moduleVersion : localVersion,
|
||||
manifest.source);
|
||||
previousMengsan = await getPreviousMengsanManifest(manifest);
|
||||
if (previousMengsan) {
|
||||
mergeRemovedFiles(
|
||||
@@ -1619,7 +1681,11 @@ async function repairMissingFiles(shuYing) {
|
||||
|
||||
await createFolders("extension/术樱包/", files, shuYing);
|
||||
|
||||
await saveUpdaterConfig({ [installedManifestKey]: {
|
||||
schema: 1, status: "updating", manifest: null,
|
||||
} });
|
||||
await downloadList(shuYing, files, manifest);
|
||||
await saveMainInstallation(manifest);
|
||||
if (mengsanManifest) {
|
||||
const moduleChanged = await getChangedFiles(mengsanManifest, shuYing);
|
||||
await installMengsanFiles(shuYing, mengsanManifest, moduleChanged, previousMengsan);
|
||||
@@ -1719,45 +1785,12 @@ async function manageMengsanModule(shuYing) {
|
||||
}
|
||||
}
|
||||
|
||||
async function repairCoreFiles(shuYing) {
|
||||
try {
|
||||
beginUpdate(shuYing, true);
|
||||
ensureProgressNode(shuYing);
|
||||
await addLog(shuYing, "核心文件修复开始");
|
||||
const manifest = await getManifest();
|
||||
const files = getManifestCoreFiles(manifest);
|
||||
if (!files.length) throw new Error("校验清单缺少核心文件");
|
||||
const coreManifest = { ...manifest, files: Object.fromEntries(files.map(file => [file, manifest.files[file]])) };
|
||||
const changed = await getChangedFiles(coreManifest, shuYing);
|
||||
const message = changed.length ? `有 ${changed.length} 个核心文件需修复,是否重新下载核心文件?`
|
||||
: "核心文件校验通过,是否仍重新下载核心文件?";
|
||||
if (!confirm(message)) return;
|
||||
await downloadCoreFiles(shuYing, manifest);
|
||||
await addLog(shuYing, "核心文件修复完成");
|
||||
alert("下载完成,重启生效");
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
recordUpdateError(shuYing, "core.repair.failed", error);
|
||||
await addLog(shuYing, `核心文件修复失败:${getErrorMessage(error)}`);
|
||||
alert("核心文件下载或校验失败,请检查服务器文件与 manifest.json 是否一致。");
|
||||
}
|
||||
finally {
|
||||
try { await finishUpdate(shuYing); }
|
||||
finally {
|
||||
removeProgressNode(shuYing);
|
||||
setBusy(shuYing, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
getOnlineVersion,
|
||||
getUpdateChannel,
|
||||
getUpdateSource,
|
||||
checkVersion,
|
||||
repairMissingFiles,
|
||||
repairCoreFiles,
|
||||
manageMengsanModule,
|
||||
getMengsanModuleStatus,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user