Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8848f6524c | ||
|
|
d5715c9f5a | ||
|
|
4036041819 | ||
|
|
b9664de9bd | ||
|
|
59e62d22c9 | ||
|
|
727f469e93 | ||
|
|
fe3ebcc0e8 | ||
|
|
2a91f43675 | ||
|
|
dce49106a0 | ||
|
|
a8942c158e | ||
|
|
b9ebbfac7e |
@@ -1,12 +1,12 @@
|
||||
name: Release extension
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "**"
|
||||
paths:
|
||||
- "extension.js"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "版本号,例如 2.0.1.2 或 2.0.1.2-rc.1"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -17,23 +17,33 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: pwsh
|
||||
env:
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Read current version and detect change
|
||||
id: version
|
||||
run: |
|
||||
./tools/resolve_release_version.ps1 `
|
||||
-Root $env:GITHUB_WORKSPACE `
|
||||
-EventName $env:GITHUB_EVENT_NAME `
|
||||
-PreviousRef $env:BEFORE_SHA
|
||||
|
||||
- name: Generate manifest and create tag
|
||||
if: steps.version.outputs.should_release == 'true'
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$version = $env:RELEASE_VERSION
|
||||
@@ -118,6 +128,7 @@ jobs:
|
||||
Write-Host "已创建包含清单的标签:$tag"
|
||||
|
||||
- name: Build ZIP from tag
|
||||
if: steps.version.outputs.should_release == 'true'
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$version = $env:RELEASE_VERSION
|
||||
@@ -179,6 +190,7 @@ jobs:
|
||||
Write-Host "安装包:$package"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: steps.version.outputs.should_release == 'true'
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
if (!$env:GITHUB_TOKEN) {
|
||||
|
||||
@@ -15,11 +15,10 @@ 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
|
||||
!/mengsan/tools/test-hud-status.cjs
|
||||
!/mengsan/tools/test-battle-hud.cjs
|
||||
!/mengsan/tools/import-status-icons.cjs
|
||||
|
||||
/.update_tmp/
|
||||
/.planning/
|
||||
@@ -50,3 +49,5 @@ Desktop.ini
|
||||
/.comet/*
|
||||
!/.comet/config.yaml
|
||||
# <<< Comet managed project state <<<
|
||||
# Added by code-review-graph
|
||||
.code-review-graph/
|
||||
|
||||
@@ -17,3 +17,49 @@ In this repository, before starting work that may need code changes or investiga
|
||||
- If configuration or state is invalid and `nextCommand` is absent, stop and report the reason; do not guess another workflow.
|
||||
- Never attach unrelated work merely because an active change exists. The Native entry inspects uncommitted work; the probe does not attribute it automatically.
|
||||
</comet-ambient-resume>
|
||||
|
||||
<!-- code-review-graph MCP tools -->
|
||||
## MCP Tools: code-review-graph
|
||||
|
||||
**This project has a knowledge graph. Start with the code-review-graph
|
||||
MCP tools to narrow scope, then read the source.** The graph is cheaper than scanning files and
|
||||
gives you structural context (callers, dependents, test coverage) that file search cannot.
|
||||
|
||||
### When to use graph tools FIRST
|
||||
|
||||
- **Exploring code**: `semantic_search_nodes_tool` or `query_graph_tool` instead of Grep
|
||||
- **Understanding impact**: `get_impact_radius_tool` instead of manually tracing imports
|
||||
- **Code review**: `detect_changes_tool` + `get_review_context_tool` instead of reading entire files
|
||||
- **Finding relationships**: `query_graph_tool` with callers_of/callees_of/imports_of/tests_for
|
||||
- **Architecture questions**: `get_architecture_overview_tool` + `list_communities_tool`
|
||||
|
||||
### Verify in the source
|
||||
|
||||
- Narrow scope with the graph, then read the source. Do not change code from graph output alone.
|
||||
- For any non-trivial change, read the implementation and the relevant tests before concluding.
|
||||
- Verify the exact source when touching behavior, database logic, migrations, retries, fallbacks,
|
||||
recovery, or compatibility code.
|
||||
- When the graph and the source disagree, the source wins. The graph may be stale or may not
|
||||
model that relationship.
|
||||
- An empty graph result can mean "not indexed" or "not statically visible", not "does not exist".
|
||||
|
||||
### Key Tools
|
||||
|
||||
| Tool | Use when |
|
||||
| ------ | ---------- |
|
||||
| `detect_changes_tool` | Reviewing code changes — gives risk-scored analysis |
|
||||
| `get_review_context_tool` | Need source snippets for review — token-efficient |
|
||||
| `get_impact_radius_tool` | Understanding blast radius of a change |
|
||||
| `get_affected_flows_tool` | Finding which execution paths are impacted |
|
||||
| `query_graph_tool` | Tracing callers, callees, imports, tests, dependencies |
|
||||
| `semantic_search_nodes_tool` | Finding functions/classes by name or keyword |
|
||||
| `get_architecture_overview_tool` | Understanding high-level codebase structure |
|
||||
| `refactor_tool` | Planning renames, finding dead code |
|
||||
|
||||
### Workflow
|
||||
|
||||
1. The graph auto-updates on file changes (via hooks).
|
||||
2. Use `detect_changes_tool` for code review.
|
||||
3. Use `get_affected_flows_tool` to understand impact.
|
||||
4. Use `query_graph_tool` pattern="tests_for" to check coverage.
|
||||
<!-- /code-review-graph MCP tools -->
|
||||
|
||||
@@ -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
|
||||
@@ -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均保留。狼顾、妄尊、夺玺、引雷及所有本轮改动的静态/动态文本和选择提示与最终规则一致。
|
||||
@@ -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.8";
|
||||
|
||||
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)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 梦三卡牌立绘规范
|
||||
|
||||
2026-10-03:铁甲战士94张已全部接入:新增67、沿用27、暂缓0;另有衍生巨石。68张新增完整5:7水墨立绘使用 `mengsan_ic_*.png`。刘备专属牌组与空无色牌组已分目录和牌组类,详情见 [制作清单](../../../../../sts2-card-capture-20261003/generation/铁甲战士制作清单.md)、[全部立绘预览](../../../../../sts2-card-capture-20261003/generation/铁甲战士立绘预览.md) 及 [牌组结构](../../cards/packs/README.md)。尚未进行实机验收。
|
||||
2026-10-03:刘备94张已全部接入:新增67、沿用27、暂缓0;另有衍生巨石。68张新增完整5:7水墨立绘使用 `mengsan_ic_*.png`。刘备专属牌组与空无色牌组已分目录和牌组类,详情见 [制作清单](../../../../../sts2-card-capture-20261003/generation/刘备制作清单.md)、[全部立绘预览](../../../../../sts2-card-capture-20261003/generation/刘备立绘预览.md) 及 [牌组结构](../../cards/packs/README.md)。尚未进行实机验收。
|
||||
|
||||
用户于 2026-10-02 指定以截图 `C:\Users\Diuse\Downloads\Ai\49.png` 中“防御”为同系列基准。
|
||||
|
||||
|
||||
@@ -3284,7 +3284,7 @@
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"美味饼干使用铁甲战士版本;四个其他角色版本仍保留在来源图集中。",
|
||||
"美味饼干使用刘备版本;四个其他角色版本仍保留在来源图集中。",
|
||||
"遗忘之魂映射 lost_soul.png;布质果实映射 looming_fruit_2.png。",
|
||||
"图标是用户提供的原游戏拆包图片,逐字节复制,未重绘。"
|
||||
]
|
||||
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,427 @@
|
||||
{
|
||||
"sourceVersion": "v0.111.0",
|
||||
"source": "images/atlases/power_atlas.sprites",
|
||||
"entries": [
|
||||
{
|
||||
"name": "strength_power.png",
|
||||
"bytes": 3435,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "6584c67190bfd47255c0f333bfb7f2ab52720209d94ce9324a30b0a8e638d7ff"
|
||||
},
|
||||
{
|
||||
"name": "ringing_power.png",
|
||||
"bytes": 5038,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "c4a3d09f976788bca91d3a5011fc0f05c3f981eaa3482a33c31a0a661705f60c"
|
||||
},
|
||||
{
|
||||
"name": "slow_power.png",
|
||||
"bytes": 5762,
|
||||
"width": 64,
|
||||
"height": 63,
|
||||
"sha256": "b39d7af28b0fdcd773a44577a6635c8b23dc03bff6280de841dc831fe6c601b0"
|
||||
},
|
||||
{
|
||||
"name": "the_bomb_power.png",
|
||||
"bytes": 4421,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "862f05ed56d4d0ebd60afd777162d23fb4744991f6cace9e43640bee29f5db58"
|
||||
},
|
||||
{
|
||||
"name": "vigor_power.png",
|
||||
"bytes": 5912,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "551b7b93d139f30cf190887ea3eef197aa693a7a78e67aef48ef2b582b1532b3"
|
||||
},
|
||||
{
|
||||
"name": "draw_cards_next_turn_power.png",
|
||||
"bytes": 3178,
|
||||
"width": 58,
|
||||
"height": 64,
|
||||
"sha256": "c0a8c2781aff2e5c6bf73a2dd2938ef820a5fde651757954f5219113cf88ea22"
|
||||
},
|
||||
{
|
||||
"name": "energy_next_turn_power.png",
|
||||
"bytes": 2896,
|
||||
"width": 43,
|
||||
"height": 64,
|
||||
"sha256": "511314b05d5c223613b6d654816f7feb5607e7c40b0487207c5b1a75f5002f67"
|
||||
},
|
||||
{
|
||||
"name": "block_next_turn_power.png",
|
||||
"bytes": 4275,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "fd5c75f11f5719f9ffb52ceda3640cbc684ec178e783a3ceb2774548df1615f8"
|
||||
},
|
||||
{
|
||||
"name": "vulnerable_power.png",
|
||||
"bytes": 3725,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "1b9a9336dd7828cb1fb61b119ab765d3c713ce4e0e98bdbee7211724fe5c78b6"
|
||||
},
|
||||
{
|
||||
"name": "weak_power.png",
|
||||
"bytes": 4738,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "f8f4277c9fd848600a503b8d377bf9e83b4eaa06fa27f8f82ebefcfbd9714763"
|
||||
},
|
||||
{
|
||||
"name": "frail_power.png",
|
||||
"bytes": 3945,
|
||||
"width": 63,
|
||||
"height": 64,
|
||||
"sha256": "d772bf035190a650a9be735282c77ee9ab2276a775a9e7079710c8b09c4a4619"
|
||||
},
|
||||
{
|
||||
"name": "artifact_power.png",
|
||||
"bytes": 5717,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "2346d2425fd40cc5f8532df0f97afa9bbb782fa649a09bcb014fe6932e845f90"
|
||||
},
|
||||
{
|
||||
"name": "constrict_power.png",
|
||||
"bytes": 4543,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "5f29c6b3f8955e275d98f9ed47a893c1214a7bfb820ce1c696c9f16fddcbd8b4"
|
||||
},
|
||||
{
|
||||
"name": "tangled_power.png",
|
||||
"bytes": 5754,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "3d4d4686876ed0265f26696ab2d0ebde8366b0537994bf223bc388a0f104ec5e"
|
||||
},
|
||||
{
|
||||
"name": "shrink_power.png",
|
||||
"bytes": 4258,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "b19a0d045135bcc6cfa00f2fb11be78dba68f16ecc12f2ca8474caa9327c05dd"
|
||||
},
|
||||
{
|
||||
"name": "plow_power.png",
|
||||
"bytes": 5865,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "dc300ee907b06d300bcd01f2c2784cc06c761796206a090e1a94f0cecfa95a31"
|
||||
},
|
||||
{
|
||||
"name": "slippery_power.png",
|
||||
"bytes": 4722,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "06b4ac80cbf1f70c625be827047be79302f72c018cf8368bfb313a27744e568b"
|
||||
},
|
||||
{
|
||||
"name": "territorial_power.png",
|
||||
"bytes": 3829,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "cf1b1d401a6fd45dbb1f3eda64049edf8cdf7e91a55f3aaf7a10f138d54699dd"
|
||||
},
|
||||
{
|
||||
"name": "infested_power.png",
|
||||
"bytes": 3554,
|
||||
"width": 64,
|
||||
"height": 50,
|
||||
"sha256": "d6cd52278e0444a8f65fdc7d3c19b7ff9dc7067e2c1d21509f891238bae234f7"
|
||||
},
|
||||
{
|
||||
"name": "demon_form_power.png",
|
||||
"bytes": 4728,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "2e183f449fc07c30356d181a1b1a5814aad2ef077598642a35899f4d8cdc3bc3"
|
||||
},
|
||||
{
|
||||
"name": "demesne_power.png",
|
||||
"bytes": 5817,
|
||||
"width": 64,
|
||||
"height": 63,
|
||||
"sha256": "554fff2e62aaeaee7b1c9cc1c8283257139ce901a5191c39d143b757cd865e96"
|
||||
},
|
||||
{
|
||||
"name": "crimson_mantle_power.png",
|
||||
"bytes": 4706,
|
||||
"width": 64,
|
||||
"height": 61,
|
||||
"sha256": "7fdeb8f49fb6bfb3ef5f4ff52ec8bcad18bf8fd1e0357d9378ac657124cb8e23"
|
||||
},
|
||||
{
|
||||
"name": "inferno_power.png",
|
||||
"bytes": 5996,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "3775622f4ad2419ed897bb90e56d793fb383c1810bae514d3e9c17eaff53fe69"
|
||||
},
|
||||
{
|
||||
"name": "rupture_power.png",
|
||||
"bytes": 3897,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "101e3b473ec6d6b565aa94ed5946840f6c013f28e62f3f0961106c2c8cd54d95"
|
||||
},
|
||||
{
|
||||
"name": "vicious_power.png",
|
||||
"bytes": 3247,
|
||||
"width": 64,
|
||||
"height": 62,
|
||||
"sha256": "4b90144a9254bb155b1cce8180d91d919af99566b8841e2135eb86880ff0765a"
|
||||
},
|
||||
{
|
||||
"name": "rage_power.png",
|
||||
"bytes": 4937,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "a53721daa6996403e61e871aca8152ae50372c3255e509f1a6d4d99bbfd01d02"
|
||||
},
|
||||
{
|
||||
"name": "flame_barrier_power.png",
|
||||
"bytes": 4786,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "48a7543a8f87c8b360a3a23a67df23b51b4a54eb6f7747cc27c9f4a230610cdf"
|
||||
},
|
||||
{
|
||||
"name": "colossus_power.png",
|
||||
"bytes": 3875,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "cfe2be38931e40856d5388b6e3b8c86a7ee2de6688ab69294f348d383e1dfb7d"
|
||||
},
|
||||
{
|
||||
"name": "free_attack_power.png",
|
||||
"bytes": 4223,
|
||||
"width": 61,
|
||||
"height": 64,
|
||||
"sha256": "5bc861f0693c0b73e95be4a2faebd8d81441e1011004f7a0ff71ced6f913573b"
|
||||
},
|
||||
{
|
||||
"name": "no_draw_power.png",
|
||||
"bytes": 4335,
|
||||
"width": 58,
|
||||
"height": 64,
|
||||
"sha256": "7f8ac6cae6cc5611d7e8217619de9cc87dcef245e19bc70107bc299da790bed0"
|
||||
},
|
||||
{
|
||||
"name": "no_energy_gain_power.png",
|
||||
"bytes": 6746,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "c31490f89b758bd63564581218ede73d89438d833ece8d8da5a8f12f773a345d"
|
||||
},
|
||||
{
|
||||
"name": "one_two_punch_power.png",
|
||||
"bytes": 3986,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "458f05559a4efcd79bcbe0fb7e1d9aa4ae47d3a0c533f11e1a72ed3e67aec78b"
|
||||
},
|
||||
{
|
||||
"name": "plating_power.png",
|
||||
"bytes": 5036,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "10a27dd11231338120274c8b132ec82187b2cc00f81d0f7fa94e3637f4506b67"
|
||||
},
|
||||
{
|
||||
"name": "feel_no_pain_power.png",
|
||||
"bytes": 4244,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "92359b3a53e10600e864a502291be3b50cf00c1792dbf5203e8eaac09fab126e"
|
||||
},
|
||||
{
|
||||
"name": "dark_embrace_power.png",
|
||||
"bytes": 5545,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "f60e5524cf10b4da15f1b788a58e65e2765aa928988100d6fcb6012829520d99"
|
||||
},
|
||||
{
|
||||
"name": "juggling_power.png",
|
||||
"bytes": 4512,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "583f560f68a3b9df4ef60376b919ff51d85d862076f0ce09848644f3171d51af"
|
||||
},
|
||||
{
|
||||
"name": "stampede_power.png",
|
||||
"bytes": 3357,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "1aea977a121c451ef134581789f629dc61caba87b771e7bd684345baae573d03"
|
||||
},
|
||||
{
|
||||
"name": "aggression_power.png",
|
||||
"bytes": 4524,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "9ec47f0a5eb259c1cc131b82e6bb9e22343df7f64c7375db7580eb326b6a97f3"
|
||||
},
|
||||
{
|
||||
"name": "tank_power.png",
|
||||
"bytes": 3191,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "f8cfbc1bbf8832f4e83ffdf4172bfc37564c85d36a4a5c14d18c1ab60bd1b76e"
|
||||
},
|
||||
{
|
||||
"name": "hellraiser_power.png",
|
||||
"bytes": 4296,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "38ca8fc4c727fb936d057a78bfb9461a39b0266dd794d062e9d278505821efc6"
|
||||
},
|
||||
{
|
||||
"name": "unmovable_power.png",
|
||||
"bytes": 4784,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "f99e4f21fcfd6e57089d47e4bc4ed1cf1a813a759f76487c38852d66dc472a19"
|
||||
},
|
||||
{
|
||||
"name": "juggernaut_power.png",
|
||||
"bytes": 5096,
|
||||
"width": 57,
|
||||
"height": 64,
|
||||
"sha256": "c0c9382989b31f246678d1d33ecb3f17f3b75a97aa9a752dd1f6551733d76112"
|
||||
},
|
||||
{
|
||||
"name": "barricade_power.png",
|
||||
"bytes": 2828,
|
||||
"width": 64,
|
||||
"height": 60,
|
||||
"sha256": "1467ded14bfef8cdc15152e62a100ef7d3be85173ac5eea2f6960d4815d5c64c"
|
||||
},
|
||||
{
|
||||
"name": "corruption_power.png",
|
||||
"bytes": 4822,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "8228d730978170f60d9c429136a395b3de59b5c98726912812f708ec803836eb"
|
||||
},
|
||||
{
|
||||
"name": "dexterity_power.png",
|
||||
"bytes": 3073,
|
||||
"width": 63,
|
||||
"height": 64,
|
||||
"sha256": "6609f4d04f4613ba0001b6254a793ddd2c25a60e66a320ff31ef6724ef15f20e"
|
||||
},
|
||||
{
|
||||
"name": "no_block_power.png",
|
||||
"bytes": 4592,
|
||||
"width": 63,
|
||||
"height": 64,
|
||||
"sha256": "469f70ce5a86ea24751ef75461d20ec127014e16495ee23a86ca0255b4246d4b"
|
||||
},
|
||||
{
|
||||
"name": "intangible_power.png",
|
||||
"bytes": 4398,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "0368e806c412ca290796c58e1e1a96e6d84bd5dda67f39249433e688006eaf1f"
|
||||
},
|
||||
{
|
||||
"name": "the_gambit_power.png",
|
||||
"bytes": 3051,
|
||||
"width": 49,
|
||||
"height": 64,
|
||||
"sha256": "204284c28d48c60a8c73f7356bb3d5aa0fc8e4b44491cb1aec19717bb4f04784"
|
||||
},
|
||||
{
|
||||
"name": "retain_hand_power.png",
|
||||
"bytes": 4225,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "a8ee3da12b0774ffc9bba33d6da7695afdfc9b7904d31b3229df077489541784"
|
||||
},
|
||||
{
|
||||
"name": "panache_power.png",
|
||||
"bytes": 3489,
|
||||
"width": 55,
|
||||
"height": 64,
|
||||
"sha256": "61dd7bda14317085c4f6cb4ccecebc759b5468c5494d16fb1a2d78c5f3fd59e0"
|
||||
},
|
||||
{
|
||||
"name": "fasten_power.png",
|
||||
"bytes": 4680,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "13518361d51775876bf4cde87e88cc4a9e63a5a1d14ba49388eb12d57404bbe7"
|
||||
},
|
||||
{
|
||||
"name": "prep_time_power.png",
|
||||
"bytes": 6445,
|
||||
"width": 64,
|
||||
"height": 63,
|
||||
"sha256": "ce1698909dcb2e91bd67714c11f7c467ae083326b7f9f239751a4c202d50bf69"
|
||||
},
|
||||
{
|
||||
"name": "automation_power.png",
|
||||
"bytes": 4452,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "c7b5b35579dc1b2ff209e4be54a1982381b1096c640a3c21bb5c9e6aeb86a08f"
|
||||
},
|
||||
{
|
||||
"name": "curious_power.png",
|
||||
"bytes": 4106,
|
||||
"width": 57,
|
||||
"height": 64,
|
||||
"sha256": "77cb0e309fc148e2b2bb723fa5a227d4dc3c2503e22421b9d0e457fb3a85c7d7"
|
||||
},
|
||||
{
|
||||
"name": "rolling_boulder_power.png",
|
||||
"bytes": 4422,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "53c820c23f09cd07d2b0e3bd00493bdaef81fbd09d05bf2d15485c3c1ef7e4a0"
|
||||
},
|
||||
{
|
||||
"name": "nostalgia_power.png",
|
||||
"bytes": 3856,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "836674ba8a9e8d0ea2263e90944acdf7078dc1f6c835771650bfcf84825d84a4"
|
||||
},
|
||||
{
|
||||
"name": "entropy_power.png",
|
||||
"bytes": 4849,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "5042bd71bd07fd605cba2c39677dd21df704439ed5d0ad9610f1102d8ed782ad"
|
||||
},
|
||||
{
|
||||
"name": "beacon_of_hope_power.png",
|
||||
"bytes": 5248,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "0d33f9098425c3d740e699926809c0c7c9121cdd05e52508284a1e34d7b7bd8d"
|
||||
},
|
||||
{
|
||||
"name": "mayhem_power.png",
|
||||
"bytes": 5287,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "354b3fe4b63c064cc96302d46b27829f6d51fd37e703aec15d53ed53a14e2d51"
|
||||
},
|
||||
{
|
||||
"name": "calamity_power.png",
|
||||
"bytes": 5331,
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"sha256": "8d510b86daa72a8066729bc5d0a3943acf5c77903be69525f325b65607c7cdd7"
|
||||
}
|
||||
],
|
||||
"missing": []
|
||||
}
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
@@ -0,0 +1,26 @@
|
||||
// Display-only readers. Entries belong to a battle session, never to the run/save.
|
||||
const sources = new WeakMap();
|
||||
|
||||
export function bindCombatStatusSource(player, session, id, read) {
|
||||
if (!player || !session?.active || typeof read !== 'function') return;
|
||||
let group = sources.get(player);
|
||||
if (!group) sources.set(player, group = new Map());
|
||||
const entry = { session, read };
|
||||
group.set(id, entry);
|
||||
session.ownResource(entry, () => {
|
||||
if (group.get(id) !== entry) return;
|
||||
group.delete(id);
|
||||
if (!group.size && sources.get(player) === group) sources.delete(player);
|
||||
});
|
||||
}
|
||||
|
||||
export function readCombatStatusSources(player, session) {
|
||||
if (!session?.active) return [];
|
||||
const result = [];
|
||||
for (const [id, entry] of sources.get(player) || []) {
|
||||
if (entry.session !== session) continue;
|
||||
const state = entry.read();
|
||||
if (state) result.push({ id, state });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
# 角色牌组
|
||||
|
||||
当前铁甲战士主表 94 张:新增 67 张、沿用 27 张;另有衍生牌【巨石】。专属牌归刘备,基础打击/痛击分别沿用杀/重杀。所有正常获得入口沿用 `canAcquireCard` 的角色检查。杀、重杀保留原有通用牌归属,并纳入刘备的随机生成牌池。旧存档的独立打击/痛击转换为杀/重杀,保留实例ID、强化和词缀;待结算候选保留原ID以避免冲突。
|
||||
当前刘备主表 94 张:新增 67 张、沿用 27 张;另有衍生牌【巨石】。专属牌归刘备,基础打击/痛击分别沿用杀/重杀。所有正常获得入口沿用 `canAcquireCard` 的角色检查。杀、重杀保留原有通用牌归属,并纳入刘备的随机生成牌池。旧存档的独立打击/痛击转换为杀/重杀,保留实例ID、强化和词缀;待结算候选保留原ID以避免冲突。
|
||||
|
||||
| 目录/文件 | 职责 |
|
||||
| --- | --- |
|
||||
|
||||
@@ -36,6 +36,8 @@ const originalOf = event => {
|
||||
return use?.mengsanIcRepeatOriginal_shuying || use?.cards?.find(c=>c.name===use.card?.name) || event.card;
|
||||
};
|
||||
|
||||
import { bindCombatStatusSource } from "../../../battle/status-sources.js";
|
||||
|
||||
export function createAdvancedIronclad({game,get,lib,status,getBattle,battleEnergy,random}) {
|
||||
const states=new WeakMap(), uses=new WeakMap(), hits=new WeakMap(), growth=new WeakMap();
|
||||
const seenUses=new WeakSet(), seenBlocks=new WeakSet(), seenDraws=new WeakSet();
|
||||
@@ -78,6 +80,7 @@ export function createAdvancedIronclad({game,get,lib,status,getBattle,battleEner
|
||||
s={player,token,powers:Object.create(null),turn:Object.create(null),queue:[],flushing:false,
|
||||
autoQueue:[],autoBusy:false,freeCards:new WeakSet(),negativeStrength:0,autoCount:0};
|
||||
states.set(player,s); player.addSkill(MARK);
|
||||
bindCombatStatusSource(player,token.session,"ironclad-advanced",()=>stateOf(player));
|
||||
token.session.ownResource(s,()=>{if(states.get(player)===s){states.delete(player);player.removeSkill(MARK);s.queue.length=0;s.autoQueue.length=0;}});
|
||||
bindIroncladHooks(player,token.session,{
|
||||
valid:()=>valid(player,token),
|
||||
@@ -444,7 +447,7 @@ export function createAdvancedIronclad({game,get,lib,status,getBattle,battleEner
|
||||
if(r.attackDiscount)parts.push("本回合每打出一张攻击牌,费用减少1。");
|
||||
if(r.draw)parts.push(`抽${r.draw}张牌。`);
|
||||
if(r.stopDraw)parts.push("本回合不能再抽牌。");
|
||||
if(r.generateAttack)parts.push("随机生成一张铁甲战士攻击牌至手牌,本回合费用为0。");
|
||||
if(r.generateAttack)parts.push("随机生成一张刘备攻击牌至手牌,本回合费用为0。");
|
||||
if(r.exhaustEnergy)parts.push(`被消耗时获得${r.exhaustEnergy}费用。`);
|
||||
if(r.energyPerAttack)parts.push("每张攻击手牌获得1费用。");
|
||||
if(r.stopEnergy)parts.push("本回合不能再获得费用。");
|
||||
@@ -461,7 +464,7 @@ export function createAdvancedIronclad({game,get,lib,status,getBattle,battleEner
|
||||
if(r.topPlays)parts.push(`免费打出抽牌堆顶的X${r.xBonus?"+1":""}张牌。`);
|
||||
if(r.transformAttacks)parts.push(`所有攻击手牌变为巨石${r.boulderUpgrade?"+":""},仅本场战斗。`);
|
||||
if(r.doubleTap)parts.push(`本回合接下来的${r.doubleTap}张攻击牌额外打出一次。`);
|
||||
if(r.stoke)parts.push(`消耗所有手牌,每张随机生成一张${r.generatedUpgrade?"已强化":""}铁甲战士或无色牌至手牌。`);
|
||||
if(r.stoke)parts.push(`消耗所有手牌,每张随机生成一张${r.generatedUpgrade?"已强化":""}刘备或无色牌至手牌。`);
|
||||
if(r.retrieveAttack)parts.push("自身回合开始,从弃牌堆随机获得一张攻击牌并强化。");
|
||||
if(r.bodyguard)parts.push("你承受敌人双倍伤害,敌人对其他友方的伤害减半。");
|
||||
if(r.autoStrike)parts.push("抽到【杀】或名称含“打击”的攻击牌时,免费向随机敌人打出。");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// 2026-10-03 灰机Wiki铁甲战士详情页主表;所有值均为当前状态。
|
||||
// 2026-10-03 灰机Wiki卡牌详情页主表;本作品中归刘备专属牌组。
|
||||
// 纯数据模块可被费用、升级、注册表及运行时共同导入,不依赖引擎。
|
||||
import { ironcladAdvancedEntries } from "./advanced-data.js";
|
||||
import { liubeiCardTraits } from "./traits.js";
|
||||
|
||||
@@ -4,6 +4,7 @@ import { canPayCard, cardCost, isActiveCardUse } from "../../../battle/combat-ru
|
||||
import { isAttackCard } from "../../../monsters/vine-tangled.js";
|
||||
import { applyMengsanDebuff } from "../../../monsters/artifact-status.js";
|
||||
import { createAdvancedIronclad } from "./advanced.js";
|
||||
import { bindCombatStatusSource } from "../../../battle/status-sources.js";
|
||||
|
||||
const POWER_SKILL = "mengsan_ic_powers_shuying";
|
||||
const belongsTo = (event, root) => {
|
||||
@@ -42,12 +43,13 @@ export function createIroncladRuntime({ game, get, lib, status, getBattle, handE
|
||||
player.removeSkill(POWER_SKILL);
|
||||
});
|
||||
states.set(player, s);
|
||||
bindCombatStatusSource(player, token.session, "ironclad", () => stateOf(player));
|
||||
player.addSkill(POWER_SKILL);
|
||||
return s;
|
||||
};
|
||||
const add = (object, key, n) => {
|
||||
const next = (object[key] || 0) + n;
|
||||
if (!Number.isFinite(next) || next < 0 || next > Number.MAX_SAFE_INTEGER) throw new RangeError("铁甲战士效果数值越界");
|
||||
if (!Number.isFinite(next) || next < 0 || next > Number.MAX_SAFE_INTEGER) throw new RangeError("刘备效果数值越界");
|
||||
object[key] = next;
|
||||
};
|
||||
const strength = (player, n, token) => {
|
||||
@@ -77,7 +79,7 @@ export function createIroncladRuntime({ game, get, lib, status, getBattle, handE
|
||||
};
|
||||
const exhaustHands = async (player, event, token, filter = () => true, afterEach = null) => {
|
||||
const pile = pileOf(player, token);
|
||||
if (typeof pile?.exhaustFromHand !== "function") throw new Error("铁甲战士缺少个人消耗接口");
|
||||
if (typeof pile?.exhaustFromHand !== "function") throw new Error("刘备缺少个人消耗接口");
|
||||
const playing = new Set(event.cards || []);
|
||||
const cards = player.getCards("h").filter(c => !playing.has(c) && filter(c));
|
||||
let count = 0;
|
||||
@@ -191,7 +193,7 @@ export function createIroncladRuntime({ game, get, lib, status, getBattle, handE
|
||||
if (r.hitsPerExhaust) hits = await exhaustHands(player, event, token);
|
||||
else if (r.vulnerableHits && target.storage?.mengsanVulnerable_shuying > 0) hits = r.vulnerableHits;
|
||||
const amount = (r.damage || 0) + (r.perExhaust ? r.perExhaust * exhaustCount(player, token) : 0);
|
||||
if (!positive(amount) || !positive(hits)) throw new RangeError("铁甲战士攻击数值越界");
|
||||
if (!positive(amount) || !positive(hits)) throw new RangeError("刘备攻击数值越界");
|
||||
if (amount > 0) for (let i = 0; i < hits && valid(player, token); i++) {
|
||||
const targets = r.all ? [...token.battle.players].filter(p => p !== player && p.isAlive() && player.isEnemyOf(p)) : [target];
|
||||
for (const t of targets) {
|
||||
@@ -260,7 +262,7 @@ export function createIroncladRuntime({ game, get, lib, status, getBattle, handE
|
||||
mark: true, marktext: "战",
|
||||
intro: { content(storage, player) {
|
||||
const s = stateOf(player);
|
||||
if (!s) return "本场战斗的铁甲战士效果";
|
||||
if (!s) return "本场战斗的刘备效果";
|
||||
const names = { turnStrength: "每回合力量", turnEnergy: "每回合费用", turnBlock: "每回合格挡", turnLoseHp: "披风每回合失血", infernoLoseHp: "狱火每回合失血", lossDamage: "失血全体伤害", lossStrength: "失血获得力量", vulnerableBonus: "易伤倍率加成", attackBlock: "攻击获得格挡", retaliation: "反击伤害", colossus: "巨像减伤" };
|
||||
return Object.entries({ ...s.powers, ...s.turn, ...s.defense }).map(([k, v]) => `${names[k] || k}:${v === true ? "50%" : k === "vulnerableBonus" ? `${v * 100}%` : v}`).join("<br>");
|
||||
} },
|
||||
|
||||
@@ -13,6 +13,7 @@ import { applyMengsanDebuff, consumeArtifact } from "../../../monsters/artifact-
|
||||
import { isStunned } from "../../../battle/stun-intent.js";
|
||||
import { canPayCard, isActiveCardUse, authorizeFreeCardUse, takeXCardUse, cardCost } from "../../../battle/combat-rules.js";
|
||||
import { grantGold } from "../../../relics/progression.js";
|
||||
import { bindCombatStatusSource } from "../../../battle/status-sources.js";
|
||||
|
||||
const BRIDGE="mengsan_shared_bridge_shuying", RULES="mengsan_shared_rules_shuying", END="mengsan_shared_end_shuying", LIMIT="mengsan_shared_limit_shuying", AFTER="mengsan_shared_after_shuying";
|
||||
const safe=n=>Number.isSafeInteger(n) && n>=0;
|
||||
@@ -51,6 +52,7 @@ export function createSharedCardRuntime({game,get,lib,status,getBattle,battleEne
|
||||
s={player,token,powers:Object.create(null),turn:Object.create(null),freeTurn:new WeakSet(),freeBattle:new WeakSet(),
|
||||
nextTurn:[],bombs:[],negativeStrength:0,autoCount:0,drawCount:0,returns:[],maulBonus:0,vigor:0,queuedAllyEnergy:0};
|
||||
states.set(player,s);player.addSkill(RULES);
|
||||
bindCombatStatusSource(player,token.session,"shared",()=>stateOf(player));
|
||||
token.session.ownResource(s,()=>{if(states.get(player)===s){states.delete(player);player.removeSkill(RULES);s.nextTurn.length=0;s.returns.length=0;}});
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
# 梦三与刘备玩法调整方案 V0.1
|
||||
|
||||
- 日期:2026-10-30
|
||||
- 状态:策划提案,尚未实施
|
||||
- 依据:当前项目源码与配置,尚未进行实战采样
|
||||
- 适用范围:梦三模式中的刘备、先天关张助战及相关构筑
|
||||
|
||||
建议把刘备定位为“能独立作战、通过仁德调度关张的协同型主角”,保留现有力量、自损、消耗牌组作为构筑分支。
|
||||
|
||||
本文中的建议数值需要通过对局验证;生成本文档不代表已经修改游戏机制,也不代表完成实机验收。
|
||||
|
||||
## 一、简单评估
|
||||
|
||||
梦三已经具备个人牌库、费用、怪物意图、地图选择、遗物和羁绊,足以支撑“战斗—构筑—路线选择”的基本循环。当前更值得优先处理的是角色特色和资源分配。
|
||||
|
||||
| 方面 | 当前情况 | 策划判断 |
|
||||
| --- | --- | --- |
|
||||
| 刘备特色 | 先天结识关张,但专属牌主要围绕力量、自损、消耗 | 角色技能与牌组之间缺少直接联系 |
|
||||
| 玩家操作 | 3费用,默认每回合摸2张,手牌上限3 | 响应牌也占用手牌,可能出现有费用却缺少主动选择 |
|
||||
| 关张差异 | 基础属性、默认牌库和意图相同 | 更换助战缺少战术意义 |
|
||||
| 战斗波动 | 初始8级助战到场概率为77.78% | 重要战斗可能因助战缺席产生明显波动 |
|
||||
| 回复收益 | 休息基础回复8;初始遗物每战结束回复6 | 休息的机会成本偏高,值得重新测试 |
|
||||
|
||||
助战的群伤尤其需要关注:当前一次6点群体意图面对3名敌人,可以造成合计18点基础伤害,随后还可以继续出牌。这个收益会随敌人数增长,不能只按“6点伤害”估价。此处为不计力量、易伤、护甲等修正的数值举例,不是实测平均伤害。
|
||||
|
||||
## 二、刘备玩法方向
|
||||
|
||||
核心体验:**刘备决定资源交给谁、何时防守、集火哪个敌人,关张负责兑现不同的战术收益。**
|
||||
|
||||
建议先保留两条主要路线:
|
||||
|
||||
| 路线 | 核心打法 | 助战选择 | 代价 |
|
||||
| --- | --- | --- | --- |
|
||||
| 协同破阵 | 刘备施加易伤、提供手牌,集中消灭关键敌人 | 关羽 | 清理大量小怪较慢 |
|
||||
| 坚守清场 | 刘备积累防御、处理杂牌,让助战持续压制群敌 | 张飞 | 单体爆发较弱 |
|
||||
|
||||
现有自损、力量、消耗体系保留为进阶构筑。先让基础协同成立,再逐步加入“刘备承担代价、换取团队收益”的专属牌。
|
||||
|
||||
## 三、核心机制调整
|
||||
|
||||
新增先天技能「仁德」:
|
||||
|
||||
> 每个自身出牌阶段限一次,消耗1费用,将一张可转移的非状态、非诅咒手牌交给另一名友方角色;然后自己摸1张牌,获得5点护甲。
|
||||
> 没有其他存活友方时,改为弃置一张符合条件的手牌,获得相同收益。
|
||||
|
||||
设计目的:
|
||||
|
||||
- 有助战时,可以把攻击牌交给关张,自己承担防守。
|
||||
- 独自作战时,仍可整理手牌。
|
||||
- 与普通「防御」相比,同为1费用、5护甲;额外收益来自调度手牌,但每回合只能使用一次。
|
||||
- 转交不视为使用或响应,不触发该牌的使用效果;费用和手牌必须同时满足才能发动。
|
||||
|
||||
先维持刘备**50生命、3费用、每回合摸2张、手牌上限3**,观察仁德是否已经改善缺牌问题,避免同时提高所有资源。
|
||||
|
||||
## 四、第一轮数值平衡
|
||||
|
||||
以下数值作为一组测试配置,与当前版本对照。实际实施时仍按第六节分阶段验证。
|
||||
|
||||
| 项目 | 当前值 | 建议值 | 目的 |
|
||||
| --- | --- | --- | --- |
|
||||
| 刘备先天关张到场 | 初始8级,77.78% | 可参战且未濒死时100%;首战限制保留 | 让角色核心能力可靠 |
|
||||
| 关张每回合费用 | 3 | 2,意图仍消耗1 | 降低助战替玩家完成战斗的程度 |
|
||||
| 关张每回合摸牌 | 2 | 1 | 让仁德送牌具有实际价值 |
|
||||
| 关张初始手牌 | 4 | 3 | 收紧开场爆发 |
|
||||
| 关张手牌上限 | 5 | 4 | 仍允许保存玩家交来的牌 |
|
||||
| 关羽最大生命 | 各等级40 | 30+等级,初始8级为38 | 等级提供小幅、确定的成长 |
|
||||
| 张飞最大生命 | 各等级40 | 34+等级,初始8级为42 | 承担更持久的群战 |
|
||||
| 休息基础回复 | 8 | 最大生命的30%,向上取整 | 50生命时回复15,让休息值得选择 |
|
||||
|
||||
关张的确定到场规则仅作用于刘备先天羁绊。等级和现有成长进度保留,等级收益由到场概率转为上述生命成长。
|
||||
|
||||
关张意图改为各自循环:
|
||||
|
||||
| 角色 | 第一次行动 | 第二次行动 | 第三次行动 |
|
||||
| --- | --- | --- | --- |
|
||||
| 关羽 | 单体7伤害 | 自身获得6护甲 | 单体10伤害 |
|
||||
| 张飞 | 对全部敌人各造成3伤害 | 自身获得6护甲 | 对全部敌人各造成4伤害 |
|
||||
|
||||
关羽优先攻击刘备上一回合最后攻击的存活敌人;没有有效目标时,攻击生命最低的敌人。张飞负责群体压制。
|
||||
|
||||
这些意图替换当前默认循环,并保留之后用剩余费用出牌的行为。面对3名敌人时,张飞第一次意图基础总伤害为9,低于当前的18;确定到场则减少了整体体验的波动。
|
||||
|
||||
## 五、初始牌组与高风险卡牌
|
||||
|
||||
初始牌组继续保持12张:
|
||||
|
||||
**4杀+3防御+1闪+1桃+1无中生有+1八卦阵+1重杀。**
|
||||
|
||||
具体是将当前的一张杀换成防御,将决斗换成重杀。这样新征程较早阶段即可体验“防御、施加易伤、关羽跟进”的基本配合。杀、防御、重杀的基础数值暂时保持。角色专属首战仍按现有限制不触发羁绊助战;关羽跟进的配合在后续允许助战的战斗中体验。
|
||||
|
||||
两张专属牌优先测试:
|
||||
|
||||
| 卡牌 | 当前效果 | 建议调整 |
|
||||
| --- | --- | --- |
|
||||
| 炽焰 | 2费,给予另一友方5/7力量 | 改为3/4力量;费用保持 |
|
||||
| 扯碎 | 2费,每次实际失血额外攻击一次,没有设计上限 | 最多额外攻击5次;每段伤害保持5/7 |
|
||||
|
||||
表中的“5/7”等数值按未强化/强化顺序列出。扯碎上限仅针对额外段数,基础1段保留,即最多6段;其他伤害修正照常结算。
|
||||
|
||||
炽焰的力量会被多段、群体攻击放大,需要随助战体系一起评估。扯碎则保留成长爽点,但避免通过拖长战斗无限增加段数。
|
||||
|
||||
初始遗物每战回复6点、自损牌的生命代价先保持,避免这一轮同时改变过多续航来源。
|
||||
|
||||
## 六、验证与实施顺序
|
||||
|
||||
先收集当前版本基线,再测试调整版。使用相同地图种子和遭遇,分别覆盖单体、群怪、精英、Boss,以及助战濒死后的独立作战。
|
||||
|
||||
重点记录:
|
||||
|
||||
- 刘备每回合剩余费用,以及“有费用却无有效牌”的次数。
|
||||
- 刘备与助战各自的伤害占比、战斗回合数。
|
||||
- 每场损失生命及战后回复后的净损失。
|
||||
- 关羽、张飞在不同遭遇中的选择价值。
|
||||
- 仁德使用率、送出的牌是否被有效使用。
|
||||
|
||||
首轮暂定目标:普通战斗约3—5个刘备回合;助战伤害占比中位数约25%—40%;关羽在单体战更有效,张飞在群怪战更有效。这些是调参目标,不是当前实测结果。
|
||||
|
||||
落地顺序建议为:**先测试助战与初始牌组数值,再加入仁德,最后调整专属牌和休息收益。** 每一步保留对照数据,确认收益来自哪里,再决定是否调整怪物强度。
|
||||
|
||||
## 七、当前配置依据
|
||||
|
||||
以下链接指向项目实际文件,便于后续实施时重新核对。本文记录的是编写时观察到的配置,源码后续可能变化。
|
||||
|
||||
- [刘备属性与先天技能](../content/scenario-characters.js)
|
||||
- [费用、手牌上限与基础伤害](../battle/combat-rules.js)
|
||||
- [刘备初始牌组](../cards/starting-deck.js)
|
||||
- [关张基础属性与成长配置](../bonds/definitions.js)
|
||||
- [助战概率、初始化与成长](../bonds/state.js)
|
||||
- [默认助战意图及费用](../bonds/intents.js)
|
||||
- [意图的敌对目标与效果结算](../battle/intent-effects.js)
|
||||
- [炽焰、扯碎等专属牌数值](../cards/packs/liubei/data.js)
|
||||
- [专属牌实际结算与失血次数统计](../cards/packs/liubei/runtime.js)
|
||||
- [休息回复](../relics/rooms.js)
|
||||
- [燃烧之血回复规则](../relics/rules.js)
|
||||
- [新征程初始遗物授予](../progression/state.js)
|
||||
@@ -0,0 +1,389 @@
|
||||
# 梦三与刘备玩法调整方案 V0.2:敌人、意图循环与 Boss 联合平衡
|
||||
|
||||
- 日期:2026-10-30
|
||||
- 状态:策划提案;建议参数尚未实施,也未完成游戏内验证。
|
||||
- 范围:当前第一章、刘备、关张助战、4种弱池遭遇、12种强池遭遇、3种精英与3种 Boss 遭遇,以及它们的随从。
|
||||
- 前版:[V0.1](liubei-gameplay-and-balance-proposal-20261030.md)。本版与前版冲突时以本版建议为准,V0.1 保留作对照。
|
||||
- 证据:当前本地源码。下文的逐轮数据是规则推算,不是自动对局胜率;没有以外部游戏原版的数值证明梦三平衡。
|
||||
|
||||
## 1. 对 V0.1 的修正
|
||||
|
||||
V0.1 没有充分考虑怪物,不能直接作为平衡改动清单执行。它主要讨论玩家、助战和回复,遗漏了以下约束:
|
||||
|
||||
1. 怪物的攻击意图目前通常作用于全部存活敌对角色,刘备和助战各承受完整伤害;助战到场并不分摊刘备受到的意图伤害。
|
||||
2. 意图通常只花1费用,之后仍能主动出牌。普通/精英/Boss 原有费用为2/3/4;图标上的意图伤害不等于整回合伤害。
|
||||
3. 力量按攻击段数放大;增益招式后的原生攻击牌会立即享受新力量,不能把增益轮直接当作安全轮。
|
||||
4. 血量之外,还有滑溜、护甲、回复、缩小、缓慢、伤口、感染、亡语和胜利条件。它们会改变击杀速度或实际生存空间。
|
||||
5. 多敌人的循环会在某一轮叠出峰值,不能把单只怪物平均伤害相加后就判定合理。
|
||||
6. V0.1 同时削减助战费用、摸牌、起手和攻击,缺少玩家能否承受后续怪物的依据;仁德换牌也不增加净手牌数量。
|
||||
|
||||
本版采用“先计算敌方整轮压力,再决定玩家可承担的防御成本和击杀时间”的方法。所有新规则均为策划建议,不能读成已经存在的机制。
|
||||
|
||||
## 2. 当前战斗规则及计算口径
|
||||
|
||||
### 2.1 已核对的事实
|
||||
|
||||
| 项目 | 当前实际规则 | 对平衡的影响 |
|
||||
| --- | --- | --- |
|
||||
| 刘备 | 50生命,3费用,默认摸2张,手牌上限3 | 不能默认每轮都有3张主动牌,更不能把3费用全部算作攻击 |
|
||||
| 防御与回复 | 防御1费获得5护甲,强化为8;桃回复8;燃烧之血战后回复6 | 战后回复救不了战斗中的致死峰值 |
|
||||
| 护甲 | 通常在持有者下次主循环回合开始时清除;保留类效果例外 | 护甲不能当作跨轮累计生命;给即将行动的助战护甲,可能先被清除 |
|
||||
| 意图攻击 | 直接造成攻击伤害,没有【杀】的【闪】响应过程 | 不能把手中的闪自动算作抵消一次意图 |
|
||||
| 普通敌人 | 摸1、上限3、费用2;大部分初始4张 | 意图后通常还留1费用 |
|
||||
| 精英 | 摸2、上限5、费用3 | 意图后通常还留2费用 |
|
||||
| Boss | 摸3、上限7、费用4 | 意图后通常还留3费用 |
|
||||
| 关张 | 当前均40生命、起手4、摸2、上限5、费用3 | 两人尚无数值与意图差异 |
|
||||
| 当前敌方牌组 | 包含杀、闪、桃、酒,较高阶还有决斗、无中、过河 | 原生出牌、响应及治疗都是额外变量 |
|
||||
| 第一章 | 前3场普通胜利用弱池,之后用强池;专属首战不计数;当前章末结束内容 | 不能把第二、三章占位配置当成当前连续难度曲线 |
|
||||
|
||||
来源:[战斗费用](../battle/combat-rules.js)、[卡牌效果](../cards/mode-cards.js)、[怪物属性与牌组](../monsters/monster.js)、[主循环与状态](../mode.js)、[普通胜场计数](../progression/encounter-progress.js)、[第一章地图](../content/acts/act1/map.js)。
|
||||
|
||||
### 2.2 应计算什么
|
||||
|
||||
以“刘备行动开始,到刘备下一次行动开始”为一轮,同时记录每个敌人实际执行的行动次数。下列简化表假设没有额外回合、击晕、提前死亡、反伤中断和异常状态;有这些效果时必须按实际行动重算。
|
||||
|
||||
- 每段攻击先计基础伤害与力量,再计虚弱、缩小,之后计目标易伤等修正。虚弱与缩小分别向下取整,易伤向上取整,不能先把所有段数合并再取整。
|
||||
- 基础攻击修正示例:`每段 = ceil(floor((基础伤害 + 力量) × 0.75) × 1.5)`,仅适用于同时有虚弱和易伤、没有其他修正的情况。
|
||||
- 一轮总压力 = 全部实际命中的意图伤害 + 实际命中的手牌攻击 + 感染/紧缠等额外伤害。
|
||||
- 生命损失按事件顺序抵扣当时护甲;不把同一份护甲重复用于多个敌人。没有中途增减护甲等效果时,才可简化为 `max(0, 本轮可抵扣伤害合计 - 本轮护甲)`。
|
||||
- 击杀所需时间同时取决于有效生命和实际输出。普通 `HP÷每轮输出` 只作估算;滑溜要数命中事件,雕像要数前置出牌,亡语要算下一批敌人。
|
||||
- 单独记录刘备和助战生命。当前全部意图打全队时,增加队友会增加队伍总承伤,不会降低刘备承伤。
|
||||
|
||||
**例如多尼斯异鸟3段啄击,+3力量后为6×3=18;若目标同时易伤,则9×3=27。绝不能算成9+3=12。**
|
||||
|
||||
来源:[意图执行](../battle/intent-effects.js)、[意图伤害](../battle/intent-damage.js)。
|
||||
|
||||
## 3. 当前普通怪物:血量、循环与压力
|
||||
|
||||
以下数值均为当前值。`→循环`表示回到循环首项;“+力”在该次攻击结算后获得,除非注明开场增益。伤害均为单个目标承受的基础值,当前通常每名友方各受一份。HP范围采用入场随机函数,不用资料页中间值冒充固定血量。
|
||||
|
||||
| 敌人 | 当前HP | 当前意图与附加机制 | 主要问题 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首关士兵 | 24 | 没有这批怪物的专属意图;依靠原生手牌、坐骑 | 单独作为教学战评价,不套专属意图伤害公式 |
|
||||
| 毛绒伏地虫 | 55—57 | 酸液4→力量+7→酸液4→循环 | 实际前6次意图伤害4、0、11、11、0、18;第2次加力后仍可能出杀13 |
|
||||
| 小啃兽 | 42—46 | 顶撞12→斩击6并护甲5→力量+2→循环 | 弱池首轮12,仍可能追加杀;双只从斩击、哈气错位开场 |
|
||||
| 缩小甲虫 | 38—40 | 首次缩小,此后7与13交替;缩小令攻击伤害降低30%,源死亡解除 | 把玩家输出降低后,还按原DPS估计战斗时长会偏乐观 |
|
||||
| 树叶史莱姆·小 | 11—15 | 每次等概率冲撞3或塞1黏液 | 随机重复塞牌拖慢循环 |
|
||||
| 树叶史莱姆·中 | 32—35 | 塞2黏液→攻击8→循环 | 群怪战的卡手压力不能只计为0伤害 |
|
||||
| 树枝史莱姆·小 | 7—11 | 每次攻击4 | 单只不高,和大怪同步时要合计 |
|
||||
| 树枝史莱姆·中 | 26—28 | 首次塞1黏液,之后攻击11;黏液不连用,攻击最多连用两次 | 连续两轮11是实际允许分支 |
|
||||
| 飞蝇菌子 | 48 | 猛砸11不连用;孢子8并2脆弱,间隔至少2次;2易伤第二次起可用,间隔至少3次 | 与同行怪物叠加易伤/脆弱,且座次决定当轮谁吃到增益 |
|
||||
| 闪光贾克斯果 | 31—33 | 每次攻击3,之后力量+2 | 意图前4轮3、5、7、9;加力同时提高当轮后续杀 |
|
||||
| 立柱构造体 | 65;初始13护甲、1人工制品 | 首次力量+2;以后攻击7并+2力、攻击7并+2力、5×2循环 | 实际前4次0、9、11、22;13初始护甲通常到其首回合开始被清除,并非永久加血 |
|
||||
| 雾菇 | 74;眼6 | 首次召眼;重击8并+1力,最多连用两次,再头槌14;眼每次塞3晕眩 | 眼不必击杀才能获胜,但放任会降低摸到有效牌的概率 |
|
||||
| 蛮兽 | 72 | 首次4×2;之后与14重击及一次性3易伤怒吼按约束抽选,同种攻击不连用 | 易伤后的14变21,仍未计额外出牌 |
|
||||
| 藤蔓蹒跚者 | 61 | 6×2→8并1缠结→16→循环 | 缠结让下一次应对大伤害时攻击牌加费,抢杀能力下降 |
|
||||
| 蛇行扼杀者 | 53—55 | 叠3紧缠→等概率7并护甲5或12→循环 | 紧缠在目标回合末造成可被护甲抵扣的非攻击伤害;来源存活时层数累计 |
|
||||
| 墨宝 | 每只11—17,开场1滑溜;三只一组 | 奇数位刺击3,偶数位2×3起手;之后随机10或2×3与刺击交替 | 第一轮三只合计12,第二轮15—23;各自滑溜增加击杀所需命中次数 |
|
||||
|
||||
循环源文件: [毛虫](../monsters/crawler-intent.js)、[小啃兽](../monsters/nibbit-intent.js)、[甲虫](../monsters/shrinker-intent.js)、[树叶小](../monsters/leafslime-intent.js)、[树叶中](../monsters/leafmedium-intent.js)、[树枝小](../monsters/twigslime-intent.js)、[树枝中](../monsters/twigmedium-intent.js)、[飞蝇](../monsters/flyconid-intent.js)、[贾克斯果](../monsters/jaxfruit-intent.js)、[立柱](../monsters/cubex-intent.js)、[雾菇](../monsters/fogmog-intent.js)、[蛮兽](../monsters/mawler-intent.js)、[藤蔓](../monsters/vine-intent.js)、[扼杀者](../monsters/strangler-intent.js)、[紧缠伤害](../monsters/strangler-constrict.js)、[墨宝](../monsters/inklet-intent.js)。
|
||||
|
||||
### 3.1 劫掠者必须作为组合评价
|
||||
|
||||
当前从5种成员随机取3名且不重复;表内HP为实际关卡抽取范围。
|
||||
|
||||
| 成员 | 当前HP | 当前循环 |
|
||||
| --- | --- | --- |
|
||||
| 暴徒 | 30—33 | 7伤害→力量+3→循环 |
|
||||
| 刺客 | 18—23 | 每次10伤害 |
|
||||
| 斧手 | 20—22 | 5伤害并护甲5→同招→12伤害→循环 |
|
||||
| 弩手 | 18—21 | 护甲3→14伤害→循环 |
|
||||
| 追踪手 | 21—25 | 首次2脆弱,此后每次1×8伤害;并非持续在两招间交替 |
|
||||
|
||||
“刺客+弩手+追踪手”第二轮有10+14+8=32点意图伤害,对当前每名友方各结算一次;三名普通敌人还各可能出一张杀。该组合不能按三只低血量敌人处理。
|
||||
|
||||
来源:[三人编队](../content/acts/act1/raider-trio.js)、[实际循环](../monsters/raider-intent.js)。
|
||||
|
||||
## 4. 当前精英与 Boss 的完整威胁
|
||||
|
||||
### 4.1 精英
|
||||
|
||||
| 精英 | 血量与实际击杀要求 | 当前攻击循环 | 压力结论 |
|
||||
| --- | --- | --- | --- |
|
||||
| 多尼斯异鸟 | 81—84 | 17与3×3交替;每个自身回合结束+1力 | 前6次17、12、19、18、21、24;多段招增长更快 |
|
||||
| 旧日雕像 | 127;己方每使用一张牌,后续攻击对它多10%伤害,同回合累计 | 沉睡→力量+10→每次13伤害 | 实际斩击23;只有沉睡明确取消出牌,苏醒轮仍可能出力量加成的手牌 |
|
||||
| 异蛙寄生虫 | 61—64,死后生成4只17—21HP扭动虫,都要处理 | 塞3感染→4×4→循环;虫生成时跳过一次完整回合,此后错位执行6伤害与塞1感染/+2力 | 总需处理生命129—148;不能按62血精英定价。感染留在手中到弃牌前,每张造成3非攻击伤害 |
|
||||
|
||||
来源:[异鸟](../monsters/byrdonis-intent.js)、[雕像](../monsters/effigy-intent.js)、[缓慢](../monsters/effigy-slow.js)、[异蛙与虫](../monsters/phrog-intent.js)、[亡语](../monsters/phrog-battle.js)、[感染](../cards/infection-card.js)。
|
||||
|
||||
### 4.2 Boss:当前逐轮意图伤害
|
||||
|
||||
以下只算意图,假定相关敌人持续存活、没有减伤和护甲。数值是对刘备的伤害;当前助战通常另承受同样一份。
|
||||
|
||||
| Boss/阶段 | 当前HP与机制 | 第1—8次行动的意图伤害 |
|
||||
| --- | --- | --- |
|
||||
| 墨影幻灵 | 173;初始9滑溜,每次实际失血至多1并耗1层;肢解塞3伤口 | 7、12、26、0、9、16、28、0,合计98 |
|
||||
| 仪式兽一阶段 | 252;首招设置150血转换阈值,之后18伤害并+2力 | 若一直不转阶段:0、18、20、22、24、26、28、30 |
|
||||
| 仪式兽二阶段 | 转换后力量归零,并跳过一次意图及出牌;之后咆哮→15→17并+3力 | 从二阶段首次咆哮起:0、15、17、0、18、20、0、21 |
|
||||
| 同族小队 | 神官190+两信徒各58—59;只需杀神官即可获胜 | 三者均存活:18、16、9、14、26、10、33、24,合计150 |
|
||||
|
||||
同族小队拆分:神官前8次为8、8、9、0、10、10、15、0;每名信徒为5、4、0、7、8、0、9、12。神官四步循环与信徒三步循环形成12轮组合;第11轮全部存活时,意图合计为53,不能只验第一圈。
|
||||
|
||||
仪式兽的150是生命阈值,不是150护甲。正常满血需先造成102实际生命损失才抵达该区间,并由符合条件的伤害事件触发转换;不能把血量写成252+150。
|
||||
|
||||
来源:[墨影](../monsters/vantom-intent.js)、[仪式兽](../monsters/beast-intent.js)、[同族](../monsters/kin-intent.js)、[Boss编队](../content/acts/act1/kin-group.js)、[意图实际执行](../monsters/actions.js)。
|
||||
|
||||
### 4.3 原生出牌造成的额外峰值
|
||||
|
||||
| 情景 | 意图 | 条件成立时的额外攻击 | 合计 |
|
||||
| --- | --- | --- | --- |
|
||||
| 墨影第3次行动 | 26 | 剩3费用,若有3杀且均合法使用、均未被闪抵消:18 | 44,另塞3伤口 |
|
||||
| 同族第1轮 | 18 | 神官3杀+两信徒各1杀,均打刘备且命中:30 | 48,另施加脆弱 |
|
||||
| 毛虫第2次行动 | 意图伤害0,力量+7 | 1杀,6+7=13 | 13,增益轮并不安全 |
|
||||
|
||||
这些是有条件的压力情景,不是AI一定执行的行为,也不是全游戏理论上限。酒、其他增益、易伤、距离、手牌、选目标与响应都会改变结果。V0.1 用普通怪3—5回合、助战占比25%—40%作为直接平衡目标,依据不足。
|
||||
|
||||
## 5. V0.2 推荐采用的战斗规则
|
||||
|
||||
### 5.1 考虑过的两种路径
|
||||
|
||||
| 路径 | 做法 | 取舍 |
|
||||
| --- | --- | --- |
|
||||
| 保留自由出牌并降数值 | 保持当前全部意图打全队,敌人自由用剩余费用出牌,只降HP/伤害 | 改动较小,但意图仍不能解释整轮风险;需要大量对局才能约束峰值 |
|
||||
| **意图明确目标,追加攻击也预告(本版推荐)** | 为招式区分单体/全体,收紧专属怪物的额外主动攻击,按完整预告调数值 | 需要调整执行与预告,换取可判断的防御窗口;保留有限的三国杀响应互动 |
|
||||
|
||||
推荐路径由本提案提出,尚不代表用户已批准实现。下列参数以推荐路径为前提;如果保留当前无限制混合出牌,不可直接套用后面的承伤结论。
|
||||
|
||||
### 5.2 意图目标与原生响应
|
||||
|
||||
1. 普通怪和精英的意图默认单体,固定预告指向刘备;墨宝旋风、藤蔓挥击、异蛙甩动、异鸟啄击明确为全体。负面状态默认只施加给本次目标。
|
||||
2. Boss 招式按第8节逐招列出的目标执行。全体招式对每名友方结算完整伤害,不分摊;单体招式不暗中随机切到关张。
|
||||
3. 来源或目标提前死亡时取消对应效果,不在行动时临时换人;现有“刘备阵亡则结束征程”的流程优先。
|
||||
4. 脚本意图继续使用护甲、虚弱等机制应对,仍不能用闪直接抵消;预告中明确标注。实际追加的杀仍走闪、距离、装备等原生规则。
|
||||
5. 敌人的闪、对决斗打出的杀和濒死救援等响应继续保留;它们仍可能延长实际击杀时间,必须纳入游戏内采样。
|
||||
|
||||
### 5.3 限制意图后的额外主动出牌
|
||||
|
||||
对有专属意图的敌人,取消“发动完意图后任意主动出牌”。只允许下述事先预告的追加杀;不再自由主动使用桃、酒、无中、决斗或过河。没有专属意图的首关士兵仍按原流程行动。
|
||||
|
||||
- 普通遭遇:整支敌队每轮最多追加1张杀。该轮只有在意图原始合计伤害加该杀伤害不超过16时可选;含力量、段数,暂不计目标易伤、护甲及玩家减益。该值是基础预算,实际易伤后的伤害必须另行显示。
|
||||
- 精英遭遇:同口径上限20,整支敌队每轮最多追加1张杀。亡语生成的虫也共享额度。
|
||||
- Boss:仅墨影“墨迹”轮允许追加1杀;仪式兽、同族小队不额外主动出牌,使用其完整意图循环提供压力。
|
||||
- 只有本轮执行伤害意图的单位可以追加。整备、沉睡、苏醒、蓄力、召唤、单独施加负面状态和阶段击晕的单位不追加,形成真实准备窗口。
|
||||
- 在刘备行动前从符合条件的敌人已有手牌中选择并锁定1张实体杀,按固定座次选首个合格单位;不得把敌人随后新摸到的牌临时追加。预告显示来源、目标、伤害和“可用闪响应”。
|
||||
- 使用仍支付原费用并重新校验牌、距离、目标和战斗会话;牌被夺走、费用不足、单位被击晕时取消,不补抽、不替换、不补偿攻击。实际伤害按当时状态结算,预告随状态刷新。
|
||||
- 群怪原始意图本身已超过16/20时,不通过暗中削伤压回预算,而是禁止追加,并按第7节调整组合和出场时机。
|
||||
|
||||
这项规则会改变敌人手牌的主动用途,也减少主动回血造成的额外有效生命;因此它与后面的HP调整需要分批验证。
|
||||
|
||||
## 6. 刘备与助战:修订 V0.1 的内容
|
||||
|
||||
| 项目 | V0.2 建议 | 理由 |
|
||||
| --- | --- | --- |
|
||||
| 刘备基础 | 保持50生命、3费用、摸2、手牌上限3 | 先核对真实手牌吞吐;若缺牌再独立试摸3,不预先认定已有足够防御与输出 |
|
||||
| 先天羁绊 | 仍提议关张可参战时100%到场,保留首战排除、濒死不能到场 | 核心玩法应可依赖 |
|
||||
| 助战资源 | **撤回同时削费、削摸牌、削起手的建议**;首轮保留费用3、摸2、起手4、上限5、HP40 | 减少叠加削弱;先改敌方目标与意图,再观察输出占比 |
|
||||
| 关羽意图 | 单体7→自身护甲8→单体10,循环;每次仍消耗1费用 | 集火,保留能应对群体攻击的防守轮 |
|
||||
| 张飞意图 | 全敌各3→自身护甲8→全敌各4,循环;每次仍消耗1费用 | 清理低血随从,与关羽形成区别 |
|
||||
| 仁德 | 沿用V0.1:1费用、每出牌阶段一次,给一张合规手牌/独行时弃一张,再摸1并自身护甲5 | 它是换牌与支援,不是净抽牌;不能当作每轮多摸1张 |
|
||||
| 初始牌库 | 保留V0.1的12张提案:4杀、3防御、闪、桃、无中、八卦、重杀各1 | 提供防御、易伤和有限响应;首战不承诺关羽配合 |
|
||||
| 炽焰/扯碎 | **撤回首轮立即削弱**;原数值作为候选基线,后续按力量多段与失血构筑实测处理 | 敌人和助战机制变化后,不能沿用旧假设直接砍关键输出 |
|
||||
| 回复 | 休息基础30%最大生命向上取整仍列为后续独立实验;遗物额外休息回复另加;战后6保留 | 必须先保证战斗可生存,再评价跨关续航 |
|
||||
|
||||
仁德的“不可转移/非状态/非诅咒”按卡牌实际资格判定;转交不等于打出。若实施为独立技能,需要明确仪式兽昏眩期间能否发动:本版建议**昏眩时仁德与主动牌共用一次主动行动额度**,避免技能绕过Boss限制;它不因此被视为卡牌,也不凭空触发出牌连锁。
|
||||
|
||||
新关羽优先攻击刘备最近一个自身回合最后攻击且仍存活的敌人,否则选最低生命敌人;张飞作用于全部存活敌人。护甲只给自己,避免“刘备先给关羽护甲、关羽回合开始立即清空”的时序问题。
|
||||
|
||||
## 7. 普通怪、编队与精英的建议参数
|
||||
|
||||
这是首轮试验值,不是已证明的最终值。未列出的机制保留;本节目标规则统一遵从5.2。所有修改按第10节分批加入,不一口气套用。
|
||||
|
||||
### 7.1 普通怪参数
|
||||
|
||||
| 敌人 | 当前HP → 建议HP | 意图建议 | 目标 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首关士兵 | 24 → 24 | 保留原生出牌;不套专属意图限制 | 校准教学战 |
|
||||
| 毛虫 | 55—57 → 45—48 | 4→加4力→4,循环 | 前6次变4、0、8、8、0、12;减缓滚雪球 |
|
||||
| 小啃兽 | 42—46 → 36—40 | 顶撞12→10;斩击6→5、护甲5保留;加2力保留 | 压低弱池首轮冲击 |
|
||||
| 缩小甲虫 | 38—40 → 34—36 | 缩小30%保留;7/13改7/11 | 用更低生命补偿其降低玩家输出的能力 |
|
||||
| 树叶小/树枝小 | 11—15/7—11不变 | 3/4伤害保留,塞1黏液保留 | 让单体集火有明确击杀收益 |
|
||||
| 树叶中 | 32—35 → 28—31 | 塞2黏液→1;伤害8→7 | 缓和多怪的卡手叠加 |
|
||||
| 树枝中 | 26—28 → 24—26 | 扑击11→9,原连用限制保留 | 降低连续两轮尖峰 |
|
||||
| 飞蝇菌子 | 48 → 42 | 猛砸11→9;伤害孢子8→6,脆弱2→1;易伤2保留 | 保留“先杀增伤源”的判断 |
|
||||
| 贾克斯果 | 31—33 → 28—30 | 3伤害保留,每次+2力→+1力 | 增长3、4、5、6,便于给玩家集火时间 |
|
||||
| 立柱 | 65 → 56;初始护甲13→10 | 开场+1力;重复轰击7并+1力,两次后4×2 | 前4次0、8、9、14,仍是成长型敌人 |
|
||||
| 雾菇 | 74 → 64;眼6不变 | 重击8并+1力保留;头槌14→12;眼每次3晕眩→2 | 保留处理眼或抢杀本体的取舍 |
|
||||
| 蛮兽 | 72 → 60 | 4×2保留;14→12;3易伤→2,怒吼一次限制保留 | 易伤重击由21降至18 |
|
||||
| 藤蔓 | 61 → 54 | 全体5×2→单体6并缠结1→单体13 | 群体招更轻,缠结后仍有危险单体招 |
|
||||
| 扼杀者 | 53—55 → 46—48 | 紧缠每次3→2;重击7/护甲5→6/4;甩动12→10 | 紧缠仍累计并由杀源解除 |
|
||||
| 墨宝三只 | 各11—17保留 | 刺击3、全体2×3保留;凝视10→8;开场滑溜1保留 | 不同时削生命与破盾机制;控制随机爆发 |
|
||||
| 劫掠者 | 五种原HP范围保留 | 暴徒5/加2力;刺客7;斧手4+护甲4、4+护甲4、9;弩手护甲3/10;追踪2脆弱后1×6 | 三人组合第二轮高压示例32→23,且无额外杀 |
|
||||
|
||||
### 7.2 编队必须有出场时机和峰值验算
|
||||
|
||||
- 原有前3次普通胜利用弱池保持。前3胜保护已有普通化精英节点的逻辑,不重复设计一个冲突的保护。
|
||||
- 建议第4—5场普通战斗优先开放单体强敌与两单位组合;三墨宝、四史莱姆、劫掠者三人组从第6场普通战斗起加入。
|
||||
- 小啃兽双只继续错位起手。建议版前3轮意图为5、12、19:第一只“斩击、加力、顶撞”,第二只“加力、顶撞、斩击”。第3轮要在预告中表现为集火节点。
|
||||
- 甲虫+毛虫要同时计缩小对队伍输出的影响。优先杀甲虫释放输出,或抢杀毛虫阻止加力,都应有可行牌序;不要仅因总血量低于两只普通怪就判断简单。
|
||||
- 飞蝇与其他怪的组合应按实际座次计算:先施加易伤,后面的怪可能当轮受益;易伤层数在目标自己的回合结束衰减,不能按全局轮数随意扣。
|
||||
- 劫掠者3选5共有10种组合,全部检查前6轮。建议版“刺客+弩手+追踪手”第2轮仍有23原始伤害,作为晚段集火考验;若代表牌组在该轮前无法稳定杀掉一个低血目标,应继续降低该组合伤害或延后出现,不靠提高所有怪物血量修补。
|
||||
- 群怪阵亡后立即移除该单位余下行动、负面来源和后续轮贡献;“所有敌人永远存活”的表仅作为压力包络。
|
||||
|
||||
来源:[遭遇组成](../content/acts/act1/encounters.js)、[小啃兽双只](../content/acts/act1/nibbit-pair.js)。
|
||||
|
||||
#### 7.2.1 劫掠者10种组合的六轮压力表
|
||||
|
||||
以下按三人始终存活、原有循环起点不变计算,仅列对刘备的意图伤害;未加入手牌攻击、玩家护甲及状态修正。建议版采用7.1的参数。当前版同样的意图伤害通常还会对助战各结算一份,建议版这些招式均为单体刘备。
|
||||
|
||||
| 组合 | 当前第1—6轮 | 建议第1—6轮 | 六轮内最高意图伤害:当前→建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| 暴徒+刺客+斧手 | 22、15、32、15、28、22 | 16、11、23、11、20、16 | 32→23 |
|
||||
| 暴徒+刺客+弩手 | 17、24、20、24、23、24 | 12、17、14、17、16、17 | 24→17 |
|
||||
| 暴徒+刺客+追踪手 | 17、18、28、18、31、18 | 12、13、20、13、22、13 | 31→22 |
|
||||
| 暴徒+斧手+弩手 | 12、19、22、19、18、26 | 9、14、16、14、13、19 | 26→19 |
|
||||
| 暴徒+斧手+追踪手 | 12、13、30、13、26、20 | 9、10、22、10、19、15 | 30→22 |
|
||||
| 暴徒+弩手+追踪手 | 7、22、18、22、21、22 | 5、16、13、16、15、16 | 22→16 |
|
||||
| 刺客+斧手+弩手 | 15、29、22、29、15、36 | 11、21、16、21、11、26 | 36→26 |
|
||||
| 刺客+斧手+追踪手 | 15、23、30、23、23、30 | 11、17、22、17、17、22 | 30→22 |
|
||||
| 刺客+弩手+追踪手 | 10、32、18、32、18、32 | 7、23、13、23、13、23 | 32→23 |
|
||||
| 斧手+弩手+追踪手 | 5、27、20、27、13、34 | 4、20、15、20、10、25 | 34→25 |
|
||||
|
||||
这张表说明建议值仍需要集火条件:例如“刺客+斧手+弩手”第6轮仍有26,“斧手+弩手+追踪手”仍有25。16点只是**追加杀的准入预算**,不是整轮伤害硬上限。对应超预算轮不追加杀;不能把这些组合标为已经平衡。
|
||||
|
||||
首轮实测优先检查第2次敌方行动前能否用正常手牌击杀刺客或弩手,并记录为此放弃的护甲和响应。若必须依赖极优起手或稀有卡,先降低相关成员伤害或延后组合开放,再复测;若常规集火已能稳定减员,则保留后续高压,让拖延具有明确代价。跟随追踪手的脆弱会使防御收益下降,仍须按实际逐张护甲向下取整,不能把上表直接减一个固定防御值就当成实战结果。
|
||||
|
||||
### 7.3 精英建议
|
||||
|
||||
| 精英 | 建议HP | 意图与机制调整 | 检验的能力 |
|
||||
| --- | --- | --- | --- |
|
||||
| 多尼斯异鸟 | 74—78 | 单体俯冲17→14;全体3×3保留;每回合+1力保留 | 前6次14、12、16、18、18、24,要求及时结束战斗或减力量/施虚弱 |
|
||||
| 旧日雕像 | 108 | 沉睡、苏醒均无额外主动攻击;苏醒加10力→5;斩击基础13→11,即稳定单体16;缓慢机制保留 | 两轮准备后持续16,考验铺场和同回合多牌增伤 |
|
||||
| 异蛙寄生虫 | 本体58—60;4虫各12—14 | 单体塞2感染与全体3×4交替;死亡仍生成4虫并各跳过一次完整回合;虫单体4与塞1感染/加1力错位循环 | 总生命106—116,考验在感染污染前杀本体并保留清场资源 |
|
||||
|
||||
异蛙的4只虫是第二阶段战斗,不是奖励动画。不能在击杀本体时提前结算胜利;也不能按同族信徒的“杀首领即可赢”规则处理它。
|
||||
|
||||
## 8. 三类 Boss 的 V0.2 方案
|
||||
|
||||
### 8.1 墨影幻灵:多段破盾与伤口管理
|
||||
|
||||
| 参数 | 当前 → 建议 |
|
||||
| --- | --- |
|
||||
| HP | 173 → 150 |
|
||||
| 初始滑溜 | 9 → 4;只初始化一次,按实际失血事件消耗 |
|
||||
| 墨迹 | 7不变,单体刘备;允许预告追加1杀 |
|
||||
| 墨水长枪 | 6×2 → 5×2,明确全体 |
|
||||
| 肢解 | 26 → 18,单体刘备;伤口3→2,只加入被攻击且存活的目标个人弃牌堆 |
|
||||
| 准备 | +2力→+1力,无主动出牌 |
|
||||
| 循环 | 墨迹→长枪→肢解→准备→循环 |
|
||||
|
||||
建议版前8次意图:**7、10、18、0、8、12、19、0**,合计74。假设两次墨迹轮都合法追加一张杀且命中,杀分别为6、7,则刘备总压力为**13、10、18、0、15、12、19、0**,合计87。关张只承受两次全体长枪,共22,不再完整复制刘备全部87点压力。
|
||||
|
||||
用“每轮恰有10护甲”的纯算术情景举例,刘备净损失为3、0、8、0、5、2、9、0,共27。没有把准备轮多余护甲挪到其他轮,也没有算战后回复。**此情景不证明2张摸牌、3费用能每轮同时打出10护甲与足够攻击,必须实战核对。**
|
||||
|
||||
滑溜会使前4次真正掉血的事件每次最多掉1;不等于加4生命。若全是单段6伤害且全部命中、没有其他效果,150血+4滑溜需要4次各1伤害,再25次6伤害,共29次命中;同口径当前173血+9滑溜需37次。多段、非攻击伤害和响应会改变这个比较。
|
||||
|
||||
### 8.2 仪式兽:明确的转阶段窗口
|
||||
|
||||
| 参数 | 当前 → 建议 |
|
||||
| --- | --- |
|
||||
| HP | 252 → 180 |
|
||||
| 转阶段阈值 | 150/252比例 → 明确108/180,即60%;覆写最大生命时按60%取整 |
|
||||
| 一阶段 | 首次跺地设置阈值;之后单体14伤害并+1力,替代18并+2力 |
|
||||
| 破阶段 | 保留符合条件伤害触发、力量归零、跳过一次意图与出牌 |
|
||||
| 二阶段咆哮 | 对全体施加昏眩,下个自身回合只有一次主动行动;原生响应保留 |
|
||||
| 二阶段踩踏 | 15 → 10,全体 |
|
||||
| 二阶段碾碎 | 17/加3力 → 14/加1力,单体刘备 |
|
||||
| 二阶段循环 | 咆哮→踩踏→碾碎→循环;无额外主动出牌 |
|
||||
|
||||
**示例时间线,前提为玩家在第4个刘备行动中把Boss打到108或以下并成功触发转换:**
|
||||
|
||||
| 敌方行动轮 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 行动 | 跺地 | 冲撞 | 冲撞 | 阶段击晕 | 咆哮 | 踩踏 | 碾碎 | 咆哮 | 踩踏 | 碾碎 |
|
||||
| 对刘备意图伤害 | 0 | 14 | 15 | 0 | 0 | 10 | 14 | 0 | 11 | 15 |
|
||||
|
||||
第4轮转阶段需要此前累计造成至少72实际伤害,平均约18/刘备行动;180生命希望在约8—10个玩家回合结束,需要整场实际输出约18—23/回合。它是检验牌组的目标,不是当前实测DPS。若第4轮没打到阈值,后续仍是16、17等一阶段冲撞,不能继续使用上表的安全窗口。
|
||||
|
||||
以每个危险轮8护甲示例,上表净损失合计31。昏眩后能否凑出8护甲尤其需要验证:一张未强化防御只有5,不能假设照常出两张牌。关张只承受表中两次全体踩踏,合计21;咆哮依然降低其有效输出。
|
||||
|
||||
### 8.3 同族小队:清随从与抢杀首领都应有价值
|
||||
|
||||
| 参数 | 当前 → 建议 |
|
||||
| --- | --- |
|
||||
| 神官HP | 190 → 140 |
|
||||
| 两名信徒HP | 各58—59 → 各24—28 |
|
||||
| 神官循环 | 单体6+1脆弱→单体6+1虚弱→全体3×2→自身+1力 |
|
||||
| 信徒循环 | 单体4→单体2×2→自身+1力;两名仍同步起手 |
|
||||
| 追加主动出牌 | 无;保留原生响应与濒死救援 |
|
||||
| 胜利 | 杀神官获胜,仍不强制杀信徒;无新增召唤或复活 |
|
||||
|
||||
全员存活时,前8轮对刘备意图为**14、14、6、10、19、7、20、16**,合计106;第11轮仍可达30。它仍是一个要求减员的Boss,不能宣称全部放着不管也已平衡。
|
||||
|
||||
若第3次敌方行动前已经杀掉一名信徒,且另一名始终存活,前8轮变为**14、14、6、5、13、7、14、10**,合计83。若每轮基础护甲8,受第1/5轮脆弱影响,第2/6轮只能获得6护甲,净损失为6、8、0、0、5、1、6、2,共28。神官第2轮施加虚弱还会降低随后玩家输出,此处没有把它忽略成免费的伤害窗口。
|
||||
|
||||
清一名随从约多花24—28点实际输出,却持续降低后续压力;抢杀神官只需处理140主体生命,但必须承担两名信徒全部攻击。建议以“第2轮能否杀掉一名信徒、杀后能否在第8轮附近结束”作为首轮验算,不把190+两只59全部当成强制击杀量。
|
||||
|
||||
## 9. 回合目标、牌组供给和跨关消耗
|
||||
|
||||
### 9.1 暂定的测试目标
|
||||
|
||||
“毛伤害”指实际扣血总量,尚未加回战中治疗及战后回复;它不同于意图原始伤害。
|
||||
|
||||
| 遭遇 | 暂定玩家回合数 | 对熟悉规则的代表牌组,刘备毛伤害目标 | 检查重点 |
|
||||
| --- | --- | --- | --- |
|
||||
| 首关 | 3—5 | 0—8 | 距离、坐骑、四回合剧情支援、提前胜利 |
|
||||
| 弱池 | 3—5 | 4—10 | 不依赖稀有卡,单人也能通过 |
|
||||
| 强池 | 4—6 | 8—16 | 组合优先击杀、应对状态牌 |
|
||||
| 精英 | 5—8 | 14—24 | 有合理构筑后通过,失败能归因于战术或构筑缺口 |
|
||||
| Boss | 7—10 | 20—32 | 约40以上生命进入时存在合理容错;各Boss按独立机制检验 |
|
||||
|
||||
这些区间是设计目标,不是对全部种子、全部操作或未成型牌组的保证。第4次普通胜利后也不能机械把敌人整体再加血来满足回合数。
|
||||
|
||||
### 9.2 同时满足防御和输出
|
||||
|
||||
不能分别证明“能打20伤害”和“能有10护甲”就认定该回合可行;必须使用同一手牌、同一费用和同一响应消耗验证。
|
||||
|
||||
- 3费用中用2张基础防御取得10护甲后,只剩1费用,基础杀只能打6且可能被闪。
|
||||
- 每轮摸2、持续打出3张还要留闪,长期会缺牌。仁德给一摸一不增加刘备净手牌;助战也要花自己的费用用收到的牌。
|
||||
- 12张初始牌库的空转周期不等于固定6轮:起手、无中、保留手牌、响应、弃牌、消耗、状态牌都会改变它。
|
||||
- Boss前代表牌组至少分别验证:普通单体攻击构筑、易伤/力量构筑、消耗/多段构筑。不能只用一套极优稀有牌组来调全部Boss。
|
||||
- 若多数代表牌组付出一费防御后,仍常常无法形成有效进攻,单独比较“刘备每回合摸3”实验;先冻结其他数值,避免一起加摸牌、加费用、减敌血。
|
||||
- 昏眩、伤口、感染和缠结专门记录损失了多少有效出牌机会;不能都折成一个固定伤害系数。
|
||||
|
||||
### 9.3 续航与濒死
|
||||
|
||||
每场净变化按“入场生命+实际战中治疗-毛伤害+可生效的战后回复”计算,并受生命上限限制;阵亡时不能用战后6点逆转失败。
|
||||
|
||||
例如强池毛伤害12、没有战中治疗、战后实际回6,净耗6;连续4场约耗24,再考虑休息、商店和路线。休息由8提高至15的候选值应在这个背景下评估,而非仅与桃比较。
|
||||
|
||||
助战一旦濒死,当前救助取得途径尚不完整,必须测试缺席后能否继续推进。V0.2先用明确单体目标减少随机永久失去伙伴的风险;救助渠道作为后续独立设计项,不把未存在的免费复活计入生存预算。
|
||||
|
||||
## 10. 落地次序与验证要求
|
||||
|
||||
本次只交付方案。若后续实施,建议使用以下顺序,每阶段保留同种子对照,不把V0.2整包数值一次性落地。
|
||||
|
||||
1. **当前版本基线。** 记录实际入场HP、座次、起手、每次意图、力量、状态持续、主动牌、响应、护甲和净掉血;特别覆盖旧版墨影第3轮、同族第2/5/11轮、仪式兽转换和异蛙亡语。
|
||||
2. **先改威胁可读性与规则。** 实施单体/全体目标、受限且可预告的追加杀,冻结HP与其他伤害;确认没有隐藏的额外主动攻击,并观察仅此改动降低多少压力。
|
||||
3. **按遭遇逐个试建议HP与循环。** 先弱池,再强池组合,再精英,最后三个Boss。若第2阶段已经达到目标,相关降血/降伤可撤回,不机械执行全表。
|
||||
4. **再试刘备与助战。** 确定到场、关张差异、仁德和初始牌组分别记录效果;先保留助战原资源,确认是否确实需要削弱。
|
||||
5. **最后处理跨关续航及强构筑。** 单独试休息30%,再检查炽焰、扯碎及多段/消耗连锁,不用削核心牌补偿未处理的怪物问题。
|
||||
|
||||
建议首轮使用12组固定起始牌序/遭遇随机种子覆盖代表场景,不把同一随机种子的重复点击当独立样本。普通与精英各覆盖单独刘备、关羽、张飞;Boss另交叉3种代表牌组、至少2档入场生命(30/45)。无法一次完成时,先完成三个Boss及最高压群怪,不以少量样本发布精确胜率。
|
||||
|
||||
| 核对项 | 需要的记录/判据 |
|
||||
| --- | --- |
|
||||
| 循环正确 | 至少两圈;同族至少12轮包络;仪式兽分别覆盖早、正常、晚转换 |
|
||||
| 伤害正确 | 按目标、按段、按当时力量及状态重算;示例中的53、44只是特定情景而非固定伤害 |
|
||||
| 防御可行 | 同一手牌与费用同时满足防御和攻击;护甲不能跨自身回合误累计 |
|
||||
| 群怪峰值 | 列出10种劫掠组合、三墨宝随机分支、双小啃兽错位、四史莱姆、飞蝇组合 |
|
||||
| 实际减员 | 第一名敌人何时死、何时减少意图压力;不得只保留永远全员存活的模型 |
|
||||
| 助战安全 | 被全体技能命中的轮次、是否在预告之外濒死、AI是否会用收到的牌 |
|
||||
| 特殊机制 | 滑溜消耗、感染每张3伤害、亡语4虫、神官与雾菇胜利条件、击晕与昏眩区别 |
|
||||
| 预告准确 | 显示目标与段数,额外杀单列;玩家改状态、夺牌或击杀后刷新;不给未锁定的新抽牌追加攻击 |
|
||||
| 保存与兼容 | 旧存档现有牌组不强制重写;新数值作用范围明确;改过的规则说明同步 |
|
||||
|
||||
参数数据、手算/脚本推演、隔离行为检查和真实游戏对局是不同证据。只有真实对局才能验证敌方AI、响应选择、抽牌波动、战斗耗时和体验;本版没有宣称这些已经完成。
|
||||
|
||||
## 11. 实施时必须同步的源码位置
|
||||
|
||||
| 事项 | 入口与约束 |
|
||||
| --- | --- |
|
||||
| 敌人HP | [monster.js](../monsters/monster.js)、各 `roll*Hp`、关卡单位覆盖及[角色资料](../content/scenario-characters.js);只改HEALTH可能被子类固定值或入场随机覆盖 |
|
||||
| 意图目标、伤害与追加牌 | [actions.js](../monsters/actions.js)、[intent-effects.js](../battle/intent-effects.js)、[mode.js](../mode.js)及意图预告;不得只改文字 |
|
||||
| 仪式兽阈值 | [beast-intent.js](../monsters/beast-intent.js)中当前比例150/252与阶段状态;新180HP下明确改60%,不能遗漏旧比例 |
|
||||
| 状态牌数量 | [infection-card.js](../cards/infection-card.js)有本体必须3、虫必须1的校验,建议本体2必须连同校验调整;伤口、晕眩同样核对生成入口 |
|
||||
| 助战与仁德 | [definitions.js](../bonds/definitions.js)、[state.js](../bonds/state.js)、[intents.js](../bonds/intents.js)、卡牌所有权与阶段限制;新单体关羽需要执行器支持 |
|
||||
| 早期与后期组合 | [encounters.js](../content/acts/act1/encounters.js)、[state.js](../progression/state.js)、[encounter-progress.js](../progression/encounter-progress.js);按普通胜利次数,不按楼层替代 |
|
||||
| 回复 | [rooms.js](../relics/rooms.js)、[rules.js](../relics/rules.js);先核对战中存活,后核对跨关净消耗 |
|
||||
|
||||
本文是可审阅的联合平衡草案,关键建议是:**限制未预告的额外伤害,逐轮计算全队压力,保留Boss机制差异,并用相同资源约束同时验证输出与生存。**
|
||||
@@ -0,0 +1,98 @@
|
||||
# 梦三统一战斗 HUD
|
||||
|
||||
> 当前范围以末尾“77.png/78.png”一节为准:仅替换玩家下方 HUD,采用完全透明布局;怪物、助战和战场出牌副本保持原生呈现。
|
||||
|
||||
用户已确认统一下方 HUD,并要求支持鼠标/触屏拖牌、多个敌人;不依赖十周年 UI,但兼容十周年 UI。本轮直接修改实际文件,保留其他已有修改,不创建修复副本、不提交或发布。
|
||||
|
||||
## 目标与边界
|
||||
|
||||
- 左侧真实玩家头像与生命/格挡/费用,中央状态条和原有真实手牌,右侧原生技能、确认、取消与结束控件。
|
||||
- 保持卡牌父容器、角色节点、控件回调及原生事件结算;不创建另一套技能执行或出牌结算。
|
||||
- 敌人与助战按现有席位身份动态布局;意图在上方,状态在下方,保留最多八席的原有校验。
|
||||
- 读取现有效果;不启用 Wiki 尚未实现的状态,不改变触发优先级、存档或数值。
|
||||
- 独立实例(炸弹、未来回合效果)分别显示,强度、回合、次数与阈值分别说明。
|
||||
- 十周年未启用时必须完整工作;启用时只适配其当前真实节点,不导入其接口或资源。
|
||||
|
||||
## 实施计划
|
||||
|
||||
1. [x] 建立会话隔离的只读状态来源;接入刘备基础/进阶、通用牌及遗物状态。先验证负值、独立实例及旧会话清理。
|
||||
2. [x] 新增 HUD 模块、样式、状态资源;保留原生手牌/控件父级,管理会话挂载与恢复。
|
||||
3. [x] 接入战斗入口与召唤刷新,适配原生和十周年控件、拖牌遮罩及多角色布局。
|
||||
4. [x] 用浏览器隔离场景检查原生/十周年样式、动态控件、拖牌、七个敌人、状态变化与重复卸载;结合源码核对并修复触屏误选、缓慢层数及遗物条裁切问题。
|
||||
|
||||
## 检查重点
|
||||
|
||||
技能进入/退出时控件重建;拖牌期间原生确认隐藏;触屏 click/touch 双触发;目标死亡/召唤后的真实命中区域;保存后换战的监听与样式清理。
|
||||
|
||||
静态、模块与浏览器隔离检查不等同于实际游戏验收。最终须在原生 UI 和实际十周年风格下分别验证完整对局。
|
||||
|
||||
## 进度
|
||||
|
||||
已检查既有修改;按本会话已确认方案直接实施。采用当前工作区,不另建工作树;不执行自动提交。
|
||||
|
||||
新增 `battle/status-sources.js` 管理会话级只读来源,`ui/hud/` 负责显示和布局。刘备基础/进阶与通用牌模块只增加显示读取注册;遗物控制器只增加状态快照,不更改结算。图标从本机 v0.111.0 切片复制60张,共270585字节,尺寸/字节/哈希见 `assets/status/manifest.json`。
|
||||
|
||||
原生 UI 与十周年三种实际 CSS 的隔离场景已覆盖1280×720、812×375、540×900、七个敌人、原节点父级保持、鼠标拖牌、触屏 PointerEvent 拖牌、十周年独立技能控件回调、状态详情和重复卸载。上述均为隔离检查,未运行完整游戏和真实安卓客户端。
|
||||
|
||||
检查结果:4项状态模型测试、270项浏览器隔离断言、13个模块语法检查通过;60张图标清单与本机切片一致。拖拽继续使用原有单目标/无目标出牌适配,复杂技能、多次选牌和特殊目标流程沿用原生选择控件。
|
||||
|
||||
显示顺序目前是稳定的状态定义/来源顺序,不宣称等同于原版的施加/触发顺序。未知原生标记继续保留;只有已由HUD展示的对应标记才隐藏。
|
||||
|
||||
## 70.png 反馈修正(2026-10-22)
|
||||
|
||||
截图暴露了上一版隔离场景未覆盖的实际结构:十周年将 `ui.control.id` 改为 `dui-controls`,人物姓名/生命位于 `camp-wrap`、`hp-wrap` 内,人物尺寸及手机 `stayleft` 操作组还有高优先级规则。原来的直接子节点选择器未覆盖这些结构,造成皮肤边框、重复数值与操作按钮错位。
|
||||
|
||||
- 下方改为通透渐隐背景,仅左侧人物信息使用紧凑墨蓝面板;人物真实头像保持原节点,移除由 HUD 替代的姓名、生命和身份装饰显示,保留未知标记及装备交互。
|
||||
- 真实操作容器按引用定位,不依赖容器 ID;原生操作组和可选技能/全局技能列分别安排空间。覆盖手机 `stayleft` 横移与旧布局过渡,恢复手杀透明渐变字体的可见性,并保留技能选中/不可用提示。
|
||||
- 遗物条按内容收缩;无状态时隐藏空标题和敌人空状态框;真实手牌上移,减少桌面底边裁切。敌人按数量在可用战场空间内排布,独立显示姓名、生命与格挡。
|
||||
- 检查扩充为原生与三种十周年风格、四组尺寸(含1234×920),使用实际人物 CSS、嵌套人物结构、重命名按钮容器及手机样式,覆盖可选全局技能和技能文字可见性。
|
||||
|
||||
本次508项浏览器隔离断言、4项状态模型测试及修改模块语法/差异检查通过。已查看渲染截图;仍未在实际游戏进程或真实安卓客户端验收。游戏需重启以加载修改后的模块。
|
||||
|
||||
## 71.png 与原方案对照(2026-10-22)
|
||||
|
||||
用户明确仍要求之前确认的统一下方HUD。上一轮实际做成浮动人物卡、手牌和操作列,虽修正了部分皮肤冲突,却偏离原示意中共用底座的视觉关系。本轮对照原 `mengsan-hud-options.html` 第一节恢复统一底座、三栏分隔、固定状态行与完整手牌,保留真实节点及事件;单个敌人使用更大尺寸,多敌人继续动态分行。
|
||||
|
||||
十周年 `playerDamagepop` 将字符串通知放在 `player.offsetTop + 80`,缩小后的头像会让“回合开始”等通知覆盖底部信息。本轮只为当前HUD中的原生 `.damage.skill-popup` 加显示类并把位置限制在底座上方,保留文字动画与原有自动移除,退出时恢复。
|
||||
|
||||
新增检查覆盖空状态行稳定占位、旋转手牌底边、单敌人尺寸、通知位置及卸载恢复;仍属浏览器隔离验证。最终视觉需以重新启动后的实际游戏截图确认,不以检查数量作为美观验收。
|
||||
|
||||
只读评审发现长遗物条会与首排意图相交,新增十遗物场景先复现失败,再为战场顶部独立预留空间。多单位布局将意图、头像、状态作为整体分配高度;短屏按每单位至少64像素确定列数,头像与标签都受单元格限宽。补查667×375/七名敌人和375×500/三名敌人边界,复核确认原遮挡及头像重叠问题已消除。最终871项浏览器隔离断言、修改模块语法与差异检查通过;仍未进行实机验收。
|
||||
|
||||
## 72.png 细节与遗物位置(2026-10-22)
|
||||
|
||||
用户要求保留统一底座,修正头像被装备覆盖的问题,并将遗物移到屏幕中上方以避开左上回合数。装备原节点保持父级、回调和选取机制,移到头像下方的24像素紧凑横滚行;玩家装备放在底座底部,敌人装备与状态分别预留空间。额外适配十周年把同一装备根节点置于独立 `ui.equipSolts` 包装内的情况,不重建或搬移卡牌。遗物条改上方居中、限制最大宽度,过多时横向滚动。
|
||||
|
||||
测试加入真实装备CSS、五件装备首尾点击、装备与头像/状态互不遮挡、独立装备包装和卸载恢复,以及遗物居中和避开回合区域。先复现装备遮头像;独立包装的旧 `justify-content:flex-end` 使首件装备溢出且不可滚回,已显式改为起始排列。最终1087项浏览器隔离断言及修改模块语法/差异检查通过,未在实际游戏或安卓客户端验收。
|
||||
|
||||
## 73.png 现代风格、透明度与阵亡拥挤(2026-10-22)
|
||||
|
||||
按用户要求使用 UI/UX 技能评审后,保留统一下方HUD,改为内缩8像素、24像素圆角、76%背景不透明度与10像素轻度模糊的底座。文字不整体降透明度;头像、状态、按钮、遗物条与详情面板采用一致的圆角层级,弱化分隔线和装备滚动条,扩大桌面栏间留白。原生控件按真实引用清除十周年高优先级的15像素侧边距,并包含按钮内边距/边框,修正右侧圆角裁切。
|
||||
|
||||
阵亡角色保留真实节点和引擎登记,只隐藏其战场呈现;复活时恢复,避免存留尸体覆盖重排后的角色。普通出牌副本在空间足够时缩成最近两张,独立放在战场下沿;小屏或空间不足时隐藏重复副本。新增 `ui/hud/played-cards.js` 仅修改可恢复的独立translate/scale,不覆盖原生transform、不移除卡牌、不更改结算或原生过渡完成回调;判定/比拼与固定、可选、抽牌/获得牌副本沿用原生呈现。
|
||||
|
||||
只读评审指出固定预留128像素会压缩812×600七角色的第二排信息,新增场景先复现,再改为优先保障角色及标注空间,余量足够时才显示最近出牌区。最终原生与十周年三种风格、七组尺寸共28组场景、1556项浏览器隔离断言通过,覆盖透明度、圆角、按钮边界、阵亡/复活、判定恢复、原生transform过渡完成、拖牌及退出恢复。已检查桌面阵亡与短屏渲染截图;模块语法及差异检查通过。上述不等于实际游戏或安卓客户端验收,需重启游戏确认最终效果。
|
||||
|
||||
## 标注图、75.png/76.png(2026-10-22)
|
||||
|
||||
用户明确取消深蓝底框,要求完全透明;左下仅玩家立绘,不存在双武将,立绘需略大以容纳标记、装备。按标注将生命、格挡和费用移到立绘右侧竖排,状态移到立绘上方左侧矩阵;提示语放在手牌上方,确认、取消、托管和结束等原生控件在其下横排,原生技能与十周年可选技能控件在最右侧竖排。桌面玩家立绘由80×116放大为144×196,装备仍在原节点的下方横滚行;窄屏采用竖排人物信息。底座无背景色、边框、阴影或模糊,文字保持不透明并用文字阴影辅助辨识。
|
||||
|
||||
75.png 的右键说明被手牌遮住。新增 `ui/hud/overlays.js` 仅调整已有简单提示和原生 `.dialog.popped` 的位置,说明放到手牌和操作提示上方,并限制窗口内边界、允许长内容滚动。保留原有右键入口、内容节点、关闭遮罩和回调;不重建说明,不干预含卡牌/按钮的原生复杂选择框。针对原生绝对定位的内容包装增加局部流式布局,避免自动高度塌缩。
|
||||
|
||||
用户后续收窄范围:怪物界面沿用原版,只替换玩家下方 HUD。已去除本 HUD 的多单位布局、姓名/生命/装备/状态代理、死亡隐藏及最近出牌副本模块;敌人与助战不添加 HUD 类、不改内联位置、不替换节点,也不隐藏它们的原生标记。
|
||||
|
||||
76.png 奖励界面后方恢复旧布局,原因是 `requestBattleFinish` 立即卸载 HUD 和手牌适配。改为先冻结呈现、停交互和监听,保留玩家及扇形手牌外观;最终仍由原会话在奖励完成后统一释放,不改变战斗停止、结算、存档或奖励选择顺序。
|
||||
|
||||
本轮浏览器隔离检查使用实际玩家 HUD、手牌模块和原生/十周年三种风格 CSS,覆盖七组尺寸、状态矩阵溢出、玩家装备回调、怪物节点保持、原生技能独立竖排、鼠标与触屏拖牌、右键说明位置/内容/关闭、提示语与横排操作分离,以及停止后保留外观并最终恢复。最终28组场景、1455项浏览器隔离断言、4项状态模型测试以及修改模块语法和差异检查通过,已复核右键说明与透明底座的渲染截图;此类检查仍不等于实际游戏与安卓客户端验收。
|
||||
|
||||
## 77.png/78.png(2026-10-23)
|
||||
|
||||
玩家信息列与立绘的横向间距缩至 8~16px;常规/横屏立绘下边距由 66px 改为 24px,数值列同步下移。装备使用原节点,在立绘下半部竖向叠放,窄尺寸可滚动;兼容十周年独立装备容器,不迁移节点或替换回调。
|
||||
|
||||
移除“当前状态/暂无状态”和“选择行动/等待行动”占位文字,无状态时隐藏状态区,有状态时自动显示。左侧状态矩阵贴近立绘上方;遗物移至右侧技能区上方,与状态区对应。窄竖屏遗物改为单行横向滚动,并作 16px 对齐补偿以避开原有怪物意图。技能区高度限于遗物下方。
|
||||
|
||||
后续按用户要求移除“技能与装备”按钮及其详情代理分支,删除自制操作按钮的纹理、字体、边框、颜色和禁用态覆盖,并移除已无引用的两张复制纹理。确定/回合结束使用当前已启用的十周年或官方原版控件和样式,保留真实节点、内容、点击回调及原有内边距;HUD 仅安排容器位置,允许原生尺寸向上撑开。信息列改为相对立绘顶部定位,避免移除按钮后数值列下沉。右侧技能区仍按原有 HUD 轨道适配,底部布局保持透明。
|
||||
|
||||
78.png 的原生标记说明层级低于立绘,原生打开函数也不拒绝重复点击。玩家标记捕获阶段仅阻止尚未完成打开或已有说明时的重复打开;由原生事件生成内容,随后将说明移到手牌操作区上方。说明与遮罩分别置于 90/89 层,取消位置和层级过渡,避免短暂遮挡。关闭仍走原生回调,关闭后可重新打开;不改怪物标记。
|
||||
|
||||
已通过原生及十周年三种风格、七种尺寸的 28 组浏览器隔离场景,共 1847 项断言;另有前一轮 4 项状态模型测试、当前修改模块语法检查和差异检查通过。检查覆盖装备在立绘内、信息间距、移除技能装备代理按钮、操作按钮原始节点/内容/外观/字体/内边距保持、原生确认和结束回调、按钮在手牌上方、遗物溢出访问、标记连续点击/层级/位置/关闭后重开。已复核透明布局与原生按钮、标记说明截图;拖牌在原生目标可见区域验证。尚未完成真实游戏或安卓客户端验收。
|
||||
@@ -73,6 +73,7 @@ import { isFogmog, isToothedEye, isFogmogActor, isEncounterEnemy,
|
||||
recordFogmogAction } from "./monsters/fogmog-intent.js";
|
||||
import { mountEnergy } from "./ui/energy-display.js";
|
||||
import { mountHandUI } from "./ui/hand/index.js";
|
||||
import { mountBattleHUD } from "./ui/hud/index.js";
|
||||
import {
|
||||
finishDeathBlow,
|
||||
hasPendingDeathBlow,
|
||||
@@ -715,7 +716,12 @@ const prepareBattle = async (run, node, encounter, session, resources) => {
|
||||
}
|
||||
}
|
||||
activeBattle.pilesUI = mountBattlePiles(session, _status.mengsanBattle_shuying);
|
||||
currentBattle.relicUI = mountRelics(session, run, currentBattle.relics);
|
||||
currentBattle.owner = me;
|
||||
currentBattle.hud = mountBattleHUD(me, currentBattle, {
|
||||
game, ui, get, lib, _status, document, window, assetBase: STYLE_PATH,
|
||||
log: message => game.log(message),
|
||||
});
|
||||
currentBattle.relicUI = mountRelics(session, run, currentBattle.relics, { stripRoot: ui.arena });
|
||||
activeBattle.gmUI = mountGMManager(session, me, activeBattle.personalPiles, _status.mengsanBattle_shuying, resources, game);
|
||||
for (let index = 0; index < plan.units.length; index++) await equipBattleUnit(participants[index], plan.units[index], resources);
|
||||
game.syncState();
|
||||
@@ -836,12 +842,15 @@ const requestBattleFinish = outcome => {
|
||||
recordBondDeath(player, _status.mengsanRun_shuying,
|
||||
message => game.log(message));
|
||||
}
|
||||
// Capture the final HUD state while its battle session is still active.
|
||||
current.hud?.refresh();
|
||||
const accepted = current.flow.requestFinish({session:current.session, hp:game.me?.hp || 0, outcome, defeatedEnemies:current.director?.defeatedEnemies ?? 1});
|
||||
if (accepted) {
|
||||
current.pilesUI?.dispose();
|
||||
current.gmUI?.dispose();
|
||||
current.relicUI?.dispose();
|
||||
current.handUI?.dispose();
|
||||
current.handUI?.freeze();
|
||||
current.hud?.freeze();
|
||||
stopBattleTurn(current.session, _status.eventManager);
|
||||
if (_status.mengsanBattle_shuying) _status.mengsanBattle_shuying.resolving = true;
|
||||
current.finished();
|
||||
@@ -920,6 +929,7 @@ const playBattle = async (run, node, encounter) => {
|
||||
if (current.engineFailed) {
|
||||
reportFlowError(current.engineError);
|
||||
session.requestStop();
|
||||
current.hud?.dispose();
|
||||
current.handUI?.dispose();
|
||||
document.body.classList.remove("mengsan-battle-ui-shuying");
|
||||
const reason = current.engineError?.message || String(current.engineError);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
| 遗忘之魂 | lost_soul.png | forgotten_soul.png |
|
||||
| 布质果实 | looming_fruit_2.png | looming_fruit.png |
|
||||
|
||||
美味饼干的另外四种角色图仍保留在源图集;当前梦三默认只映射刘备/铁甲战士。
|
||||
美味饼干的另外四种角色图仍保留在源图集;当前梦三默认只接入刘备对应版本。
|
||||
束带是既有梦三道具,不属于Wiki的298件遗物,无对应图集图片,继续文字展示。
|
||||
|
||||
战斗遗物栏、背包、选遗物、商店、战利品清单使用这些图片;加载错误保留名称和描述。
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- [遗物查找页](https://sts2.huijiwiki.com/wiki/模板:查找/遗物);[结构化数据页](https://sts2.huijiwiki.com/wiki/Data:Relic.tabx),快照修订号 `28579`。
|
||||
- `relics/catalog.js` 保存池、原始稀有度、先古归属、图鉴顺序、中文名、可读描述、原始描述/原始 Wiki 标记、风味文字、前代及来源链接。
|
||||
- 298 个唯一 Wiki ID;图鉴顺序完整覆盖 1—298。头环原始稀有度仍为“遗物”;先古归属与稀有度分别保存。
|
||||
- 遗物池:铁甲战士 9;静默猎手 9;储君 9;亡灵契约师 9;故障机器人 9;通用 253。
|
||||
- 遗物池:刘备 9;静默猎手 9;储君 9;亡灵契约师 9;故障机器人 9;通用 253。
|
||||
- 稀有度:初始 10;普通 30;罕见 40;稀有 50;商店 30;先古之民 102;事件 35;遗物 1。
|
||||
- 先古归属:无 196;涅奥 30;欧洛巴斯 10;佩尔 10;特兹卡塔拉 10;诺奴佩普 10;坦克斯 10;瓦库 10;达弗 12。
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
来源为用户提供的 `assets/sts2/Godot_Atlas_Sprites_v0.111.0/images/atlases/relic_atlas.sprites/` 拆分 PNG 目录。运行时使用 `assets/relics/<wikiId>.png`,逐字节复制,不拉伸、不重新绘图。所有图片都在 480×672、300 KiB 以内。
|
||||
298 张图片合计 **2,173,564 字节**。每张的源文件名、尺寸、透明通道、体积、SHA-256 见 [资源清单](../assets/relics/manifest.json)。
|
||||
|
||||
特殊映射:美味饼干使用 `yummy_cookie_ironclad.png`;遗忘之魂使用 `lost_soul.png`;布质果实使用 `looming_fruit_2.png`。美味饼干的其他角色图仍在原图集,当前仅接入铁甲战士图。原梦三束带没有对应 STS2 原图,继续显示文字。
|
||||
特殊映射:美味饼干使用 `yummy_cookie_ironclad.png`;遗忘之魂使用 `lost_soul.png`;布质果实使用 `looming_fruit_2.png`。美味饼干的其他角色图仍在原图集,当前仅接入刘备图。原梦三束带没有对应 STS2 原图,继续显示文字。
|
||||
战斗遗物栏、地图背包、遗物选择、商店和战利品清单已经接入图片;池/稀有度/先古归属可从遗物栏与背包查看。
|
||||
|
||||
## 效果与获取接线
|
||||
@@ -34,7 +34,7 @@
|
||||
- 商店保留【杀】与回复生命,新增3个遗物货位、折扣与信使补货。遗物基价暂设120金币;不是 Wiki 公布的商品价格。购买后完成所有遗物选牌,再提交房间结果。
|
||||
- 普通/罕见/稀有遗物池登记 **81** 项;可售遗物池登记 **91** 项,均在实际获取时继续过滤重复与角色池。
|
||||
- 原有精英固定遗物奖励、第一章宝箱和初次剧情遗物选择自动使用扩展后的可用池。Boss 极品卡牌池保留原有专属稀有卡约定。
|
||||
- 新建刘备征程自动持有燃烧之血,符合铁甲战士初始池;既有存档不追补、不重复发放初始遗物。其他武将暂不指定初始遗物。
|
||||
- 新建刘备征程自动持有燃烧之血,符合刘备初始池;既有存档不追补、不重复发放初始遗物。其他武将暂不指定初始遗物。
|
||||
- 先古/事件/初始遗物已提供独立奖励池;**尚未创建对应先古会面和事件剧情**,不会把这些条目混入普通掉落。后续节点可以引用下表池名或调用已有 `grantRelic`。
|
||||
- 拾取操作在可序列化副本上计算,成功后替换运行状态;尚未实现的遗物/重复获取不会改金币、随机数或牌组。战后效果在现有结算候选里只计算一次,存档失败重试不重新发奖。
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
1. **烹饪数值与流程**:网页只说明能够烹饪,目前采用最大生命+5。确定原作具体数值后可替换适配值。
|
||||
2. **非战斗生命归零流程**:危险剪刀/芬芳蘑菇当前扣血保留至少1生命。需要明确是否允许地图中直接死亡,以及对应失败弹窗/存档规则。
|
||||
3. **先古会面及事件入口**:请确定梦三在哪些章节、节点或剧情中接入各先古和事件遗物。已有数据分类、效果、专用奖励池,但尚无原作的完整会面事件。
|
||||
4. **角色池映射**:当前只把刘备映射为铁甲战士,其他四个 STS2 角色未绑定梦三武将;不擅自指定。
|
||||
4. **角色池映射**:当前仅接入刘备专属角色池,其他四个 STS2 角色未绑定梦三武将;不擅自指定。
|
||||
5. **原作机制**:辉星、充能球、集中、奥斯提、灾厄、仆从、铸造、可使用药水,以及 Wiki 指定附魔,需要对应玩法接口;完整逐件缺口见下表。现有词缀不会冒充原作附魔。
|
||||
6. **先古牌与击杀口径**:尘封魔典当前抽取现有先古事件牌;石之剑按精英战胜利次数计数,需要完整原作候选表及精英单位身份口径。
|
||||
7. **实机验收**:需要用户回来在实际梦三运行以下场景;此次不在正在进行的征程上自动操纵或替换存档。
|
||||
@@ -95,7 +95,7 @@
|
||||
- **微型帐篷**:每个休息选项可各选择一次,直至选择离开;不允许无限重复同一选项。
|
||||
- **切肉刀**:网页没有烹饪细则;暂定每次烹饪最大生命+5,待用户确认。
|
||||
- **大蘑菇**:沿用梦三初始手牌4张,扣减2张;未改成STS2的基础手牌数量。
|
||||
- **长蛇戒指**:刘备仅映射铁甲战士池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。
|
||||
- **长蛇戒指**:刘备默认使用专属遗物池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。
|
||||
- **符文金字塔**:梦三原本按手牌上限弃牌;这里让全部手牌不计入弃牌上限。虚无牌照常消耗。
|
||||
- **松动羊毛剪**:选牌后扣除16点生命;当前地图流程保留至少1点生命,待补充非战斗死亡结算。
|
||||
- **芳香蘑菇**:随机强化2张并扣除15点生命;当前地图流程保留至少1点生命,待补充非战斗死亡结算。
|
||||
@@ -111,13 +111,13 @@
|
||||
|
||||
| 序号 | 遗物 / Wiki ID | 池 | 稀有度 | 先古归属 | 原文效果 | 状态 | 执行 / 缺口 | 来源图片 |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | [燃烧之血](https://sts2.huijiwiki.com/wiki/%E7%87%83%E7%83%A7%E4%B9%8B%E8%A1%80)<br>`burning_blood` | 铁甲战士 | 初始 | — | 在战斗结束时,回复6点生命。 | 已接入,待实机 | battleHeal=6 | `burning_blood.png`<br>71×81;6202 B |
|
||||
| 1 | [燃烧之血](https://sts2.huijiwiki.com/wiki/%E7%87%83%E7%83%A7%E4%B9%8B%E8%A1%80)<br>`burning_blood` | 刘备 | 初始 | — | 在战斗结束时,回复6点生命。 | 已接入,待实机 | battleHeal=6 | `burning_blood.png`<br>71×81;6202 B |
|
||||
| 2 | [蛇之戒指](https://sts2.huijiwiki.com/wiki/%E8%9B%87%E4%B9%8B%E6%88%92%E6%8C%87)<br>`ring_of_the_snake` | 静默猎手 | 初始 | — | 在每场战斗开始时,额外抽2张牌。 | 已接入,待实机 | openingDraw=2 | `ring_of_the_snake.png`<br>81×81;9868 B |
|
||||
| 3 | [天赋君权](https://sts2.huijiwiki.com/wiki/%E5%A4%A9%E8%B5%8B%E5%90%9B%E6%9D%83)<br>`divine_right` | 储君 | 初始 | — | 在每场战斗开始时,获得[辉星][辉星][辉星]。 | 待补充,禁入奖励 | 辉星系统 | `divine_right.png`<br>71×81;8739 B |
|
||||
| 4 | [缚魂命匣](https://sts2.huijiwiki.com/wiki/%E7%BC%9A%E9%AD%82%E5%91%BD%E5%8C%A3)<br>`bound_phylactery` | 亡灵契约师 | 初始 | — | 在你的回合开始时,召唤1 | 待补充,禁入奖励 | 奥斯提召唤系统 | `bound_phylactery.png`<br>77×82;8874 B |
|
||||
| 5 | [破损核心](https://sts2.huijiwiki.com/wiki/%E7%A0%B4%E6%8D%9F%E6%A0%B8%E5%BF%83)<br>`cracked_core` | 故障机器人 | 初始 | — | 在每场战斗开始时,生成1个闪电充能球。 | 待补充,禁入奖励 | 充能球系统 | `cracked_core.png`<br>79×78;8536 B |
|
||||
| 6 | [黑暗之血](https://sts2.huijiwiki.com/wiki/%E9%BB%91%E6%9A%97%E4%B9%8B%E8%A1%80)<br>`black_blood` | 铁甲战士 | 初始 | — | 在战斗结束时,回复12点生命。 | 已接入,待实机 | battleHeal=12;replaces="burning_blood" | `black_blood.png`<br>66×79;6334 B |
|
||||
| 7 | [长蛇戒指](https://sts2.huijiwiki.com/wiki/%E9%95%BF%E8%9B%87%E6%88%92%E6%8C%87)<br>`ring_of_the_drake` | 静默猎手 | 初始 | — | 在战斗开始时的前3个回合,你额外抽2张牌。 | 梦三适配,待实机 | 刘备仅映射铁甲战士池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。 | `ring_of_the_drake.png`<br>78×80;7525 B |
|
||||
| 6 | [黑暗之血](https://sts2.huijiwiki.com/wiki/%E9%BB%91%E6%9A%97%E4%B9%8B%E8%A1%80)<br>`black_blood` | 刘备 | 初始 | — | 在战斗结束时,回复12点生命。 | 已接入,待实机 | battleHeal=12;replaces="burning_blood" | `black_blood.png`<br>66×79;6334 B |
|
||||
| 7 | [长蛇戒指](https://sts2.huijiwiki.com/wiki/%E9%95%BF%E8%9B%87%E6%88%92%E6%8C%87)<br>`ring_of_the_drake` | 静默猎手 | 初始 | — | 在战斗开始时的前3个回合,你额外抽2张牌。 | 梦三适配,待实机 | 刘备默认使用专属遗物池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。 | `ring_of_the_drake.png`<br>78×80;7525 B |
|
||||
| 8 | [天命所归](https://sts2.huijiwiki.com/wiki/%E5%A4%A9%E5%91%BD%E6%89%80%E5%BD%92)<br>`divine_destiny` | 储君 | 初始 | — | 在每场战斗开始时,获得[辉星][辉星][辉星][辉星][辉星][辉星]。 | 待补充,禁入奖励 | 辉星系统 | `divine_destiny.png`<br>71×81;8401 B |
|
||||
| 9 | [无界命匣](https://sts2.huijiwiki.com/wiki/%E6%97%A0%E7%95%8C%E5%91%BD%E5%8C%A3)<br>`phylactery_unbound` | 亡灵契约师 | 初始 | — | 在每场战斗开始时,召唤5。在你的回合开始时,召唤2。 | 待补充,禁入奖励 | 奥斯提召唤系统 | `phylactery_unbound.png`<br>76×80;9269 B |
|
||||
| 10 | [注能核心](https://sts2.huijiwiki.com/wiki/%E6%B3%A8%E8%83%BD%E6%A0%B8%E5%BF%83)<br>`infused_core` | 故障机器人 | 初始 | — | 在每场战斗开始时,生成3个闪电充能球。闪电充能球额外造成1点伤害。 | 待补充,禁入奖励 | 充能球系统 | `infused_core.png`<br>80×78;10310 B |
|
||||
@@ -146,7 +146,7 @@
|
||||
| 33 | [战纹涂料](https://sts2.huijiwiki.com/wiki/%E6%88%98%E7%BA%B9%E6%B6%82%E6%96%99)<br>`war_paint` | 通用 | 普通 | — | 拾起时,随机升级2张技能牌。 | 已接入,待实机 | gainUpgrade=2;upgradeType="skill" | `war_paint.png`<br>80×53;7563 B |
|
||||
| 34 | [准备背包](https://sts2.huijiwiki.com/wiki/%E5%87%86%E5%A4%87%E8%83%8C%E5%8C%85)<br>`bag_of_preparation` | 通用 | 普通 | — | 在每场战斗开始时,额外抽2张牌。 | 已接入,待实机 | openingDraw=2 | `bag_of_preparation.png`<br>81×74;8988 B |
|
||||
| 35 | [紫水晶茄子](https://sts2.huijiwiki.com/wiki/%E7%B4%AB%E6%B0%B4%E6%99%B6%E8%8C%84%E5%AD%90)<br>`amethyst_aubergine` | 通用 | 普通 | — | 敌人额外掉落15金币。 | 已接入,待实机 | battleGold=15 | `amethyst_aubergine.png`<br>81×77;7960 B |
|
||||
| 36 | [红头骨](https://sts2.huijiwiki.com/wiki/%E7%BA%A2%E5%A4%B4%E9%AA%A8)<br>`red_skull` | 铁甲战士 | 普通 | — | 当你的生命值低于或等于50%时,你额外获得3点力量 | 已接入,待实机 | lowHpStrength=3 | `red_skull.png`<br>76×81;8046 B |
|
||||
| 36 | [红头骨](https://sts2.huijiwiki.com/wiki/%E7%BA%A2%E5%A4%B4%E9%AA%A8)<br>`red_skull` | 刘备 | 普通 | — | 当你的生命值低于或等于50%时,你额外获得3点力量 | 已接入,待实机 | lowHpStrength=3 | `red_skull.png`<br>76×81;8046 B |
|
||||
| 37 | [异蛇头骨](https://sts2.huijiwiki.com/wiki/%E5%BC%82%E8%9B%87%E5%A4%B4%E9%AA%A8)<br>`snecko_skull` | 静默猎手 | 普通 | — | 每当你给予敌人中毒时,所给予的中毒层数增加1层。 | 待补充,禁入奖励 | 玩家中毒牌与中毒系统 | `snecko_skull.png`<br>80×73;6524 B |
|
||||
| 38 | [击剑指南](https://sts2.huijiwiki.com/wiki/%E5%87%BB%E5%89%91%E6%8C%87%E5%8D%97)<br>`fencing_manual` | 储君 | 普通 | — | 在每场战斗开始时,铸造10。 | 待补充,禁入奖励 | 铸造系统 | `fencing_manual.png`<br>59×80;6554 B |
|
||||
| 39 | [骨笛](https://sts2.huijiwiki.com/wiki/%E9%AA%A8%E7%AC%9B)<br>`bone_flute` | 亡灵契约师 | 普通 | — | 每当奥斯提攻击时,获得2点格挡。 | 待补充,禁入奖励 | 奥斯提攻击系统 | `bone_flute.png`<br>81×78;5418 B |
|
||||
@@ -181,8 +181,8 @@
|
||||
| 68 | [招财异鱼](https://sts2.huijiwiki.com/wiki/%E6%8B%9B%E8%B4%A2%E5%BC%82%E9%B1%BC)<br>`lucky_fysh` | 通用 | 罕见 | — | 每当你将一张卡牌加入你的牌组时,获得15金币。 | 已接入,待实机 | deckAddGold=15 | `lucky_fysh.png`<br>81×83;9458 B |
|
||||
| 69 | [招架盾](https://sts2.huijiwiki.com/wiki/%E6%8B%9B%E6%9E%B6%E7%9B%BE)<br>`parrying_shield` | 通用 | 罕见 | — | 如果你在回合结束时拥有至少10点格挡,则对随机敌人造成6点伤害。 | 已接入,待实机 | endBlockThreshold=10;endBlockDamage=6 | `parrying_shield.png`<br>81×81;8633 B |
|
||||
| 70 | [烛台](https://sts2.huijiwiki.com/wiki/%E7%83%9B%E5%8F%B0)<br>`candelabra` | 通用 | 罕见 | — | 在你的第2回合开始时,获得[能量][能量]。 | 已接入,待实机 | nthTurn=2;nthTurnEnergy=2 | `candelabra.png`<br>81×81;5701 B |
|
||||
| 71 | [纸蛙](https://sts2.huijiwiki.com/wiki/%E7%BA%B8%E8%9B%99)<br>`paper_phrog` | 铁甲战士 | 罕见 | — | 有易伤状态的敌人受到的伤害增加75%而非50%。 | 梦三适配,待实机 | 依梦三易伤口径向上取整;作用于玩家发起的攻击,保留既有卡牌的额外易伤增幅。 | `paper_phrog.png`<br>81×69;5557 B |
|
||||
| 72 | [自成型黏土](https://sts2.huijiwiki.com/wiki/%E8%87%AA%E6%88%90%E5%9E%8B%E9%BB%8F%E5%9C%9F)<br>`self_forming_clay` | 铁甲战士 | 罕见 | — | 每当你在战斗中失去生命,就在下回合获得3点格挡。 | 已接入,待实机 | hpLossNextBlock=3 | `self_forming_clay.png`<br>81×80;7786 B |
|
||||
| 71 | [纸蛙](https://sts2.huijiwiki.com/wiki/%E7%BA%B8%E8%9B%99)<br>`paper_phrog` | 刘备 | 罕见 | — | 有易伤状态的敌人受到的伤害增加75%而非50%。 | 梦三适配,待实机 | 依梦三易伤口径向上取整;作用于玩家发起的攻击,保留既有卡牌的额外易伤增幅。 | `paper_phrog.png`<br>81×69;5557 B |
|
||||
| 72 | [自成型黏土](https://sts2.huijiwiki.com/wiki/%E8%87%AA%E6%88%90%E5%9E%8B%E9%BB%8F%E5%9C%9F)<br>`self_forming_clay` | 刘备 | 罕见 | — | 每当你在战斗中失去生命,就在下回合获得3点格挡。 | 已接入,待实机 | hpLossNextBlock=3 | `self_forming_clay.png`<br>81×80;7786 B |
|
||||
| 73 | [铜钹](https://sts2.huijiwiki.com/wiki/%E9%93%9C%E9%92%B9)<br>`tingsha` | 静默猎手 | 罕见 | — | 你每在你的回合丢弃一张牌,就对一名随机敌人造成3点伤害。 | 待补充,禁入奖励 | 尚缺效果接口:discardDamage | `tingsha.png`<br>77×72;6163 B |
|
||||
| 74 | [扭曲漏斗](https://sts2.huijiwiki.com/wiki/%E6%89%AD%E6%9B%B2%E6%BC%8F%E6%96%97)<br>`twisted_funnel` | 静默猎手 | 罕见 | — | 在每场战斗开始时,给予所有敌人4层中毒。 | 待补充,禁入奖励 | 中毒系统 | `twisted_funnel.png`<br>80×71;6585 B |
|
||||
| 75 | [星系尘埃](https://sts2.huijiwiki.com/wiki/%E6%98%9F%E7%B3%BB%E5%B0%98%E5%9F%83)<br>`galactic_dust` | 储君 | 罕见 | — | 每消耗10点[辉星],就获得10格挡。 | 待补充,禁入奖励 | 辉星消费系统 | `galactic_dust.png`<br>80×82;7036 B |
|
||||
@@ -226,9 +226,9 @@
|
||||
| 113 | [钨合金棍](https://sts2.huijiwiki.com/wiki/%E9%92%A8%E5%90%88%E9%87%91%E6%A3%8D)<br>`tungsten_rod` | 通用 | 稀有 | — | 你每次失去生命时,减少失去的生命值1点。 | 已接入,待实机 | reduceHpLoss=1 | `tungsten_rod.png`<br>80×75;5669 B |
|
||||
| 114 | [蜥蜴尾巴](https://sts2.huijiwiki.com/wiki/%E8%9C%A5%E8%9C%B4%E5%B0%BE%E5%B7%B4)<br>`lizard_tail` | 通用 | 稀有 | — | 当你的生命值将要降低至0或以下时,回复到最大生命值的50%(仅能起效一次)。 | 已接入,待实机 | deathHealFraction=0.5 | `lizard_tail.png`<br>82×81;7566 B |
|
||||
| 115 | [转经轮](https://sts2.huijiwiki.com/wiki/%E8%BD%AC%E7%BB%8F%E8%BD%AE)<br>`prayer_wheel` | 通用 | 稀有 | — | 普通敌人额外掉落一次卡牌奖励。 | 待补充,禁入奖励 | 独立的额外卡牌奖励组 | `prayer_wheel.png`<br>57×81;6409 B |
|
||||
| 116 | [卡戎之灰](https://sts2.huijiwiki.com/wiki/%E5%8D%A1%E6%88%8E%E4%B9%8B%E7%81%B0)<br>`charons_ashes` | 铁甲战士 | 稀有 | — | 每当你消耗一张牌,对所有敌人造成3点伤害。 | 已接入,待实机 | exhaustDamage=3 | `charons_ashes.png`<br>80×79;7056 B |
|
||||
| 117 | [恶魔之舌](https://sts2.huijiwiki.com/wiki/%E6%81%B6%E9%AD%94%E4%B9%8B%E8%88%8C)<br>`demon_tongue` | 铁甲战士 | 稀有 | — | 每回合第一次在回合内失去生命值时,回复等量的生命值。 | 待补充,禁入奖励 | 尚缺效果接口:firstTurnLossHeal | `demon_tongue.png`<br>79×79;5413 B |
|
||||
| 118 | [损毁头盔](https://sts2.huijiwiki.com/wiki/%E6%8D%9F%E6%AF%81%E5%A4%B4%E7%9B%94)<br>`ruined_helmet` | 铁甲战士 | 稀有 | — | 你在每场战斗中第一次获得的力量值翻倍。 | 待补充,禁入奖励 | 尚缺效果接口:firstStrengthDouble | `ruined_helmet.png`<br>67×78;6839 B |
|
||||
| 116 | [卡戎之灰](https://sts2.huijiwiki.com/wiki/%E5%8D%A1%E6%88%8E%E4%B9%8B%E7%81%B0)<br>`charons_ashes` | 刘备 | 稀有 | — | 每当你消耗一张牌,对所有敌人造成3点伤害。 | 已接入,待实机 | exhaustDamage=3 | `charons_ashes.png`<br>80×79;7056 B |
|
||||
| 117 | [恶魔之舌](https://sts2.huijiwiki.com/wiki/%E6%81%B6%E9%AD%94%E4%B9%8B%E8%88%8C)<br>`demon_tongue` | 刘备 | 稀有 | — | 每回合第一次在回合内失去生命值时,回复等量的生命值。 | 待补充,禁入奖励 | 尚缺效果接口:firstTurnLossHeal | `demon_tongue.png`<br>79×79;5413 B |
|
||||
| 118 | [损毁头盔](https://sts2.huijiwiki.com/wiki/%E6%8D%9F%E6%AF%81%E5%A4%B4%E7%9B%94)<br>`ruined_helmet` | 刘备 | 稀有 | — | 你在每场战斗中第一次获得的力量值翻倍。 | 待补充,禁入奖励 | 尚缺效果接口:firstStrengthDouble | `ruined_helmet.png`<br>67×78;6839 B |
|
||||
| 119 | [螺线飞镖](https://sts2.huijiwiki.com/wiki/%E8%9E%BA%E7%BA%BF%E9%A3%9E%E9%95%96)<br>`helical_dart` | 静默猎手 | 稀有 | — | 你每打出一张小刀,就在本回合获得1点敏捷。 | 待补充,禁入奖励 | 小刀牌与静默猎手牌组 | `helical_dart.png`<br>79×80;6241 B |
|
||||
| 120 | [纸鹤](https://sts2.huijiwiki.com/wiki/%E7%BA%B8%E9%B9%A4)<br>`paper_krane` | 静默猎手 | 稀有 | — | 有虚弱状态的敌人造成的伤害降低40%而非25%。 | 梦三适配,待实机 | 依梦三虚弱口径向下取整,敌人攻击倍率0.6。 | `paper_krane.png`<br>76×78;5861 B |
|
||||
| 121 | [结实绷带](https://sts2.huijiwiki.com/wiki/%E7%BB%93%E5%AE%9E%E7%BB%B7%E5%B8%A6)<br>`tough_bandages` | 静默猎手 | 稀有 | — | 你每在你的回合丢弃一张牌,就获得3点格挡。 | 待补充,禁入奖励 | 尚缺效果接口:discardBlock | `tough_bandages.png`<br>80×78;7351 B |
|
||||
@@ -266,7 +266,7 @@
|
||||
| 153 | [勇气投石索](https://sts2.huijiwiki.com/wiki/%E5%8B%87%E6%B0%94%E6%8A%95%E7%9F%B3%E7%B4%A2)<br>`sling_of_courage` | 通用 | 商店 | — | 在与精英敌人战斗时,获得2点力量。 | 已接入,待实机 | eliteOpeningStrength=2 | `sling_of_courage.png`<br>81×77;6202 B |
|
||||
| 154 | [幽灵种子](https://sts2.huijiwiki.com/wiki/%E5%B9%BD%E7%81%B5%E7%A7%8D%E5%AD%90)<br>`ghost_seed` | 通用 | 商店 | — | 打击和防御获得虚无。 | 待补充,禁入奖励 | 尚缺效果接口:starterVoid | `ghost_seed.png`<br>71×78;7167 B |
|
||||
| 155 | [羽翼护符](https://sts2.huijiwiki.com/wiki/%E7%BE%BD%E7%BF%BC%E6%8A%A4%E7%AC%A6)<br>`wing_charm` | 通用 | 商店 | — | 每次卡牌奖励中,都会有随机一张牌被附魔:迅捷1。 | 待补充,禁入奖励 | 迅捷附魔 | `wing_charm.png`<br>76×76;7790 B |
|
||||
| 156 | [硫磺](https://sts2.huijiwiki.com/wiki/%E7%A1%AB%E7%A3%BA)<br>`brimstone` | 铁甲战士 | 商店 | — | 在你的每个回合开始时,你获得2点力量,所有敌人获得1点力量。 | 待补充,禁入奖励 | 尚缺效果接口:turnStrength、enemyTurnStrength | `brimstone.png`<br>79×72;7533 B |
|
||||
| 156 | [硫磺](https://sts2.huijiwiki.com/wiki/%E7%A1%AB%E7%A3%BA)<br>`brimstone` | 刘备 | 商店 | — | 在你的每个回合开始时,你获得2点力量,所有敌人获得1点力量。 | 待补充,禁入奖励 | 尚缺效果接口:turnStrength、enemyTurnStrength | `brimstone.png`<br>79×72;7533 B |
|
||||
| 157 | [忍术卷轴](https://sts2.huijiwiki.com/wiki/%E5%BF%8D%E6%9C%AF%E5%8D%B7%E8%BD%B4)<br>`ninja_scroll` | 静默猎手 | 商店 | — | 每场战斗开始时,将3张小刀加入你的手牌。 | 待补充,禁入奖励 | 小刀牌与静默猎手牌组 | `ninja_scroll.png`<br>78×79;8733 B |
|
||||
| 158 | [维特鲁威仆从](https://sts2.huijiwiki.com/wiki/%E7%BB%B4%E7%89%B9%E9%B2%81%E5%A8%81%E4%BB%86%E4%BB%8E)<br>`vitruvian_minion` | 储君 | 商店 | — | 名字中有“仆从”的卡牌造成双倍的伤害与格挡。 | 待补充,禁入奖励 | 仆从牌与储君牌组 | `vitruvian_minion.png`<br>81×81;9816 B |
|
||||
| 159 | [不死符文](https://sts2.huijiwiki.com/wiki/%E4%B8%8D%E6%AD%BB%E7%AC%A6%E6%96%87)<br>`undying_sigil` | 亡灵契约师 | 商店 | — | 当敌人的灾厄层数大于等于其生命值时,它造成的伤害降低50%。 | 待补充,禁入奖励 | 灾厄系统 | `undying_sigil.png`<br>78×75;9913 B |
|
||||
|
||||
@@ -34,6 +34,7 @@ export function createRelicBattle(run,{active,draw,log,effect,owner,node={},enco
|
||||
const drawMore=async (relic,amount)=>{ if(!usable() || !(amount>0))return; if(!turns)openingHandBonus+=amount; log(relic,`摸${amount}张牌`); await draw(amount); };
|
||||
const controller={
|
||||
has,
|
||||
displayState(){return {dexterity,plating,nextEnergy,nextBlock};},
|
||||
retainsHand(){return has("runic_pyramid") || has("ringing_triangle") && turns===1;},
|
||||
get openingHandBonus(){return openingHandBonus;},
|
||||
get turns(){return turns;},
|
||||
|
||||
@@ -5,7 +5,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "burning_blood",
|
||||
"name": "燃烧之血",
|
||||
"description": "在战斗结束时,回复6点生命。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "初始",
|
||||
"ancient": null,
|
||||
"order": 1,
|
||||
@@ -90,7 +90,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "black_blood",
|
||||
"name": "黑暗之血",
|
||||
"description": "在战斗结束时,回复12点生命。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "初始",
|
||||
"ancient": null,
|
||||
"order": 6,
|
||||
@@ -600,7 +600,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "red_skull",
|
||||
"name": "红头骨",
|
||||
"description": "当你的生命值低于或等于50%时,你额外获得3点力量",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "普通",
|
||||
"ancient": null,
|
||||
"order": 36,
|
||||
@@ -1195,7 +1195,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "paper_phrog",
|
||||
"name": "纸蛙",
|
||||
"description": "有易伤状态的敌人受到的伤害增加75%而非50%。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "罕见",
|
||||
"ancient": null,
|
||||
"order": 71,
|
||||
@@ -1212,7 +1212,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "self_forming_clay",
|
||||
"name": "自成型黏土",
|
||||
"description": "每当你在战斗中失去生命,就在下回合获得3点格挡。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "罕见",
|
||||
"ancient": null,
|
||||
"order": 72,
|
||||
@@ -1960,7 +1960,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "charons_ashes",
|
||||
"name": "卡戎之灰",
|
||||
"description": "每当你消耗一张牌,对所有敌人造成3点伤害。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "稀有",
|
||||
"ancient": null,
|
||||
"order": 116,
|
||||
@@ -1977,7 +1977,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "demon_tongue",
|
||||
"name": "恶魔之舌",
|
||||
"description": "每回合第一次在回合内失去生命值时,回复等量的生命值。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "稀有",
|
||||
"ancient": null,
|
||||
"order": 117,
|
||||
@@ -1994,7 +1994,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "ruined_helmet",
|
||||
"name": "损毁头盔",
|
||||
"description": "你在每场战斗中第一次获得的力量值翻倍。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "稀有",
|
||||
"ancient": null,
|
||||
"order": 118,
|
||||
@@ -2640,7 +2640,7 @@ export const relicCatalog = Object.freeze([
|
||||
"wikiId": "brimstone",
|
||||
"name": "硫磺",
|
||||
"description": "在你的每个回合开始时,你获得2点力量,所有敌人获得1点力量。",
|
||||
"pool": "铁甲战士",
|
||||
"pool": "刘备",
|
||||
"tier": "商店",
|
||||
"ancient": null,
|
||||
"order": 156,
|
||||
|
||||
@@ -7,7 +7,7 @@ export const relicAdaptations = Object.freeze({
|
||||
miniature_tent:"每个休息选项可各选择一次,直至选择离开;不允许无限重复同一选项。",
|
||||
meat_cleaver:"网页没有烹饪细则;暂定每次烹饪最大生命+5,待用户确认。",
|
||||
big_mushroom:"沿用梦三初始手牌4张,扣减2张;未改成STS2的基础手牌数量。",
|
||||
ring_of_the_drake:"刘备仅映射铁甲战士池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。",
|
||||
ring_of_the_drake:"刘备默认使用专属遗物池;其他角色池需配置player.relicPool才可获取。效果按前三个自身回合各摸2张接入。",
|
||||
runic_pyramid:"梦三原本按手牌上限弃牌;这里让全部手牌不计入弃牌上限。虚无牌照常消耗。",
|
||||
precarious_shears:"选牌后扣除16点生命;当前地图流程保留至少1点生命,待补充非战斗死亡结算。",
|
||||
fragrant_mushroom:"随机强化2张并扣除15点生命;当前地图流程保留至少1点生命,待补充非战斗死亡结算。",
|
||||
@@ -34,7 +34,7 @@ export const relicDefinitions = Object.freeze(Object.fromEntries(entries.map(ent
|
||||
const aliases = Object.freeze(Object.fromEntries(entries.flatMap(entry => [[entry.id, entry.id], [entry.wikiId, entry.id], [`mengsan_${entry.wikiId}_shuying`, entry.id]])));
|
||||
export const getRelic = id => Object.hasOwn(aliases, id) ? relicDefinitions[aliases[id]] : null;
|
||||
export const heldRelics = run => [...new Set((run?.player?.items || []).map(id => getRelic(id)?.id).filter(Boolean))].map(getRelic);
|
||||
export const relicPoolFor = run => run?.player?.relicPool || (run?.player?.character === "mengsan_liubei_shuying" ? "铁甲战士" : null);
|
||||
export const relicPoolFor = run => run?.player?.relicPool || (run?.player?.character === "mengsan_liubei_shuying" ? "刘备" : null);
|
||||
export function canAcquireRelic(run, id, { allowPending = false } = {}) {
|
||||
const relic = getRelic(id);
|
||||
if (!run?.player || !relic || !allowPending && relic.implementation === "pending") return false;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
(async () => {
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const { statusIconNames } = await import(pathToFileURL(path.join(root, 'ui/hud/status-model.js')).href);
|
||||
const source = path.join(root, 'assets/sts2/Godot_Atlas_Sprites_v0.111.0/images/atlases/power_atlas.sprites');
|
||||
const output = path.join(root, 'assets/status');
|
||||
fs.mkdirSync(output, {recursive:true});
|
||||
const entries = [], missing = [];
|
||||
for (const name of statusIconNames) {
|
||||
if (!/^[a-z0-9_]+\.png$/.test(name)) throw new Error('Invalid icon name');
|
||||
if (!fs.existsSync(path.join(source,name))) { missing.push(name);continue; }
|
||||
const bytes = fs.readFileSync(path.join(source,name));
|
||||
if (bytes.subarray(0,8).toString('hex')!=='89504e470d0a1a0a') throw new Error('Invalid PNG: '+name);
|
||||
const target=path.join(output,name);
|
||||
if(fs.existsSync(target)&&!fs.readFileSync(target).equals(bytes))throw new Error('Existing icon differs: '+name);
|
||||
fs.writeFileSync(target,bytes);
|
||||
entries.push({name,bytes:bytes.length,width:bytes.readUInt32BE(16),height:bytes.readUInt32BE(20),sha256:crypto.createHash('sha256').update(bytes).digest('hex')});
|
||||
}
|
||||
fs.writeFileSync(path.join(output,'manifest.json'),JSON.stringify({sourceVersion:'v0.111.0',source:'images/atlases/power_atlas.sprites',entries,missing},null,2)+'\n');
|
||||
console.log(JSON.stringify({icons:entries.length,missing,bytes:entries.reduce((n,e)=>n+e.bytes,0)}));
|
||||
})().catch(error=>{console.error(error);process.exitCode=1;});
|
||||
@@ -17,6 +17,11 @@ IMAGE_ALIASES = {
|
||||
"forgotten_soul": "lost_soul.png",
|
||||
"looming_fruit": "looming_fruit_2.png",
|
||||
}
|
||||
# Stable Wiki IDs keep the project's character name when importing raw snapshots.
|
||||
LIUBEI_RELICS = frozenset({
|
||||
"burning_blood", "black_blood", "red_skull", "paper_phrog",
|
||||
"self_forming_clay", "charons_ashes", "demon_tongue", "ruined_helmet", "brimstone",
|
||||
})
|
||||
|
||||
|
||||
def main():
|
||||
@@ -43,7 +48,8 @@ def main():
|
||||
entries.append({
|
||||
"id": ALIASES.get(wiki_id, f"mengsan_{wiki_id}_shuying"),
|
||||
"wikiId": wiki_id, "name": row["name"],
|
||||
"description": row["rendered_description"], "pool": row["pool"],
|
||||
"description": row["rendered_description"],
|
||||
"pool": "刘备" if wiki_id in LIUBEI_RELICS else row["pool"],
|
||||
"tier": row["tier"], "ancient": row["ancient"] or None,
|
||||
"order": int(row["compendium_order"]),
|
||||
"image": f"extension/术樱包/mengsan/assets/relics/{wiki_id}.png",
|
||||
@@ -63,7 +69,7 @@ def main():
|
||||
"snapshot_date": "2026-10-04", "atlas_version": "v0.111.0",
|
||||
"data_source": source["data_source_url"], "data_revision": source["source_revision"],
|
||||
"assets": assets,
|
||||
"notes": ["美味饼干使用铁甲战士版本;四个其他角色版本仍保留在来源图集中。",
|
||||
"notes": ["美味饼干使用刘备版本;四个其他角色版本仍保留在来源图集中。",
|
||||
"遗忘之魂映射 lost_soul.png;布质果实映射 looming_fruit_2.png。",
|
||||
"图标是用户提供的原游戏拆包图片,逐字节复制,未重绘。"]
|
||||
}, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
// Real browser DOM/CSS with engine services isolated; this is not a full-game test.
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const app = path.resolve(__dirname, '../../../..');
|
||||
const packages = process.env.CODEX_NODE_MODULES || 'C:/Users/Diuse/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules';
|
||||
const { chromium } = require(path.join(packages, 'playwright'));
|
||||
const prefix = '/extension/' + encodeURIComponent('术樱包') + '/mengsan';
|
||||
const holdStatus = async (page, locator, pointerType = 'mouse') => {
|
||||
const box = await locator.boundingBox();
|
||||
const point = {x:box.x + box.width / 2,y:box.y + box.height / 2};
|
||||
if (pointerType === 'touch') {
|
||||
await locator.dispatchEvent('pointerdown', {button:0,pointerId:44,
|
||||
pointerType,clientX:point.x,clientY:point.y});
|
||||
await page.waitForTimeout(550);
|
||||
await page.evaluate(() => window.dispatchEvent(
|
||||
new PointerEvent('pointerup', {pointerId:44,pointerType:'touch'})));
|
||||
} else {
|
||||
await page.mouse.move(point.x,point.y); await page.mouse.down();
|
||||
await page.waitForTimeout(550); await page.mouse.up();
|
||||
}
|
||||
};
|
||||
// Native themes use different button sizes. Drag onto an exposed point on the
|
||||
// original player, rather than assuming its center is clear in the dense fixture.
|
||||
const exposedTarget = (page,index) => page.evaluate(index => {
|
||||
const player=game.players[index],rect=player.getBoundingClientRect();
|
||||
for(const y of [.5,.15,.85,.05,.95])for(const x of [.5,.15,.85]) {
|
||||
const px=rect.left+rect.width*x,py=rect.top+rect.height*y;
|
||||
if(document.elementFromPoint(px,py)?.closest('.player')===player)return {x:px,y:py,width:0,height:0};
|
||||
}
|
||||
throw new Error('Native target has no exposed drag point: '+index);
|
||||
},index);
|
||||
const fixture = `<!doctype html><meta charset="utf-8"><link rel="stylesheet" href="/layout/default/layout.css"><div id="window"><div id="arena"><div id="mebg"></div><div id="me" class="mengsan-hand-root-shuying"><div id="handcards1" class="mengsan-hand-scroll-shuying"><div class="handcards mengsan-hand-zone-shuying mengsan-hand-layout-shuying"></div></div><div id="handcards2"><div class="handcards"></div></div></div><div id="control"><div class="control"><div>测试技能</div><div>结束出牌</div></div></div></div></div><style>html,body,#window{margin:0;width:100%;height:100%;position:absolute}#arena{inset:0;width:100%;height:100%;position:absolute}#arena>.player{position:absolute;width:100px;height:130px;background:#345;color:#fff}#arena>.player .avatar{height:100%;width:100%;background:linear-gradient(#334f4b,#172d29)}#arena>.player .name{position:absolute;top:4px;left:4px}#me{position:absolute;bottom:0;width:100%;height:160px}#handcards1,#handcards2{position:absolute;width:100%;height:100%}.handcards{height:100%}.card{background:#e8d7b2;color:#263b35;position:absolute}.mengsan-intent-shuying{position:absolute}</style>`;
|
||||
const server = http.createServer((req, res) => {
|
||||
if (req.url === '/fixture') { res.setHeader('Content-Type','text/html;charset=utf-8');res.end(fixture);return; }
|
||||
if(req.url==='/noname.js'){res.setHeader('Content-Type','text/javascript');res.end('export const _status={};');return;}
|
||||
if(decodeURIComponent(new URL(req.url,'http://localhost').pathname)==='/extension/术樱包/diagnostics/index.js') {
|
||||
res.setHeader('Content-Type','text/javascript');res.end('export const getDiagnostics=()=>({status:()=>({enabled:false}),scope:()=>({info(){},error(){}})});');return;
|
||||
}
|
||||
const target = path.resolve(app, '.' + decodeURIComponent(new URL(req.url, 'http://localhost').pathname));
|
||||
if (!target.startsWith(app + path.sep)) { res.writeHead(403);res.end();return; }
|
||||
const types = { '.js':'text/javascript', '.css':'text/css', '.png':'image/png' };
|
||||
fs.readFile(target, (error, bytes) => { if(error){res.writeHead(404);res.end();return;}res.setHeader('Content-Type',types[path.extname(target)]||'text/plain');res.end(bytes); });
|
||||
});
|
||||
(async () => {
|
||||
await new Promise(resolve => server.listen(0,'127.0.0.1',resolve));
|
||||
const origin = 'http://127.0.0.1:' + server.address().port;
|
||||
const browser = await chromium.launch({channel:'msedge',headless:true});
|
||||
let checks = 0;
|
||||
try {
|
||||
for (const skin of process.env.HUD_TEST_SKIN?[process.env.HUD_TEST_SKIN]:['native','shousha','shizhounian','xinsha']) {
|
||||
for (const viewport of [{width:1280,height:720},{width:1234,height:920},{width:812,height:600},{width:812,height:375},{width:667,height:375},{width:375,height:500},{width:540,height:900}]) {
|
||||
const page = await browser.newPage({viewport,hasTouch:true});
|
||||
await page.goto(origin+'/fixture');
|
||||
await page.addStyleTag({content:'body{background:#161e29 url("/image/background/lanting_bg.jpg") center/cover no-repeat}'});
|
||||
await page.addStyleTag({url:origin+prefix+'/ui/style.css'});
|
||||
if(skin!=='native') {
|
||||
for(const css of ['/extension/'+encodeURIComponent('十周年UI')+'/src/styles/layout.css','/extension/'+encodeURIComponent('十周年UI')+'/ui/styles/base.css','/extension/'+encodeURIComponent('十周年UI')+'/ui/styles/skill/'+skin+'.css','/extension/'+encodeURIComponent('十周年UI')+'/ui/styles/lbtn/'+skin+'.css'])await page.addStyleTag({url:origin+css});
|
||||
await page.addStyleTag({url:origin+'/extension/'+encodeURIComponent('十周年UI')+'/src/styles/player'+({shousha:2,shizhounian:1,xinsha:3}[skin])+'.css'});
|
||||
await page.addStyleTag({url:origin+'/extension/'+encodeURIComponent('十周年UI')+'/src/styles/equip.css'});
|
||||
}
|
||||
await page.addStyleTag({url:origin+prefix+'/ui/hand/style.css'});
|
||||
const initial = await page.evaluate(async ({prefix,skin}) => {
|
||||
const mod = await import(prefix+'/ui/hud/index.js').catch(error=>({error:String(error)}));
|
||||
if(typeof mod.mountBattleHUD!=='function')return {module:false,error:mod.error};
|
||||
const releases=[];window.session={id:'hud-test',active:true,ownResource:(_,f)=>releases.push(f)};
|
||||
window.releaseAll=()=>releases.reverse().forEach(f=>f());
|
||||
const arena=document.getElementById('arena'), hand=document.getElementById('me'), control=document.getElementById('control');
|
||||
if(skin!=='native') {
|
||||
// Match create.js and player-element.js, including the renamed container and nested wraps.
|
||||
control.id='dui-controls';hand.classList.add('hand-wrap');arena.classList.add('decadeUI');
|
||||
arena.dataset.mode='identity';arena.dataset.outcropSkin='shizhounian';arena.dataset.playerHeight='default';
|
||||
if(window.innerHeight<=500||window.innerWidth<=600)arena.classList.add('dui-mobile');
|
||||
control.firstElementChild.classList.add('stayleft');
|
||||
}
|
||||
document.body.classList.add('mengsan-hand-ui-shuying');control.firstElementChild.style.opacity='1';
|
||||
[...control.firstElementChild.children].forEach((node,index)=>{
|
||||
node.classList.add(index?'primary2':'primary');
|
||||
const label=index?'回合结束':'确定';
|
||||
if(skin==='native')node.textContent=label;else {const span=document.createElement('span');span.textContent=label;node.replaceChildren(span);}
|
||||
node.addEventListener('click',()=>{window.nativeActionClicks=(window.nativeActionClicks||0)+1;});
|
||||
});
|
||||
const players=[];
|
||||
for(let i=0;i<8;i++) {
|
||||
const player=document.createElement('div');player.className='player';player.dataset.position=String(i);player.playerid=String(i);player.name=i?'enemy'+i:'liubei';player.hp=40;player.maxHp=60;player.hujia=8;
|
||||
player.storage={mengsanCamp_shuying:i?'enemy':'ally',mengsanStrength_shuying:i===1?3:0,mengsanVulnerable_shuying:i===2?2:0,mengsanEnergy_shuying:3,mengsanMaxEnergy_shuying:3};
|
||||
player.innerHTML=skin==='native' ? '<div class="avatar"></div><div class="name">'+(i?'敌人'+i:'刘备')+'</div><div class="hp">40/60</div><div class="marks"></div>' : '<div class="primary-avatar has-outcrop"><div class="action">回合开始</div></div><div class="framebg"></div><div class="identity">汉</div><div class="camp-wrap"><div class="camp-border"></div><div class="avatar-name">'+(i?'敌人'+i:'刘备')+'</div></div><div class="hp-wrap"><div class="hp">40/60</div></div><div class="card-count">8</div><div class="dui-marks"></div>';
|
||||
if(i)player.insertAdjacentHTML('beforeend','<div class="mengsan-intent-shuying">攻击 8×2</div>');
|
||||
player.node={avatar:player.querySelector('.avatar,.primary-avatar'),hp:player.querySelector('.hp'),hpWrap:player.querySelector('.hp-wrap'),campWrap:player.querySelector('.camp-wrap'),name:player.querySelector('.name,.avatar-name'),identity:player.querySelector('.identity'),framebg:player.querySelector('.framebg'),count:player.querySelector('.card-count'),marks:player.querySelector('.marks,.dui-marks')};
|
||||
player.node.avatar.style.backgroundImage='url("/image/character/'+(i?'caocao':'liubei')+'.jpg")';
|
||||
if(i){player.style.cssText=`left:${(i-1)%4*21+5}%;top:${68+Math.floor((i-1)/4)*96}px;width:68px;height:82px;transform:none`;}
|
||||
const equips=document.createElement('div');equips.className='equips';player.node.equips=equips;player.appendChild(equips);
|
||||
if(i===0||i===1)for(let j=0;j<(i===0?5:2);j++){
|
||||
const equip=document.createElement('div');equip.className='card equip'+(j+1);equip.innerHTML='<div class="name2"><span>♠2 </span><span>'+(['八卦阵','的卢','青釭剑','赤兔','木牛'][j])+'</span></div>';
|
||||
equip.addEventListener('click',e=>{e.stopPropagation();window.equipClicks=(window.equipClicks||0)+1;});equips.appendChild(equip);
|
||||
}
|
||||
player.getCards=zone=>zone==='e'?[...equips.children]:[];
|
||||
player.marks={};player.hasSkill=()=>false;player.getSkills=()=>[];player.isAlive=()=>player.hp>0;arena.appendChild(player);players.push(player);
|
||||
}
|
||||
const me=players[0];window.game={me,players,dead:[]};window.battle={session,players:new Set(players),owner:me};
|
||||
const nativeMark=document.createElement('div');nativeMark.className='mark';nativeMark.textContent='义';me.node.marks.appendChild(nativeMark);window.testNativeMark=nativeMark;
|
||||
nativeMark.addEventListener('click',event=>{
|
||||
event.stopPropagation();window.markIntroOpens=(window.markIntroOpens||0)+1;
|
||||
const box=me.getBoundingClientRect(),tip=document.createElement('div');tip.className='dialog popped static';
|
||||
tip.style.cssText=`position:absolute;left:${box.right-20}px;top:${box.top-30}px;width:236px;height:160px;padding:16px;z-index:21`;
|
||||
tip.innerHTML='<div class="content-container"><div class="content"><div class="caption">桃园羁绊</div><div>先天结识关羽、张飞。保留原生标记说明内容与关闭回调。</div></div></div>';
|
||||
const layer=document.createElement('div');layer.className='poplayer';layer.style.cssText='position:absolute;inset:0';
|
||||
const close=()=>{tip.remove();layer.remove();window.markIntroClosed=true;};layer.addEventListener('click',close);tip._close=close;
|
||||
ui.window.append(tip,layer);window.testMarkIntro=tip;
|
||||
});
|
||||
window.hudEventState={currentPhase:me,event:{name:'chooseToUse',player:me,isMine:()=>true}};
|
||||
window.ui={arena,me:hand,mebg:document.getElementById('mebg'),control,window:document.getElementById('window'),handcards1Container:document.getElementById('handcards1'),handcards2Container:document.getElementById('handcards2'),selected:{cards:[],targets:[],buttons:[]},click:{}};
|
||||
if(skin==='xinsha') {
|
||||
const host=document.createElement('div');host.className='equips-wrap';host.style.cssText='position:absolute;left:150px;top:0;width:130px;height:100%';
|
||||
host.innerHTML='<div class="equips-back"><div>装备槽</div></div>';arena.insertBefore(host,hand);host.appendChild(me.node.equips);ui.equipSolts=host;
|
||||
}
|
||||
window.originalEquipParent=me.node.equips.parentNode;
|
||||
me.node.handcards1=ui.handcards1Container.firstElementChild;me.node.handcards2=ui.handcards2Container.firstElementChild;
|
||||
const get={translation:value=>value==='liubei'?'刘备':String(value).replace(/^enemy/,'敌人'),select:value=>value};
|
||||
const card=document.createElement('div');card.className='card selectable';card.textContent='杀';card.name='sha';me.node.handcards1.appendChild(card);window.card=card;
|
||||
card.addEventListener('contextmenu',event=>{
|
||||
event.preventDefault();window.introOpens=(window.introOpens||0)+1;
|
||||
const tip=document.createElement('div');tip.className='dialog popped static';
|
||||
const rect=card.getBoundingClientRect();tip.style.cssText=`position:absolute;left:${rect.left}px;top:${rect.top-30}px;width:236px;height:160px;z-index:21;padding:16px`;
|
||||
tip.innerHTML='<div class="content-container"><div class="content"><div class="caption">杀 · 卡牌说明</div><div>对选定敌人造成伤害。说明保留原生内容与关闭操作。</div></div></div>';
|
||||
const layer=document.createElement('div');layer.className='poplayer';layer.style.cssText='position:absolute;inset:0';
|
||||
layer.addEventListener('click',()=>{tip.remove();layer.remove();window.nativeIntroClosed=true;});
|
||||
ui.window.append(tip,layer);window.testIntro=tip;
|
||||
});
|
||||
window.oldParents={hand:hand.parentNode,control:control.parentNode,card:card.parentNode};
|
||||
window.nativeActionGroup=control.firstElementChild;window.nativeActionButtons=[...nativeActionGroup.children];
|
||||
window.actionPaint=node=>{const s=getComputedStyle(node);return Object.fromEntries(['backgroundImage','backgroundColor','borderImageSource','borderTopColor','borderTopStyle','borderTopWidth','borderTopLeftRadius','color','fontFamily','fontSize','fontWeight','textShadow','boxShadow','filter','paddingTop','paddingRight','paddingBottom','paddingLeft'].map(key=>[key,s[key]]));};
|
||||
// Decade animates the initial class/theme change; compare settled native styles.
|
||||
await new Promise(resolve=>setTimeout(resolve,650));
|
||||
window.originalActionPaint=[nativeActionGroup,...nativeActionButtons].map(actionPaint);
|
||||
window.originalActionHTML=nativeActionGroup.innerHTML;
|
||||
window.originalEnemyPresentation=players.slice(1).map(p=>({style:p.style.cssText,classes:p.className,html:p.innerHTML}));
|
||||
window.hud=mod.mountBattleHUD(me,battle,{ui,game,get,lib:{skill:{},translate:{}},_status:hudEventState,document,window,assetBase:prefix});
|
||||
const {mountRelicStrip}=await import(prefix+'/ui/relic-strip.js');
|
||||
window.testRelics=[];window.testStrip=mountRelicStrip(arena,()=>testRelics,relic=>{window.lastRelicOpened=relic.id;},()=>{
|
||||
const icon=document.createElement('img');icon.src=prefix+'/assets/status/strength_power.png';return icon;
|
||||
},document,window);releases.push(()=>testStrip.dispose());
|
||||
let submitted=0;game.check=()=>ui.selected.cards.length===1&&ui.selected.targets.length===1;game.uncheck=()=>{ui.selected.cards=[];ui.selected.targets=[];};
|
||||
ui.click.card=function(){ui.selected.cards=[this];players.slice(1).forEach(p=>p.classList.add('selectable'));};ui.click.target=function(){ui.selected.targets=[this];};ui.click.ok=()=>{submitted++;game.uncheck();};
|
||||
game.isSubmitted=()=>submitted;const event=hudEventState.event;event.selectTarget=[1,1];event.selectCard=[1,1];
|
||||
const {mountHandUI}=await import(prefix+'/ui/hand/index.js');
|
||||
window.testHandUI=await mountHandUI(me,session,{ui,game,get,lib:{card:{},translate:{}},_status:hudEventState,document,window,isActiveCardUse:()=>true,canPayCard:()=>true,cardCost:()=>1,styleURL:prefix+'/ui/hand/style.css'});
|
||||
window.testInput={owns:()=>false};hud.refresh();window.testFan={sync:()=>testHandUI.refresh()};
|
||||
return {module:true,parentKept:hand.parentNode===oldParents.hand&&control.parentNode===oldParents.control&&card.parentNode===oldParents.card,skin};
|
||||
},{prefix,skin});
|
||||
assert.equal(initial.module,true,initial.error||'HUD mount module must exist');checks++;
|
||||
assert.equal(initial.parentKept,true);checks++;
|
||||
await page.waitForTimeout(70);
|
||||
const releasedControls = await page.evaluate(() => {
|
||||
const control = document.createElement('div');
|
||||
control.className = 'control';
|
||||
control.style.setProperty('left', '7px', 'important');
|
||||
ui.control.appendChild(control);
|
||||
hud.refresh();
|
||||
const managed = control.style.left === 'auto';
|
||||
control.remove();
|
||||
hud.refresh();
|
||||
const released = control.style.left === '7px' && control.style.getPropertyPriority('left') === 'important';
|
||||
control.style.left = '13px';
|
||||
ui.control.appendChild(control);
|
||||
hud.refresh();
|
||||
const reacquired = control.style.left === 'auto';
|
||||
control.remove();
|
||||
hud.refresh();
|
||||
const restoredAgain = control.style.left === '13px';
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'damage skill-popup';
|
||||
popup.style.left = game.me.offsetLeft + 'px';
|
||||
popup.style.top = game.me.offsetTop + 80 + 'px';
|
||||
ui.arena.appendChild(popup);
|
||||
hud.refresh();
|
||||
const popupManaged = popup.classList.contains('mengsan-hud-notification-shuying');
|
||||
popup.remove();
|
||||
hud.refresh();
|
||||
return { managed, released, reacquired, restoredAgain, popupManaged,
|
||||
popupReleased: !popup.classList.contains('mengsan-hud-notification-shuying') };
|
||||
});
|
||||
assert.ok(Object.values(releasedControls).every(Boolean), 'detached controls and notifications release HUD ownership and can be acquired again: ' + JSON.stringify(releasedControls));checks++;
|
||||
await page.evaluate(()=>testFan.sync(true));
|
||||
const corners=await page.evaluate(()=>({portrait:parseFloat(getComputedStyle(game.me.node.avatar).borderTopLeftRadius)}));
|
||||
assert.ok(corners.portrait>=12,'portrait retains rounded edges: '+JSON.stringify(corners));checks++;
|
||||
const glass=await page.locator('.mengsan-hud-shuying').evaluate(n=>{const s=getComputedStyle(n);return {background:s.backgroundColor,opacity:s.opacity,blur:s.backdropFilter};});
|
||||
assert.equal(glass.background,'rgba(0, 0, 0, 0)','footer is completely transparent');checks++;
|
||||
assert.equal(glass.blur,'none','footer has no background blur');checks++;
|
||||
assert.equal(glass.opacity,'1','text and buttons retain full opacity');checks++;
|
||||
const equipLayout=await page.evaluate(()=>{
|
||||
const p=game.me.getBoundingClientRect(),e=game.me.node.equips.getBoundingClientRect(),enemy=game.players[1],p2=enemy.getBoundingClientRect(),e2=enemy.node.equips.getBoundingClientRect();
|
||||
return {self:p.toJSON(),selfEquips:e.toJSON(),enemy:p2.toJSON(),enemyEquips:e2.toJSON()};
|
||||
});
|
||||
assert.ok(equipLayout.selfEquips.top>=equipLayout.self.top&&equipLayout.selfEquips.bottom<=equipLayout.self.bottom&&equipLayout.selfEquips.left>=equipLayout.self.left&&equipLayout.selfEquips.right<=equipLayout.self.right,'equipment overlays the enlarged portrait: '+JSON.stringify(equipLayout));checks++;
|
||||
assert.ok(equipLayout.selfEquips.bottom<=viewport.height-8,'equipment stays inside the player portrait');checks++;
|
||||
assert.equal(await page.evaluate(()=>getComputedStyle(game.me.node.equips).flexDirection),'column','equipment is stacked vertically over the portrait');checks++;
|
||||
try { await page.locator('.mengsan-hud-self-equips-shuying>.card').first().click({timeout:3000}); }
|
||||
catch(error) { console.log(await page.evaluate(()=>[game.me.node.equips.firstElementChild,game.me.node.equips,game.me.node.equips.parentNode,ui.arena].map(n=>({cls:n.className,rect:n.getBoundingClientRect().toJSON(),style:n.style.cssText,overflow:getComputedStyle(n).overflow,scroll:[n.scrollLeft,n.scrollTop]}))));throw error; }
|
||||
assert.equal(await page.evaluate(()=>window.equipClicks),1,'real equipment callback is preserved');checks++;
|
||||
assert.equal(await page.evaluate(()=>game.me.node.equips.parentNode===originalEquipParent),true,'optional separate equipment wrapper keeps its real parent');checks++;
|
||||
await page.evaluate(()=>{const cards=game.me.node.equips.children;cards[4].classList.add('selectable');cards[4].scrollIntoView({block:'nearest',inline:'nearest'});});
|
||||
await page.locator('.mengsan-hud-self-equips-shuying>.card').last().click();
|
||||
assert.equal(await page.evaluate(()=>window.equipClicks),2,'last of five equipped cards remains reachable');checks++;
|
||||
const layout=await page.evaluate(()=>{
|
||||
const rect=n=>{const r=n.getBoundingClientRect();return {x:r.x,y:r.y,right:r.right,bottom:r.bottom,width:r.width,height:r.height};};
|
||||
return {hud:rect(document.querySelector('.mengsan-hud-shuying')),hand:rect(ui.me),control:rect(ui.control),players:game.players.slice(1).map(rect),statusCount:document.querySelectorAll('.mengsan-unit-status-shuying').length,
|
||||
statusRow:rect(document.querySelector('.mengsan-hud-statuses-shuying'))};
|
||||
});
|
||||
assert.equal(layout.statusCount,0,'enemy information is not replaced by the player HUD');checks++;
|
||||
assert.ok(layout.control.bottom<=layout.hand.y-7,'horizontal actions sit above the hand area');checks++;
|
||||
assert.equal(await page.evaluate(()=>game.players.slice(1).every((p,i)=>p.style.cssText===originalEnemyPresentation[i].style&&p.innerHTML===originalEnemyPresentation[i].html&&p.className===originalEnemyPresentation[i].classes)),true,'enemy and ally DOM, inline positions and classes stay native');checks++;
|
||||
assert.equal(await page.evaluate(()=>{const p=game.me.getBoundingClientRect(),s=document.querySelector('.mengsan-hud-info-shuying').getBoundingClientRect();return p.right<=s.left||p.bottom<=s.top||p.left>=s.right||p.top>=s.bottom;}),true,'enlarged portrait clears the vertical statistics');checks++;
|
||||
assert.equal(await page.evaluate(()=>{const p=game.me.getBoundingClientRect(),s=document.querySelector('.mengsan-hud-info-shuying').getBoundingClientRect();return ui.arena.dataset.mengsanHudLayout==='narrow'?p.bottom<=s.top&&s.top-p.bottom<32:s.left-p.right>=0&&s.left-p.right<=16;}),true,'statistics stay close to the portrait');checks++;
|
||||
assert.equal(await page.locator('.mengsan-hud-center-shuying').isVisible(),false,'empty status area is hidden');checks++;
|
||||
assert.equal(await page.locator('.mengsan-hud-shuying').innerText().then(t=>/当前状态|暂无状态|选择行动|等待行动/.test(t)),false,'status headings and action filler text are absent');checks++;
|
||||
assert.deepEqual(await page.evaluate(()=>[nativeActionGroup,...nativeActionButtons].map(actionPaint)),await page.evaluate(()=>originalActionPaint),'native action group and button paint, typography and padding stay unchanged');checks++;
|
||||
assert.equal(await page.evaluate(()=>ui.control.firstElementChild===nativeActionGroup&&nativeActionGroup.innerHTML===originalActionHTML&&nativeActionButtons.every((node,i)=>nativeActionGroup.children[i]===node)),true,'real native action nodes and contents are retained');checks++;
|
||||
assert.equal(await page.locator('.mengsan-hud-inspect-shuying').count(),0,'skills and equipment proxy button is removed');checks++;
|
||||
await page.locator('.mengsan-hud-controls-shuying>.control>div').first().click();
|
||||
await page.locator('.mengsan-hud-controls-shuying>.control>div').nth(1).click();
|
||||
assert.equal(await page.evaluate(()=>nativeActionClicks),2,'both real native confirm and end-turn callbacks remain usable');checks++;
|
||||
assert.equal(await page.evaluate(()=>nativeActionButtons.every(n=>n.getBoundingClientRect().bottom<=ui.me.getBoundingClientRect().top-4)),true,'native button dimensions remain fully above the hand');checks++;
|
||||
await page.evaluate(()=>{for(let i=0;i<6;i++)testNativeMark.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true}));});
|
||||
await page.waitForTimeout(60);
|
||||
await page.evaluate(()=>{for(let i=0;i<3;i++)testNativeMark.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true}));});
|
||||
const markIntro=await page.evaluate(()=>({opens:markIntroOpens,count:ui.window.querySelectorAll('.dialog.popped').length,layers:ui.window.querySelectorAll('.poplayer').length,layerClass:ui.window.querySelector('.poplayer').className,layerStyle:ui.window.querySelector('.poplayer').style.cssText,box:testMarkIntro.getBoundingClientRect().toJSON(),portrait:game.me.getBoundingClientRect().toJSON(),info:document.querySelector('.mengsan-hud-info-shuying').getBoundingClientRect().toJSON(),z:getComputedStyle(testMarkIntro).zIndex,layerZ:getComputedStyle(ui.window.querySelector('.poplayer')).zIndex}));
|
||||
assert.ok(markIntro.opens===1&&markIntro.count===1&&markIntro.layers===1,'rapid mark clicks keep one native popup and one backdrop: '+JSON.stringify(markIntro));checks++;
|
||||
assert.ok(markIntro.box.bottom<=Math.min(markIntro.portrait.top,markIntro.info.top)&&markIntro.box.x>=8&&markIntro.box.right<=viewport.width-8,'mark details clear the player portrait and statistics: '+JSON.stringify(markIntro));checks++;
|
||||
assert.ok(Number(markIntro.z)>Number(markIntro.layerZ)&&Number(markIntro.layerZ)>31,'native mark popup and backdrop draw above player and skills: '+JSON.stringify(markIntro));checks++;
|
||||
if(viewport.width===1234){await page.waitForTimeout(350);await page.screenshot({path:path.join(process.env.HUD_ARTIFACTS||__dirname,'hud-mark-description-'+skin+'.png')});}
|
||||
await page.locator('.poplayer').click({position:{x:4,y:4}});
|
||||
assert.equal(await page.evaluate(()=>markIntroClosed),true,'native mark close callback is preserved');checks++;
|
||||
await page.evaluate(()=>testNativeMark.dispatchEvent(new MouseEvent('click',{bubbles:true,cancelable:true})));
|
||||
await page.waitForTimeout(30);
|
||||
assert.equal(await page.evaluate(()=>markIntroOpens),2,'mark details can reopen after closing');checks++;
|
||||
await page.locator('.poplayer').click({position:{x:4,y:4}});
|
||||
assert.equal(await page.locator('.mengsan-hand-relics-shuying').isVisible(),false,'empty relic strip stays hidden');checks++;
|
||||
const nativeSkills=await page.evaluate(()=>{
|
||||
const skills=document.createElement('div');skills.className='control';skills.innerHTML='<div>原生技能甲</div><div>原生技能乙</div>';
|
||||
ui.control.appendChild(skills);ui.skills=skills;window.testNativeSkills=skills;
|
||||
skills.firstElementChild.addEventListener('click',()=>window.nativeSkillUsed=true);hud.refresh();
|
||||
const boxes=[...skills.children].map(n=>n.getBoundingClientRect());
|
||||
const rail=skills.getBoundingClientRect();return {vertical:boxes[0].bottom<=boxes[1].top,right:rail.left>=ui.me.getBoundingClientRect().right,contained:boxes.every(r=>r.left>=rail.left-1&&r.right<=rail.right+1)};
|
||||
});
|
||||
assert.ok(nativeSkills.vertical&&nativeSkills.right&&nativeSkills.contained,'native skill references stay vertically on the right without moving their parent or clipping buttons: '+JSON.stringify(nativeSkills));checks++;
|
||||
await page.getByText('原生技能甲',{exact:true}).click();
|
||||
assert.equal(await page.evaluate(()=>window.nativeSkillUsed),true,'native skill callbacks survive the separate rail');checks++;
|
||||
await page.evaluate(()=>{testNativeSkills.remove();delete ui.skills;hud.refresh();});
|
||||
const relicGeometry=await page.evaluate(()=>{
|
||||
testRelics=[{id:'test',name:'遗物'}];testStrip.refresh();
|
||||
const strip=document.querySelector('.mengsan-hand-relics-shuying').getBoundingClientRect(),icon=document.querySelector('.mengsan-hand-relics-shuying button').getBoundingClientRect();
|
||||
return {strip:strip.toJSON(),icon:icon.toJSON()};
|
||||
});
|
||||
assert.ok(relicGeometry.icon.y>=relicGeometry.strip.y&&relicGeometry.icon.bottom<=relicGeometry.strip.bottom,'top relic icon fits without clipping');checks++;
|
||||
assert.ok(relicGeometry.strip.width<=142,'relic area stays compact above the right skill rail');checks++;
|
||||
assert.ok(relicGeometry.strip.x>=viewport.width-166,'relic area is on the right');checks++;
|
||||
assert.ok(Math.abs(relicGeometry.strip.bottom-(viewport.height-Number.parseFloat(await page.locator('#arena').evaluate(n=>n.style.getPropertyValue('--mengsan-relic-dock-bottom')))))<2,'relic area aligns with the side dock, with a compact narrow-screen offset');checks++;
|
||||
await page.evaluate(()=>{testRelics=Array.from({length:10},(_,i)=>({id:'r'+i,name:'遗物'+i}));testStrip.refresh();hud.refresh();});
|
||||
const battlefieldClear=await page.evaluate(()=>{
|
||||
const strip=document.querySelector('.mengsan-hand-relics-shuying').getBoundingClientRect();
|
||||
const intersects=(a,b)=>a.left<b.right&&a.right>b.left&&a.top<b.bottom&&a.bottom>b.top;
|
||||
return {clear:[...document.querySelectorAll('.player>.mengsan-intent-shuying')].every(n=>!intersects(n.getBoundingClientRect(),strip)),strip:strip.toJSON(),intents:[...document.querySelectorAll('.player>.mengsan-intent-shuying')].map(n=>n.getBoundingClientRect().toJSON())};
|
||||
});
|
||||
assert.equal(battlefieldClear.clear,true,'a full relic strip must not cover enemy intent: '+JSON.stringify(battlefieldClear));checks++;
|
||||
await page.locator('.mengsan-hand-relics-shuying button').last().focus();
|
||||
await page.locator('.mengsan-hand-relics-shuying button').last().press('Enter');
|
||||
assert.equal(await page.evaluate(()=>lastRelicOpened),'r9','overflow relics retain keyboard access to details');checks++;
|
||||
await page.evaluate(()=>{testRelics=[{id:'test',name:'遗物'}];testStrip.refresh();});
|
||||
const start=await page.locator('.handcards>.card').boundingBox(),target=await exposedTarget(page,3);
|
||||
await page.mouse.move(start.x+start.width/2,start.y+30);await page.mouse.down();
|
||||
await page.mouse.move(target.x+target.width/2,target.y+target.height/2,{steps:8});await page.mouse.up();
|
||||
assert.equal(await page.evaluate(()=>game.isSubmitted()),1,'single target drag submits once');checks++;
|
||||
if(skin!=='native') {
|
||||
await page.evaluate(()=>{
|
||||
const skills=document.createElement('div');skills.className='skill-control';skills.innerHTML='<div class="enable"><div class="skillitem usable">主动技能</div></div>';ui.arena.appendChild(skills);ui.skillControl=skills;
|
||||
skills.querySelector('.skillitem').addEventListener('click',()=>{hudEventState.event.skill='test-skill';});hud.refresh();
|
||||
});
|
||||
await page.locator('.skillitem').click();
|
||||
assert.equal(await page.evaluate(()=>hudEventState.event.skill),'test-skill');checks++;
|
||||
assert.notEqual(await page.locator('.skillitem').evaluate(n=>getComputedStyle(n).webkitTextFillColor),'rgba(0, 0, 0, 0)','skin gradient text remains readable after reskinning');checks++;
|
||||
assert.equal(await page.evaluate(()=>ui.skillControl.getBoundingClientRect().left>=ui.me.getBoundingClientRect().right),true,'vertical skills stay to the right of the hand');checks++;
|
||||
await page.evaluate(()=>{
|
||||
const globalSkills=document.createElement('div');globalSkills.className='gskill-control';globalSkills.innerHTML='<div class="trigger"><div class="skillitem">仁德</div></div>';
|
||||
ui.arena.appendChild(globalSkills);ui.gskillControl=globalSkills;hud.refresh();
|
||||
});
|
||||
const rails=await page.evaluate(()=>({skills:ui.skillControl.getBoundingClientRect().toJSON(),global:ui.gskillControl.getBoundingClientRect().toJSON(),native:ui.control.getBoundingClientRect().toJSON(),hud:document.querySelector('.mengsan-hud-shuying').getBoundingClientRect().toJSON()}));
|
||||
assert.ok(rails.global.bottom<=rails.skills.top-4&&rails.global.top>=rails.hud.top,'optional global skill controls stay inside the HUD without overlap: '+JSON.stringify(rails));checks++;
|
||||
await page.evaluate(()=>{delete hudEventState.event.skill;});
|
||||
assert.equal(await page.evaluate(()=>ui.arena.scrollLeft===0&&ui.arena.scrollTop===0),true,'skill controls must not scroll the arena');checks++;
|
||||
}
|
||||
const beforeTouch = await page.evaluate(()=>game.isSubmitted());
|
||||
const touchCard = await page.locator('.handcards>.card').boundingBox();
|
||||
await page.evaluate(({start,target})=>{
|
||||
const send=(host,type,x,y)=>host.dispatchEvent(new PointerEvent(type,{bubbles:true,cancelable:true,pointerId:17,pointerType:'touch',isPrimary:true,button:0,clientX:x,clientY:y}));
|
||||
const x=start.x+start.width/2,y=start.y+25,tx=target.x+target.width/2,ty=target.y+target.height/2;
|
||||
send(card,'pointerdown',x,y);send(window,'pointermove',tx,ty);send(window,'pointerup',tx,ty);
|
||||
},{start:touchCard,target:await exposedTarget(page,2)});
|
||||
const afterTouch=await page.evaluate(()=>({submitted:game.isSubmitted(),selected:ui.selected.cards.length,targets:ui.selected.targets.length,eventSkill:hudEventState.event.skill,classes:card.className,owns:testInput.owns(),hand:ui.me.getBoundingClientRect().toJSON(),card:card.getBoundingClientRect().toJSON()}));
|
||||
if(afterTouch.submitted!==beforeTouch+1){afterTouch.geometry=await page.evaluate(()=>['window','arena','me','control'].map(id=>{const n=id==='control'?ui.control:document.getElementById(id),s=getComputedStyle(n);return {id,rect:n.getBoundingClientRect().toJSON(),css:{transform:s.transform,zoom:s.zoom,left:s.left,top:s.top,width:s.width,height:s.height,translate:s.translate},inline:n.style.cssText};}));await page.screenshot({path:path.join(process.env.HUD_ARTIFACTS||__dirname,'hud-failure.png')});}
|
||||
assert.equal(afterTouch.submitted,beforeTouch+1,'touch pointer drag submits once: '+JSON.stringify(afterTouch));checks++;
|
||||
await page.evaluate(()=>{
|
||||
const group=document.createElement('div');group.className='control';group.style.opacity='1';
|
||||
const button=document.createElement('div');button.textContent='动态技能';button.addEventListener('click',()=>{window.dynamicSkillUsed=true;});group.appendChild(button);ui.control.replaceChildren(group);hud.refresh();
|
||||
});
|
||||
await page.getByText('动态技能',{exact:true}).click();
|
||||
assert.equal(await page.evaluate(()=>window.dynamicSkillUsed),true,'new native controls retain their callbacks');checks++;
|
||||
await page.evaluate(()=>{game.players[7].hp=0;hud.refresh();});
|
||||
assert.equal(await page.evaluate(()=>game.players[7].style.cssText===originalEnemyPresentation[6].style&&!game.players[7].classList.contains('mengsan-hud-defeated-shuying')),true,'death keeps the original battlefield presentation');checks++;
|
||||
await page.evaluate(()=>{game.players[7].hp=40;game.me.storage.mengsanVulnerable_shuying=3;hud.refresh();});
|
||||
assert.equal(await page.locator('.mengsan-hud-statuses-shuying').innerText(),'','status chips contain icons only');checks++;
|
||||
const heldStatus = page.locator('.mengsan-hud-statuses-shuying button').first();
|
||||
await heldStatus.dispatchEvent('pointerdown', {button:0,pointerId:45,
|
||||
pointerType:'touch',clientX:10,clientY:10});
|
||||
await page.waitForTimeout(150);
|
||||
await page.evaluate(() => {
|
||||
game.me.storage.mengsanWeak_shuying=1; hud.refresh();
|
||||
});
|
||||
await page.waitForTimeout(450);
|
||||
assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),1,'unrelated status update preserves an active hold');checks++;
|
||||
await page.evaluate(() => window.dispatchEvent(
|
||||
new PointerEvent('pointerup', {pointerId:45,pointerType:'touch'})));
|
||||
await page.evaluate(() => {
|
||||
game.me.storage.mengsanVulnerable_shuying=5; hud.refresh();
|
||||
});
|
||||
assert.match(await page.locator('dialog.mengsan-hud-dialog-shuying').innerText(),/易伤 5/,'open details show the current value');checks++;
|
||||
await page.evaluate(() => {
|
||||
delete game.me.storage.mengsanVulnerable_shuying; hud.refresh();
|
||||
});
|
||||
assert.match(await page.locator('dialog.mengsan-hud-dialog-shuying').innerText(),/已失效/,'removed state is explicitly expired');checks++;
|
||||
await page.getByRole('button',{name:'关闭',exact:true}).click();
|
||||
await page.evaluate(() => {
|
||||
delete game.me.storage.mengsanWeak_shuying;
|
||||
game.me.storage.mengsanVulnerable_shuying=3; hud.refresh();
|
||||
});
|
||||
await page.evaluate(()=>{for(const key of ['Weak','Frail','Artifact','Constrict','Tangled','Shrink','Plow','Slippery'])game.me.storage['mengsan'+key+'_shuying']=2;hud.refresh();});
|
||||
assert.equal(await page.evaluate(()=>{const row=document.querySelector('.mengsan-hud-statuses-shuying').getBoundingClientRect();return [...document.querySelectorAll('.mengsan-hud-statuses-shuying button')].every(n=>{const r=n.getBoundingClientRect();return r.bottom<=row.bottom+1&&r.right<=row.right+1;});}),true,'matrix overflow keeps the visible statuses and more button inside the grid');checks++;
|
||||
// Compact icon chips fit more states; register enough real display fields
|
||||
// to exercise overflow on wide screens as well as phones.
|
||||
await page.evaluate(async prefix => {
|
||||
const {bindCombatStatusSource} = await import(prefix+'/battle/status-sources.js');
|
||||
const keys=['turnStrength','turnEnergy','turnBlock','turnLoseHp',
|
||||
'infernoLoseHp','lossDamage','lossStrength','vulnerableBonus',
|
||||
'attackBlock','retaliation','colossus','freeAttack','noDraw',
|
||||
'noEnergy','doubleTap','plating','exhaustBlock','exhaustDraw',
|
||||
'vulnerableDraw','juggle','endAttack','retrieveAttack','bodyguard',
|
||||
'autoStrike','firstBlock','blockDamage','barricade','corruption'];
|
||||
bindCombatStatusSource(game.me,session,'overflow-test',() => ({
|
||||
powers:Object.fromEntries(keys.map(key => [key,1])),
|
||||
}));
|
||||
hud.refresh();
|
||||
},prefix);
|
||||
await holdStatus(page,page.getByRole('button',{
|
||||
name:'点击查看全部状态',exact:true}));
|
||||
assert.ok(await page.getByRole('dialog').isVisible(),'overflow states remain available in the details');checks++;
|
||||
await page.getByRole('button',{name:'关闭',exact:true}).click();
|
||||
assert.equal(await page.evaluate(()=>{const s=document.querySelector('.mengsan-hud-center-shuying').getBoundingClientRect(),p=game.me.getBoundingClientRect();return s.bottom<=p.top&&p.top-s.bottom<=13;}),true,'status chips automatically appear just above the portrait');checks++;
|
||||
await page.locator('.mengsan-hud-statuses-shuying button').first().tap();
|
||||
assert.equal(await page.locator('dialog.mengsan-hud-dialog-shuying').count(),1,'short tap opens the status list');checks++;
|
||||
const statusList = await page.locator('.mengsan-hud-detail-body-shuying').evaluate(body => ({
|
||||
rows:body.querySelectorAll('details').length,
|
||||
position:getComputedStyle(body).position,
|
||||
minHeight:Math.min(...[...body.querySelectorAll('summary')].map(row=>row.getBoundingClientRect().height)),
|
||||
height:body.getBoundingClientRect().height,
|
||||
scrollable:body.scrollHeight>body.clientHeight,
|
||||
}));
|
||||
assert.ok(statusList.rows>20&&statusList.position!=='absolute'&&statusList.minHeight>=48&&statusList.height>0&&statusList.scrollable,'status list keeps all entries in normal flow with large tap targets: '+JSON.stringify(statusList));checks++;
|
||||
await page.getByRole('button',{name:'关闭',exact:true}).click();
|
||||
await holdStatus(page,page.locator('.mengsan-hud-statuses-shuying button').first());
|
||||
assert.ok(await page.locator('dialog.mengsan-hud-dialog-shuying').isVisible());checks++;
|
||||
await page.getByRole('button',{name:'关闭',exact:true}).click();
|
||||
await holdStatus(page,page.locator('.mengsan-hud-statuses-shuying button').first(),'touch');
|
||||
assert.ok(await page.locator('dialog.mengsan-hud-dialog-shuying').isVisible(),'touch opens the player status details');checks++;
|
||||
await page.getByRole('button',{name:'关闭',exact:true}).click();
|
||||
await page.evaluate(()=>game.uncheck());
|
||||
const prompt=await page.evaluate(()=>{
|
||||
const n=document.createElement('div');n.className='dialog';n.innerHTML='<div class="content-container"><div class="content"><div class="caption">请弃置两张手牌</div></div></div>';ui.arena.appendChild(n);ui.dialog=n;hudEventState.event.dialog=n;window.testPrompt=n;hud.refresh();
|
||||
return {r:n.getBoundingClientRect().toJSON(),control:ui.control.getBoundingClientRect().toJSON()};
|
||||
});
|
||||
assert.ok(prompt.r.bottom<=prompt.control.top-5,'native selection prompt stays above the horizontal action row');checks++;
|
||||
await page.locator('.handcards>.card').first().click({button:'right'});
|
||||
await page.waitForTimeout(80);
|
||||
const intro=await page.evaluate(()=>({opened:introOpens,box:testIntro.getBoundingClientRect().toJSON(),content:testIntro.querySelector('.content').getBoundingClientRect().toJSON(),hand:ui.me.getBoundingClientRect().toJSON(),prompt:testPrompt.getBoundingClientRect().toJSON(),z:Number(getComputedStyle(testIntro).zIndex),scroll:getComputedStyle(testIntro).overflowY}));
|
||||
assert.equal(intro.opened,1,'right click still invokes the original card introduction');checks++;
|
||||
assert.ok(intro.box.height>=40,'native absolute content wrapper does not collapse the description');checks++;
|
||||
assert.ok(intro.content.height>20&&intro.content.top>=intro.box.top&&intro.content.top<intro.box.bottom,'description content starts inside its scrollable popup');checks++;
|
||||
assert.ok(intro.box.bottom<=intro.hand.y-8&&intro.box.bottom<=intro.prompt.top&&intro.box.x>=8&&intro.box.right<=viewport.width-8,'description stays above hand and prompt, inside the viewport: '+JSON.stringify(intro));checks++;
|
||||
assert.ok(intro.z>24&&intro.scroll==='auto','description draws above cards and scrolls when height is limited');checks++;
|
||||
if(viewport.width===1234){await page.waitForTimeout(350);await page.screenshot({path:path.join(process.env.HUD_ARTIFACTS||__dirname,'hud-card-description-'+skin+'.png')});}
|
||||
await page.locator('.poplayer').click({position:{x:4,y:4}});
|
||||
assert.equal(await page.evaluate(()=>window.nativeIntroClosed),true,'original popup backdrop still closes the description');checks++;
|
||||
await page.evaluate(()=>{testPrompt.remove();delete ui.dialog;delete hudEventState.event.dialog;hud.refresh();});
|
||||
if(viewport.width===1234||skin==='native'&&viewport.width===812) {
|
||||
await page.evaluate(()=>{
|
||||
for(let i=0;i<6;i++) {
|
||||
const c=i===0?card:card.cloneNode(true);c.textContent=['杀','八卦阵','杀','桃','杀','无懈可击'][i];c.style.background='#e9dfc9 url("/image/card/'+(['sha','bagua','sha','tao','sha','wuxie'][i])+'.png") center/100% 100%';
|
||||
if(i)game.me.node.handcards1.appendChild(c);
|
||||
}
|
||||
if(ui.arena.clientWidth>1000){game.players.slice(4).forEach(p=>{p.hp=0;p.remove();battle.players.delete(p);});delete game.me.storage.mengsanVulnerable_shuying;}
|
||||
testFan.sync(true);hud.refresh();
|
||||
});
|
||||
await page.waitForTimeout(180);
|
||||
const fullCards=await page.locator('.handcards>.card').evaluateAll(nodes=>nodes.map(n=>n.getBoundingClientRect().toJSON()));
|
||||
assert.ok(fullCards.every(r=>r.bottom<=viewport.height-8),'rotated hand cards remain fully above the bottom edge: '+JSON.stringify(fullCards));checks++;
|
||||
await page.screenshot({path:path.join(process.env.HUD_ARTIFACTS||__dirname,'hud-transparent-'+skin+'-'+viewport.width+'x'+viewport.height+'.png')});
|
||||
}
|
||||
const popup=await page.evaluate(()=>{
|
||||
const n=document.createElement('div');n.className='damage skill-popup';n.style.cssText=`position:absolute;left:${game.me.offsetLeft}px;top:${game.me.offsetTop+80}px;width:${game.me.offsetWidth}px;z-index:9999`;
|
||||
for(const char of '回合开始'){const s=document.createElement('span');s.textContent=char;n.appendChild(s);}window.testPopup=n;window.testPopupTop=n.style.top;ui.arena.appendChild(n);hud.refresh();
|
||||
const rect=n.getBoundingClientRect(),footer=document.querySelector('.mengsan-hud-shuying').getBoundingClientRect();
|
||||
return {bottom:rect.bottom,footer:footer.top};
|
||||
});
|
||||
assert.ok(popup.bottom<=popup.footer-4,'skin notification stays above the command footer: '+JSON.stringify(popup));checks++;
|
||||
await page.evaluate(()=>{testHandUI.freeze();hud.freeze();session.active=false;testHandUI.refresh();hud.refresh();});
|
||||
await page.waitForTimeout(240);
|
||||
assert.equal(await page.evaluate(()=>document.querySelector('.mengsan-hud-shuying')!==null&&game.me.classList.contains('mengsan-hud-self-shuying')&&game.me.node.handcards1.classList.contains('mengsan-hand-layout-shuying')),true,'stopped battle retains player HUD and fan until reward cleanup');checks++;
|
||||
await page.evaluate(()=>{hud.dispose();hud.dispose();});
|
||||
assert.equal(await page.evaluate(()=>game.me.node.equips.parentNode===originalEquipParent&&!game.me.node.equips.classList.contains('mengsan-hud-equips-shuying')&&!game.me.node.equips.style.getPropertyValue('top')),true,'equipment ownership and styles restore');checks++;
|
||||
assert.equal(await page.evaluate(()=>!testPopup.classList.contains('mengsan-hud-notification-shuying')&&testPopup.style.top===testPopupTop),true,'notification placement is restored when HUD ends');checks++;
|
||||
assert.equal(await page.locator('.mengsan-hud-shuying').count(),0);checks++;
|
||||
assert.equal(await page.evaluate(()=>ui.me.parentNode===oldParents.hand&&ui.control.parentNode===oldParents.control&&!document.body.classList.contains('mengsan-hud-active-shuying')),true);checks++;
|
||||
assert.equal(await page.evaluate(()=>game.players.slice(1).every((p,i)=>p.style.cssText===originalEnemyPresentation[i].style)),true,'native actor styles survive final cleanup');checks++;
|
||||
await page.evaluate(()=>releaseAll());
|
||||
console.log(skin+' '+viewport.width+'x'+viewport.height+' passed');
|
||||
await page.close();
|
||||
}
|
||||
}
|
||||
console.log('HUD browser checks: '+checks);
|
||||
} finally {await browser.close();await new Promise(resolve=>server.close(resolve));}
|
||||
})().catch(error=>{console.error(error);server.close();process.exitCode=1;});
|
||||
@@ -0,0 +1,58 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const path = require('node:path');
|
||||
const moduleAt = name => import(pathToFileURL(path.join(__dirname, '..', name)).href)
|
||||
.catch(error => { if (error.code === 'ERR_MODULE_NOT_FOUND') return {}; throw error; });
|
||||
|
||||
test('HUD reads strength, duration and charges without changing combat storage', async () => {
|
||||
const { collectCombatStatuses } = await moduleAt('ui/hud/status-model.js');
|
||||
assert.equal(typeof collectCombatStatuses, 'function');
|
||||
const player = { storage: { mengsanStrength_shuying: 3, mengsanSetupStrength_shuying: 2,
|
||||
mengsanVulnerable_shuying: 2, mengsanArtifact_shuying: 1, mengsanRinging_shuying: true, mengsanRingingUsed_shuying: 1 } };
|
||||
const before = structuredClone(player.storage);
|
||||
const statuses = collectCombatStatuses(player, { session: { active: true } });
|
||||
assert.equal(statuses.find(s => s.id === 'strength').value, 5);
|
||||
assert.equal(statuses.find(s => s.id === 'vulnerable').unit, '回合');
|
||||
assert.equal(statuses.find(s => s.id === 'artifact').unit, '次');
|
||||
assert.match(statuses.find(s => s.id === 'ringing').detail, /响应/);
|
||||
assert.deepEqual(player.storage, before);
|
||||
});
|
||||
|
||||
test('private effect snapshots show signed strength and separate countdown instances', async () => {
|
||||
const { collectCombatStatuses } = await moduleAt('ui/hud/status-model.js');
|
||||
const { bindCombatStatusSource } = await moduleAt('battle/status-sources.js');
|
||||
assert.equal(typeof bindCombatStatusSource, 'function');
|
||||
const releases = [], session = { active: true, ownResource: (_, release) => releases.push(release) };
|
||||
const player = { storage: { mengsanStrength_shuying: 2 } };
|
||||
const state = { powers: { barricade: true, vulnerableDraw: 2 }, negativeStrength: 5,
|
||||
bombs: [{ turns: 1, damage: 40 }, { turns: 3, damage: 50 }] };
|
||||
bindCombatStatusSource(player, session, 'shared', () => state);
|
||||
const result = collectCombatStatuses(player, { session });
|
||||
assert.equal(result.find(s => s.id === 'strength').value, -3);
|
||||
assert.equal(result.find(s => s.name === '凶恶').value, 2);
|
||||
assert.deepEqual(result.filter(s => s.name === '炸弹').map(s => s.value), [1, 3]);
|
||||
result.find(s => s.id === 'strength').value = 99;
|
||||
assert.equal(player.storage.mengsanStrength_shuying, 2);
|
||||
releases.forEach(release => release());
|
||||
assert.equal(collectCombatStatuses(player, { session }).filter(s => s.name === '炸弹').length, 0);
|
||||
});
|
||||
|
||||
test('old session release does not remove a new battle status source', async () => {
|
||||
const { bindCombatStatusSource, readCombatStatusSources } = await moduleAt('battle/status-sources.js');
|
||||
assert.equal(typeof bindCombatStatusSource, 'function');
|
||||
let releaseOld;
|
||||
const old = { active: true, ownResource: (_, release) => { releaseOld = release; } };
|
||||
const next = { active: true, ownResource() {} }, player = {};
|
||||
bindCombatStatusSource(player, old, 'cards', () => ({ vigor: 2 }));
|
||||
bindCombatStatusSource(player, next, 'cards', () => ({ vigor: 7 }));
|
||||
releaseOld();
|
||||
assert.equal(readCombatStatusSources(player, next)[0].state.vigor, 7);
|
||||
assert.equal(readCombatStatusSources(player, old).length, 0);
|
||||
});
|
||||
|
||||
test('slow display uses layers as well as the current card counter', async () => {
|
||||
const { collectCombatStatuses } = await moduleAt('ui/hud/status-model.js');
|
||||
const player={storage:{mengsanSlow_shuying:3,mengsanSlowCards_shuying:2},hasSkill:id=>id==='mengsan_slow_shuying'};
|
||||
assert.equal(collectCombatStatuses(player,{session:{active:true}}).find(s=>s.id==='slow').value,60);
|
||||
});
|
||||
@@ -116,6 +116,7 @@ export function openCardLibrary(run, options = {}) {
|
||||
const existing = document.querySelector("dialog.mengsan-deck-dialog-shuying[open]");
|
||||
if (existing) { existing.focus(); return () => {}; }
|
||||
const previousFocus = document.activeElement;
|
||||
const selection = options.selection;
|
||||
const dialog = element("dialog", "mengsan-deck-dialog-shuying", null);
|
||||
dialog.setAttribute("aria-label", options.title || "卡牌库");
|
||||
const header = element("header", "ms-deck-header", null, dialog);
|
||||
@@ -143,12 +144,13 @@ export function openCardLibrary(run, options = {}) {
|
||||
onClose?.(close);
|
||||
};
|
||||
button("关闭", header, close);
|
||||
let view = !options.sections && options.view === "catalog" ? "catalog" : "deck";
|
||||
let view = selection || (!options.sections && options.view === "catalog")
|
||||
? "catalog" : "deck";
|
||||
const navigation = element("nav", "ms-deck-tabs", null, dialog);
|
||||
navigation.setAttribute("aria-label", "卡牌列表视图");
|
||||
const deckTab = button("持有牌", navigation, () => { view = "deck"; render(); });
|
||||
const catalogTab = button("图鉴", navigation, () => { view = "catalog"; render(); });
|
||||
deckTab.hidden = catalogTab.hidden = Boolean(options.sections);
|
||||
deckTab.hidden = catalogTab.hidden = Boolean(options.sections || selection);
|
||||
let sectionIndex = 0;
|
||||
const sectionTabs = options.sections ? options.sections().map((section, index) =>
|
||||
button(section.title, navigation, () => { sectionIndex = index; render(); })) : [];
|
||||
@@ -222,6 +224,24 @@ export function openCardLibrary(run, options = {}) {
|
||||
element("p", "ms-deck-rules", details.description, copy);
|
||||
if (!details.available) element("p", "ms-deck-warning", "该卡牌定义未加载;保留存档记录,不删除此牌。", copy);
|
||||
for (const affix of details.affixes) element("p", "ms-deck-rules", affix, copy);
|
||||
if (catalog && selection) {
|
||||
const reason = !details.available ? "该卡牌定义未加载。" :
|
||||
selection.unavailableReason?.(card) || "";
|
||||
const feedback = element("p", "ms-deck-rules", reason, copy);
|
||||
feedback.setAttribute("role", "status");
|
||||
const confirm = button(selection.label, copy, () => {
|
||||
if (closed || confirm.disabled) return;
|
||||
confirm.disabled = true;
|
||||
try {
|
||||
selection.onSelect(card);
|
||||
close();
|
||||
} catch (error) {
|
||||
feedback.textContent = error.message || "添加失败,请重试。";
|
||||
confirm.disabled = false;
|
||||
}
|
||||
});
|
||||
confirm.disabled = Boolean(reason);
|
||||
}
|
||||
body.scrollTop = 0; backButton.focus();
|
||||
}
|
||||
function fitCards() {
|
||||
@@ -296,7 +316,8 @@ export function openCardLibrary(run, options = {}) {
|
||||
summary.textContent = options.sections ? "剩余牌堆按从左到右、从上到下的顺序摸取;第 1 张为下一张。" :
|
||||
`本次征程实际携带 · 共 ${current.length} 张 · 同名不同花色、点数分别展示`;
|
||||
}
|
||||
footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" :
|
||||
if (catalog && selection) footer.textContent = selection.description;
|
||||
else footer.textContent = catalog ? "图鉴仅供查阅,不会获得卡牌或改变征程牌组 · 点击卡牌查看完整介绍" :
|
||||
"点击卡牌查看完整介绍 · 仅供查看,不改变牌组或抽牌顺序";
|
||||
virtualGrid.setRecords(catalog || options.sections ? records : current.map(card => ({ card })));
|
||||
if (!(catalog || options.sections ? records : current).length) {
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
import { AFFIX_INFO } from "../cards/affixes.js";
|
||||
import { hasCardAffix } from "../cards/intrinsic-affixes.js";
|
||||
import { describeLibraryCard } from "./card-library.js";
|
||||
import { describeLibraryCard, openCardLibrary } from "./card-library.js";
|
||||
import { cardDefinitions } from "../cards/card-definitions.js";
|
||||
import { createCardData } from "../cards/card-data.js";
|
||||
import { DAZED_NAME, createDazedData } from "../cards/status-cards.js";
|
||||
import { SLIMED_NAME, createSlimedData } from "../cards/slimed-card.js";
|
||||
import { WOUND_NAME, createWoundData } from "../cards/wound-card.js";
|
||||
import { INFECTION_NAME, createInfectionData } from "../cards/infection-card.js";
|
||||
import { getDiagnostics } from "../../diagnostics/index.js";
|
||||
import { _status } from "../../../../noname.js";
|
||||
|
||||
const statusFactories = {
|
||||
[DAZED_NAME]: createDazedData,
|
||||
[SLIMED_NAME]: createSlimedData,
|
||||
[WOUND_NAME]: createWoundData,
|
||||
[INFECTION_NAME]: createInfectionData,
|
||||
};
|
||||
|
||||
const node = (tag, className, label, parent) => {
|
||||
const element = document.createElement(tag);
|
||||
@@ -20,6 +35,10 @@ const button = (label, parent, action) => {
|
||||
// Keep the command battle-scoped; further GM actions can be added as panels here.
|
||||
export function mountGMManager(session, owner, piles, battle, resources, game) {
|
||||
let dialog = null, selected = null, previousFocus = null, disposed = false;
|
||||
let closePicker = null, sequence = 0, message = "";
|
||||
const diagnostics = getDiagnostics().scope("mengsan.gm", () => ({
|
||||
sessionId: session.id,
|
||||
}));
|
||||
const available = () => !disposed && session.active && !battle.resolving;
|
||||
const close = () => {
|
||||
if (!dialog) return;
|
||||
@@ -32,6 +51,7 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
|
||||
};
|
||||
const render = () => {
|
||||
if (!dialog) return;
|
||||
dialog.querySelector(".ms-gm-feedback").textContent = message;
|
||||
const list = dialog.querySelector(".ms-gm-hand");
|
||||
const editor = dialog.querySelector(".ms-gm-editor");
|
||||
list.replaceChildren(); editor.replaceChildren();
|
||||
@@ -60,9 +80,89 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
|
||||
item.title = info.description;
|
||||
}
|
||||
};
|
||||
const chooseTemporaryCard = () => {
|
||||
if (!available() || closePicker) return;
|
||||
close();
|
||||
closePicker = openCardLibrary(null, {
|
||||
title: "添加临时牌", eyebrow: "梦三 · GM",
|
||||
view: "catalog",
|
||||
selection: {
|
||||
label: "加入手牌 · 仅本场战斗",
|
||||
description: "选择卡牌查看详情并加入手牌;正常弃置和洗牌," +
|
||||
"战斗结束清理。默认黑桃 A、未强化。",
|
||||
unavailableReason: card =>
|
||||
Object.hasOwn(cardDefinitions, card.name) ||
|
||||
Object.hasOwn(statusFactories, card.name) ? "" :
|
||||
"该卡牌尚无可用的战斗创建规则。",
|
||||
onSelect(card) {
|
||||
if (!available() || !owner.isAlive()) {
|
||||
throw new Error("当前战斗已结束或角色已阵亡,无法添加。");
|
||||
}
|
||||
if (!describeLibraryCard(card).available) {
|
||||
throw new Error("该卡牌定义未加载。");
|
||||
}
|
||||
const id = `mengsan_gm_${session.id}_${++sequence}`;
|
||||
const factory = Object.hasOwn(statusFactories, card.name)
|
||||
? statusFactories[card.name] : null;
|
||||
const data = factory ? factory(id) : createCardData({
|
||||
id, name: card.name, suit: "spade", number: 1,
|
||||
nature: null, affixes: [], upgrade: 0,
|
||||
}, { enemy: true });
|
||||
// GM injection must not await the paused native event queue.
|
||||
const created = piles.addToDiscard(data);
|
||||
if (!created) throw new Error("当前战斗无法创建临时牌。");
|
||||
try {
|
||||
owner.directgain([created]);
|
||||
if (!owner.getCards("h").includes(created)) {
|
||||
throw new Error("临时牌未能加入手牌。");
|
||||
}
|
||||
} catch (error) {
|
||||
created.remove();
|
||||
diagnostics.error("temporaryCard.failed", error, {
|
||||
cardName: data.name, cardId: id,
|
||||
});
|
||||
game.log("GM 临时牌添加失败", data.name, id,
|
||||
error.message);
|
||||
throw error;
|
||||
}
|
||||
selected = created;
|
||||
message = `已将【${describeLibraryCard(data).name}】加入手牌,` +
|
||||
"仅本场战斗有效。";
|
||||
game.log("GM 添加临时牌", owner, data.name, id,
|
||||
session.id);
|
||||
diagnostics.info("temporaryCard.added", {
|
||||
cardName: data.name, cardId: id,
|
||||
});
|
||||
try {
|
||||
// check() reuses the choices from before directgain.
|
||||
// Invalidate them without clearing the user's selection.
|
||||
const event = _status.event;
|
||||
if (event?.player === owner) {
|
||||
delete event._cardChoice;
|
||||
delete event._targetChoice;
|
||||
delete event._skillChoice;
|
||||
}
|
||||
game.check();
|
||||
}
|
||||
catch (error) {
|
||||
game.log("GM 临时牌已入手,选择状态刷新失败",
|
||||
error.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
onClose() {
|
||||
closePicker = null;
|
||||
if (available()) open();
|
||||
},
|
||||
});
|
||||
};
|
||||
const open = () => {
|
||||
if (!available()) return false;
|
||||
if (closePicker) return true;
|
||||
if (dialog?.open) { render(); dialog.focus(); return true; }
|
||||
const existing = document.querySelector(
|
||||
"dialog.mengsan-deck-dialog-shuying[open]");
|
||||
if (existing) { existing.focus(); return false; }
|
||||
previousFocus = document.activeElement;
|
||||
dialog = node("dialog", "mengsan-deck-dialog-shuying mengsan-gm-dialog-shuying", null);
|
||||
dialog.setAttribute("aria-label", "梦三 GM 管理器");
|
||||
@@ -70,8 +170,12 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
|
||||
const header = node("header", "ms-deck-header", null, dialog);
|
||||
node("h2", "ms-deck-title", "GM 管理器", header);
|
||||
button("关闭", header, close);
|
||||
node("p", "ms-deck-summary", "手牌词缀 · 仅本场战斗生效,不改永久牌组或存档。", dialog);
|
||||
node("p", "ms-deck-summary",
|
||||
"手牌词缀 / 添加临时牌 · 仅本场战斗生效,不改永久牌组或存档。", dialog);
|
||||
button("添加临时牌", dialog, chooseTemporaryCard);
|
||||
button("刷新手牌", dialog, render);
|
||||
const feedback = node("p", "ms-gm-feedback", "", dialog);
|
||||
feedback.setAttribute("role", "status");
|
||||
node("div", "ms-gm-hand", null, dialog);
|
||||
node("section", "ms-gm-editor", null, dialog);
|
||||
const created = dialog;
|
||||
@@ -85,7 +189,11 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
|
||||
};
|
||||
resources.field(globalThis, "mengsanGM_shuying", open);
|
||||
resources.field(game, "mengsanGM_shuying", open);
|
||||
const handle = { dispose: close };
|
||||
session.ownResource(handle, () => { disposed = true; close(); });
|
||||
const handle = { dispose() {
|
||||
disposed = true;
|
||||
closePicker?.(); closePicker = null;
|
||||
close();
|
||||
} };
|
||||
session.ownResource(handle, handle.dispose);
|
||||
return handle;
|
||||
}
|
||||
|
||||