2 Commits
Author SHA1 Message Date
Diuse 8848f6524c fixed # 不想写了 2026-10-30 00:55:36 +08:00
Diuse d5715c9f5a fixed # 修复状态列表异常的bug 2026-10-29 15:21:11 +08:00
13 changed files with 730 additions and 40 deletions
+2
View File
@@ -49,3 +49,5 @@ Desktop.ini
/.comet/*
!/.comet/config.yaml
# <<< Comet managed project state <<<
# Added by code-review-graph
.code-review-graph/
+46
View File
@@ -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 -->
+1 -1
View File
@@ -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.7";
const shuYingLocalVersion = "2.0.2.3-rc.8";
shuYing.name = "术樱包";
shuYing.editable = false;
@@ -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机制差异,并用相同资源约束同时验证输出与生存。**
+41 -2
View File
@@ -152,6 +152,36 @@ const server = http.createServer((req, res) => {
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++;
@@ -322,12 +352,21 @@ const server = http.createServer((req, res) => {
hud.refresh();
},prefix);
await holdStatus(page,page.getByRole('button',{
name:'长按查看全部状态',exact:true}));
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(),0,'short tap does not open status details');checks++;
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();
+12 -1
View File
@@ -8,6 +8,7 @@ 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,
@@ -132,7 +133,17 @@ export function mountGMManager(session, owner, piles, battle, resources, game) {
diagnostics.info("temporaryCard.added", {
cardName: data.name, cardId: id,
});
try { game.check(); }
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);
+32 -6
View File
@@ -69,7 +69,8 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log
event.name !== "chooseToUse" || event.player !== player || !event.isMine?.() ||
event.skill || event.complexSelect || event.custom?.replace?.card || event.custom?.replace?.target || event.custom?.replace?.confirm ||
!env.isActiveCardUse(event, player) || !env.canPayCard?.(player, card) ||
ui.selected.cards.length || ui.selected.targets.length || ui.selected.buttons.length ||
(ui.selected.cards.length && (ui.selected.cards.length !== 1 || ui.selected.cards[0] !== card)) ||
ui.selected.targets.length || ui.selected.buttons.length ||
!card.classList.contains("selectable") || card.classList.contains("noclick") ||
status.mousedragging || status.dragged || status.paused2) return false;
const cards = get.select(event.selectCard);
@@ -83,19 +84,22 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log
aim.setAttribute("aria-hidden", "true");
aim.setAttribute("preserveAspectRatio", "none");
const path = document.createElementNS(aim.namespaceURI, "path");
aim.appendChild(path); document.body.appendChild(aim);
// Keep the SVG in the same zoom/transform space as the real hand.
// Android WebView can paint a body-fixed SVG in a different viewport.
aim.appendChild(path); ui.arena.appendChild(aim);
}
const rect = card.getBoundingClientRect();
const x = rect.left + rect.width / 2, y = rect.top + 12;
aim.setAttribute("viewBox", `0 0 ${window.innerWidth} ${window.innerHeight}`);
const width = ui.arena.clientWidth, height = ui.arena.clientHeight;
aim.setAttribute("viewBox", `0 0 ${width} ${height}`);
// Use painted bounds instead of relying on WebView CTM handling of zoom.
// With preserveAspectRatio=none, viewport-to-viewBox scaling is linear.
const bounds = aim.getBoundingClientRect();
if (!(bounds.width > 0 && bounds.height > 0)) {
removeAim(); return;
}
const scaleX = window.innerWidth / bounds.width;
const scaleY = window.innerHeight / bounds.height;
const scaleX = width / bounds.width;
const scaleY = height / bounds.height;
const start = { x: (x - bounds.left) * scaleX,
y: (y - bounds.top) * scaleY };
const end = { x: (point.x - bounds.left) * scaleX,
@@ -163,7 +167,7 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log
if (!state.moved && Math.hypot(point.x - state.x, point.y - state.y) < 9) return;
if (!state.moved) {
state.moved = true;
engineCall(() => ui.click.card.call(state.card));
if (!ui.selected.cards.includes(state.card)) engineCall(() => ui.click.card.call(state.card));
state.selected = ui.selected.cards.includes(state.card);
const targets = range(state.event);
if (!state.selected || !live(state) || !targets) { cleanup(true); return; }
@@ -235,6 +239,28 @@ export function createHandInput(player, session, adapter, fan, env, refresh, log
for (const type of ["mousedown", "mousemove", "mouseup", "click", "touchstart", "touchmove", "touchend"]) {
listen(window, type, compatibility, { capture: true, passive: false });
}
} else if (adapter.root && status && get) {
// Older Android WebViews expose TouchEvent but no PointerEvent.
// Route them through the same selection and drawing code.
const touchEvent = (event, touch, type) => ({
button: 0, isPrimary: true, pointerId: touch.identifier,
target: event.target, clientX: touch.clientX, clientY: touch.clientY,
type, preventDefault: () => event.preventDefault(),
stopImmediatePropagation: () => event.stopImmediatePropagation(),
});
const options = { capture: true, passive: false };
listen(window, "touchstart", event => {
if (event.touches.length !== 1) { cancel(); return; }
start(touchEvent(event, event.touches[0], "pointerdown"));
}, options);
for (const [type, handler] of [["touchmove", move], ["touchend", end], ["touchcancel", end]]) {
listen(window, type, event => {
const touch = Array.from(event.changedTouches).find(item => item.identifier === gesture?.id);
if (touch) handler(touchEvent(event, touch, type === "touchcancel" ? "pointercancel" : type));
}, options);
}
listen(window, "click", compatibility, options);
listen(window, "blur", cancel);
}
listen(adapter.root, "pointerover", hover);
listen(adapter.root, "pointerleave", () => { if (!gesture) fan.setHover(null); });
+3 -3
View File
@@ -77,10 +77,10 @@
transform-origin: 50% 50% !important;
}
.mengsan-hand-aim-shuying {
position: fixed;
position: absolute;
inset: 0;
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
z-index: 1000;
pointer-events: none;
overflow: visible;
+46 -17
View File
@@ -8,11 +8,24 @@ export function mountBattleHUD(player, battle, env) {
if (!arena || !ui.me || !ui.control || !battle.session.active) throw new Error('梦三 HUD 缺少原生界面容器');
const handRoot = ui.me, controlRoot = ui.control;
const overlays = createOverlayLayout(arena, handRoot, ui, player);
const restores = [], styled = new WeakMap(), hiddenMarks = new Set();
const restores = [], nodeRestores = new Map(), styled = new WeakMap(), hiddenMarks = new Set();
let disposed = false, frozen = false, frame = null, dialog = null, previousFocus = null, resize, observer;
const retain = (node, restore) => {
let releases = nodeRestores.get(node);
if (!releases) nodeRestores.set(node, releases = []);
releases.push(restore);
};
const releaseNodes = (detachedOnly = false) => {
for (const [node, releases] of nodeRestores) {
if (detachedOnly && node.isConnected) continue;
for (const restore of releases.reverse()) restore();
nodeRestores.delete(node);
styled.delete(node);
}
};
const ownClass = (node, name) => {
if (!node || node.classList.contains(name)) return;
node.classList.add(name); restores.push(() => node.classList.remove(name));
node.classList.add(name); retain(node, () => node.classList.remove(name));
};
const style = (node, key, value) => {
let values = styled.get(node);
@@ -20,7 +33,7 @@ export function mountBattleHUD(player, battle, env) {
if (!values.has(key)) {
const old = node.style.getPropertyValue(key), priority = node.style.getPropertyPriority(key);
const record = { last: null }; values.set(key, record);
restores.push(() => {
retain(node, () => {
if (node.style.getPropertyValue(key) !== record.last) return;
if (old) node.style.setProperty(key, old, priority); else node.style.removeProperty(key);
});
@@ -67,29 +80,40 @@ export function mountBattleHUD(player, battle, env) {
};
const refreshDetails = () => {
if (!dialog || !detailBody) return;
const entries = collectCombatStatuses(detailTarget, battle)
.filter(entry => detailId === null || entry.id === detailId);
const entries = collectCombatStatuses(detailTarget, battle);
const signature = JSON.stringify(entries);
if (signature === detailSignature) return;
const expanded = detailSignature === null ? new Set(detailId ? [detailId] : []) :
new Set([...detailBody.querySelectorAll('details[open]')].map(row => row.dataset.statusId));
detailSignature = signature; detailBody.replaceChildren();
if (detailId && !entries.some(entry => entry.id === detailId)) {
create('p', '', detailBody, '该状态已失效。');
}
for (const entry of entries) {
const row = create('section', 'mengsan-hud-detail-row-shuying',
const row = create('details', 'mengsan-info-row-shuying',
detailBody);
create('strong', '', row, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`);
row.dataset.statusId = entry.id;
row.open = expanded.has(entry.id);
const summary = create('summary', '', row);
if (entry.icon) {
const icon = create('img', 'mengsan-relic-icon-shuying', summary);
icon.src = `${assetBase}/assets/status/${entry.icon}`; icon.alt = '';
}
create('strong', '', summary, `${entry.name}${entry.value == null ? '' : ` ${entry.value}${entry.unit ? ` ${entry.unit}` : ''}`}`);
create('p', '', row, entry.detail);
}
if (!entries.length) create('p', '', detailBody,
detailId === null ? '暂无已登记状态。' : '该状态已失效。');
if (!entries.length && !detailId) create('p', '', detailBody, '暂无已登记状态。');
};
const openDetails = (target, id = null) => {
if (disposed || frozen || !battle.session.active) return;
closeDialog(); previousFocus = document.activeElement;
dialog = create('dialog', 'mengsan-hud-dialog-shuying', document.body);
dialog = create('dialog', 'mengsan-hud-dialog-shuying mengsan-deck-dialog-shuying mengsan-relic-dialog-shuying', document.body);
const current = dialog;
create('h2', '', dialog, `${nameOf(target)} · 当前状态`);
const close = create('button', 'mengsan-hud-dialog-close-shuying', dialog, '关闭'); close.addEventListener('click', closeDialog);
const header = create('header', 'ms-deck-header', dialog);
create('h2', '', header, `${nameOf(target)} · 当前状态`);
const close = create('button', 'ms-deck-button', header, '关闭'); close.addEventListener('click', closeDialog);
detailTarget = target; detailId = id;
detailBody = create('div', 'mengsan-hud-detail-body-shuying', dialog);
detailBody = create('div', 'mengsan-hud-detail-body-shuying mengsan-relic-list-shuying', dialog);
refreshDetails();
// Unknown/native marks retain their engine intro and are never hidden by the HUD.
dialog.addEventListener('cancel', event => { event.preventDefault(); closeDialog(); });
@@ -149,8 +173,8 @@ export function mountBattleHUD(player, battle, env) {
statuses.addEventListener('click', event => {
event.preventDefault();
const button = statusButton(event);
// Pointer clicks require a hold; keyboard activation stays accessible.
if (button && event.detail === 0) {
// Every entry opens the whole list; precise long presses are optional.
if (button && !dialog) {
openDetails(player, button.dataset.statusId || null);
}
});
@@ -173,7 +197,7 @@ export function mountBattleHUD(player, battle, env) {
const button = existing.get(entry.id) ||
create('button', 'mengsan-hud-status-shuying', null);
button.dataset.statusId = entry.id;
button.setAttribute('aria-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},长按查看状态详情`);
button.setAttribute('aria-label', `${entry.name} ${entry.value ?? ''} ${entry.unit},点击查看状态列表`);
button.setAttribute('aria-haspopup', 'dialog');
if (button.dataset.icon !== (entry.icon || '')) {
button.dataset.icon = entry.icon || '';
@@ -195,7 +219,7 @@ export function mountBattleHUD(player, battle, env) {
const more = existing.get(null) || create('button',
'mengsan-hud-status-shuying mengsan-hud-status-more-shuying',
null);
more.setAttribute('aria-label', '长按查看全部状态');
more.setAttribute('aria-label', '点击查看全部状态');
more.setAttribute('aria-haspopup', 'dialog');
wantedButtons.push(more);
}
@@ -337,6 +361,9 @@ export function mountBattleHUD(player, battle, env) {
function refresh() {
if (disposed || frozen) return;
if (!battle.session.active || !arena.isConnected || !handRoot.isConnected || ui.me !== handRoot || ui.control !== controlRoot) { dispose(); return; }
// Closed native controls and expired popups must not stay retained for the whole battle.
// Restore them before forgetting their snapshots so reused nodes get a fresh baseline.
releaseNodes(true);
text(name, nameOf(player)); text(hp, `生命 ${player.hp} / ${player.maxHp}`); text(block, `格挡 ${player.hujia || 0}`);
healthFill.style.width = `${Math.max(0, Math.min(100, player.maxHp > 0 ? player.hp / player.maxHp * 100 : 0))}%`;
text(energy, `费用 ${player.storage.mengsanEnergy_shuying ?? 0} / ${player.storage.mengsanMaxEnergy_shuying ?? 0}`);
@@ -355,6 +382,7 @@ export function mountBattleHUD(player, battle, env) {
frozen=true;window.clearInterval(timer);
if(frame!==null){window.cancelAnimationFrame(frame);frame=null;}
observer?.disconnect();resize?.disconnect();closeDialog();overlays.dispose();
releaseNodes(true);
ownClass(arena,'mengsan-hud-frozen-shuying');
}
function dispose() {
@@ -367,6 +395,7 @@ export function mountBattleHUD(player, battle, env) {
closeDialog();
for (const mark of hiddenMarks) mark.classList.remove('mengsan-hud-represented-mark-shuying'); hiddenMarks.clear();
for (const restore of restores.reverse()) restore();
releaseNodes();
root.remove(); link.remove();
}
ownClass(document.body, 'mengsan-hud-active-shuying'); ownClass(arena, 'mengsan-hud-arena-shuying');
+2 -3
View File
@@ -2,7 +2,7 @@ export function mountRelicStrip(root, getRelics, open, createIcon,
document, window) {
const strip = document.createElement("div");
strip.className = "mengsan-hand-relics-shuying";
strip.setAttribute("aria-label", "持有遗物,长按查看描述");
strip.setAttribute("aria-label", "持有遗物,点击查看列表");
const before = root.style.getPropertyValue("--mengsan-relic-space");
const priority = root.style.getPropertyPriority("--mengsan-relic-space");
let pending = null, timer = null, signature = null, disposed = false;
@@ -38,7 +38,6 @@ export function mountRelicStrip(root, getRelics, open, createIcon,
const click = event => {
stop(event);
event.preventDefault();
if (event.detail !== 0) return;
const button = event.target.closest?.("button");
const relic = getRelics().find(item => item.id === button?.dataset.relicId);
if (relic) open(relic);
@@ -71,7 +70,7 @@ export function mountRelicStrip(root, getRelics, open, createIcon,
const button = document.createElement("button");
button.type = "button";
button.dataset.relicId = relic.id;
button.setAttribute("aria-label", `${relic.name},长按查看描述`);
button.setAttribute("aria-label", `${relic.name},点击查看遗物列表`);
button.setAttribute("aria-haspopup", "dialog");
const fallback = document.createElement("span");
fallback.textContent = relic.name;
+14 -7
View File
@@ -26,22 +26,29 @@ export function mountRelics(session, run, battleRelics, { stripRoot = ui.me } =
button.textContent = `遗物 ${relics.length}`;
strip?.refresh();
if (!list) return;
const expanded = new Set([...list.querySelectorAll('details[open]')].map(row => row.dataset.relicId));
if (!list.children.length && selected) expanded.add(selected);
list.replaceChildren();
if (!relics.length) {
const empty = document.createElement("p");
empty.textContent = "本次征程尚未获得遗物。";
list.appendChild(empty);
}
for (const relic of selected ? relics.filter(r => r.id === selected) : relics) {
const row = document.createElement("section");
const icon=createRelicIcon(relic.image);if(icon)row.appendChild(icon);
const name = document.createElement("h3");
for (const relic of relics) {
const row = document.createElement("details");
row.className = "mengsan-info-row-shuying";
row.dataset.relicId = relic.id;
row.open = expanded.has(relic.id);
const summary = document.createElement("summary");
const icon=createRelicIcon(relic.image);if(icon)summary.appendChild(icon);
const name = document.createElement("strong");
name.textContent = relic.name;
summary.appendChild(name);
const description = document.createElement("p");
description.textContent = relic.description;
const status = document.createElement("small");
status.textContent = `${relic.pool} · ${relic.tier}${relic.ancient ? ` · ${relic.ancient}` : ""} · ${battleRelics.status(relic)}`;
row.append(name, description, status);
row.append(summary, description, status);
list.appendChild(row);
}
};
@@ -53,11 +60,11 @@ export function mountRelics(session, run, battleRelics, { stripRoot = ui.me } =
const currentDialog = dialog;
dialog.className = "mengsan-deck-dialog-shuying " +
"mengsan-relic-dialog-shuying";
dialog.setAttribute("aria-label", relic?.name || "遗物(道具)");
dialog.setAttribute("aria-label", "遗物(道具)");
const header = document.createElement("header");
header.className = "ms-deck-header";
const title = document.createElement("h2");
title.textContent = relic?.name || "遗物(道具)";
title.textContent = "遗物(道具)";
const dismiss = document.createElement("button");
dismiss.type = "button";
dismiss.className = "ms-deck-button";
+7
View File
@@ -1434,6 +1434,13 @@ body.mengsan-battle-ui-shuying :is(
}
.mengsan-relic-dialog-shuying { max-width: 620px; }
.mengsan-relic-list-shuying { overflow-y: auto; padding: 12px 0; }
.mengsan-deck-dialog-shuying .mengsan-relic-list-shuying { position:relative;display:block;flex:1;min-height:0;overscroll-behavior:contain;touch-action:pan-y; }
.mengsan-info-row-shuying { display:block;border-bottom:1px solid #b9975d55;padding:8px 0; }
.mengsan-info-row-shuying>summary { display:list-item;min-height:48px;padding:8px 10px;cursor:pointer;touch-action:manipulation;color:#ffe0a1;line-height:32px; }
.mengsan-info-row-shuying>summary:focus-visible { outline:2px solid #ffe0a1;outline-offset:-2px; }
.mengsan-info-row-shuying>summary>.mengsan-relic-icon-shuying { width:32px;height:32px;vertical-align:middle;margin-right:12px; }
.mengsan-info-row-shuying>summary>strong { display:inline;vertical-align:middle;overflow-wrap:anywhere; }
.mengsan-info-row-shuying>p,.mengsan-info-row-shuying>small { display:block;padding:8px 12px;white-space:pre-line;overflow-wrap:anywhere; }
.mengsan-relic-list-shuying section {
padding: 14px 0;
border-bottom: 1px solid #b9975d55;