iter49-卡1:效果端口与掷格分件批(轴戊两件建件):EffectPort六Callable路由+RewardRoller掷格面迁出+两业务类零引擎直连+effect_port_test V1-V3

This commit is contained in:
DKChan committed 2026-11-02 07:30:33 +08:00
1 parent 926b05a61c
commit 11beb4fc59
31 files changed
+697 -586

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@
需求卡片 §8「改动—验收对照表」每一条都要落成可执行的验证;本文定统一写法,卡里定每批的具体内容(测什么/注入哪里)。写卡期查不出执行载体的条目=不可测,当场呈报,禁拖到实现期。
1. **执行载体五选一,逐条声明**:单元测试(`tests/run_all`)/系统测试断言(`tests/system_test.gd`)/场景探针(`probe/` 下 `*_probe`)/MC 同种子逐项比对/人工核对(仅限无法自动化的纪律类需求点,如命名锁定、数值占位)。行为类条目以自动化载体为主。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言已入 `tools/extract_checks.py` 扫描面(2026-11-02 提取器改版:三通道同口径提取,静默通道条目 quiet 列记 `channel:check_quiet`),验收尾缀 grep 对该通道有效。
2. **粒度与对应关系**:每条清单条目至少一个自动断言,且断言必须从该条目的真实入口进入;多条条目可合用一个测试函数,但每条都要有自己单独可判红的判据(取证口径见 §8.4 唯一口径——禁变异/禁撤销)。断言失败消息末尾带编号尾缀〔卡短名 Vn〕(如〔秘宝批 V3〕)——断言与卡片清单可互相 grep 查找。**工具面局限备案(2026-10-27 迭代 39 验收审计)**:`Kit.check_quiet` 静默收集形态(计数不入判定即打印、失败消息进 problems 汇总)的断言消息不进 `tools/extract_checks.py` 扫描面——验收尾缀 grep 对该通道失效;如需覆盖属提取器改版(工程批)。
3. **断言词汇按验证对象五选一**:
| 词汇 | 适用场景 |
@@ -1,7 +1,7 @@
# 锻造工坊
> 一句话:营地的武器与护甲成长——武器锻造区三条路线、护甲锻造区三条路线各练各的,钥石消费,一次冒险各选一件,满级特效留位本期不做
> 状态:**v0.11(2026-11-02 补验收标准节:九条结构面判据落文,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕
> 状态:**v0.10(2026-10-29 实现内容清理:删「现役代码事实」「实装边界」两节与正文代码指证,规则与数值零变化)**——全部数值 provisional,终值归 Alpha 收官 M4 数值批〔未排期〕
## 设计目的
用户原案(playnote 2026-10-06):把现役"营地花钱买攻击 +1"改造成有等级、有成长、有满级特效的**武器升级路线**;新增长枪(穿刺)与法杖(魔法)实现武器间差异化;护甲同理——初始只有轻甲(升级只提 AC),最高等级后解锁重甲/魔法护甲,基础 AC 各异、各带满级特效。
@@ -209,17 +209,3 @@
### 10 · 数值与验证
全部数值 provisional 归 Alpha 收官 M4 数值批;实装批必须带改前 / 改后同随机种子配对读数验证难度影响。
## 验收标准
本节判据全部绑结构与关系、不绑具体数值:价格、增量、等级上限等行值 provisional 归 M4 数值批,改值不推翻判据;判据失效只发生在结构关系本身被改时。
- **柜台解锁与默认值解耦**:天赋树的锻造工坊钥匙节点(见[天赋树](天赋树.md))点亮后本柜台开放、可购买;未点亮时柜台锁定、不存在任何购买途径;未点亮期间武器/甲默认值照常经参数面注入冒险,不存在「未解锁就读不到默认值」的窗口(2026-10-11 树侧钥匙裁)。
- **路线解锁双通道并联**:长枪、法杖、重甲、魔法护甲四条路线的解锁条件全部在本系统内部、与关卡进度零绑定;每条路线同时开两条互不影响的通道——练满前一档、或支付一次性解锁价直购——任一通道成立即解锁(4b/4b-1 裁)。
- **窗口内容**:营地四柜台并排呈现;本柜台分武器、护甲两区,各列三条路线行,行名=件名+锻造;出场行显示当前选中(武器+甲);满级特效无窗口入口、无文案(本期不做)。
- **单选注入与树零攻防**:攻击加值唯一来源=当前选中武器路线、AC 加值唯一来源=当前选中甲路线;0 级=未练合法态、零加值;天赋树淬炼对攻击与 AC 零贡献;开局注入按「账号层基础值+树淬炼 flat+锻造加值+局内加算区」分层可加,锻造加值只取当前选中(2026-10-11 所有权切分裁)。
- **切换语义**:营地内任意武器×甲组合切换免费、即时生效;换装不丢级不生效——旧选中等级保留、加值待命;一次冒险=一个武器类型+一个甲型;冒险开始后锁死、局内不存在任何换装途径(1a/1b/1c 裁)。
- **定价结构**:每级价格随已购级数几何递增(第 N+1 级价格 > 第 N 级价格,基价与公比读表单点);路线解锁价一次性收取;钥石不足时购买被拒、余额与等级零变化;GP 全程不可用于本柜台(5b 裁)。
- **存档闭合**:六条路线等级+当前武器/甲选中标记写入存档,读档恢复与写入前一致;存档域无特效域(6 裁连带)。
- **祝福件负断言**:局内商店购买攻击/护甲祝福后,本柜台路线等级与选中态零变化(8a 裁连带——货架行名带「攻击」「护甲」字样,属性祝福与锻造升级是两条互不相通的通道)。
- **甲型双轴可感**:切换甲型后基础 AC 与挨普通攻击的克制修正(轻甲 0/重甲 −1/魔法护甲 +1)双轴同时变化(3b 裁;现行敌方攻击全部按普通处理,断言条件当下即成立)。
+15 -15
View File
@@ -37,15 +37,15 @@ var _pending_ap_delta := 0 # 下轮行动点修正排队(act
var _charge_layers := 0
var charge_layers: int:
get: return _charge_layers
# 效果引擎(dig_paid/charge_full 发射面;round_end 转存决策面)
var _fx: EffectEngine = null
var _ctx: EffectContext = null
# 效果端口件(dig_paid/dig_stop/charge_full 发射面经端口路由宿主;组装期单点注入——
# host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义)
var _port: EffectPort = null
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
_fx = fx
_ctx = ctx
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
func bind_port(port: EffectPort) -> void:
_port = port
func _init(db_ref: ConfigDb) -> void:
@@ -116,16 +116,16 @@ func _apply_dig_paid_hits(base_cost: int) -> Dictionary:
var cost := base_cost
var dig_refund := 0
var rap_gain := 0
if _fx != null:
for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
if _port != null:
for h in _port.emit("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
if String(h.get("kind", "")) == "counter_spend":
var extra := int(h.get("value", 1))
if _ap >= cost + extra:
cost += extra
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), 1)
_port.counter_add(String(h.get("counter_id", "")), 1, false)
elif String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
elif String(h.get("kind", "")) == "grant_ap":
dig_refund += int(h.get("value", 0))
elif String(h.get("kind", "")) == "grant_reserve_ap":
@@ -174,13 +174,13 @@ func request_stop() -> bool:
return false
# dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
if _fx != null:
for h in _fx.dispatch(_ctx, "dig_stop", {"ap_left": _ap}):
if _port != null:
for h in _port.emit("dig_stop", {"ap_left": _ap}):
if String(h.get("kind", "")) == "counter_add":
var val := int(h.get("value", 0))
if String(h.get("value_event", "")) == "ap_left":
val = _ap
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), val, h.has("set"))
_port.counter_add(String(h.get("counter_id", "")), val, h.has("set"))
_last_stop_ap = _ap
_ap = 0
_finish_round("stopped", "same_board")
@@ -201,8 +201,8 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
if transfer:
var prev_layers := _charge_layers
_charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap)
if _fx != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
_fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers})
if _port != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
_port.emit("charge_full", {"layers": _charge_layers})
else:
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
_last_stop_ap = 0
+55 -2
View File
@@ -7,11 +7,16 @@
## 留宿主,禁回调宿主私有族;揭示奖励兑付走 bind_grant 注入的兑付回调(RewardGrant 授予面——
## snapshot 查询与 grant_routed 同面);对本件零反向强引用(禁 RefCounted 双向环——两件均随每战新建)。
## 随机只经注入 rng 的 fx_reveal 封闭流(除该流外零随机;零 await——效果引擎全同步解释器)。
## 类变量封闭面(14≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb,
## 类变量封闭面(15≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb,
## 缺一键 assert——引用束注入防 _init max-parameters 新键)+grant_cb+engine+counters_ctx+
## ac 束(键集封闭 base/round/battle)+_fx_bonus_delta+_fx_new_resource_types+reveal 束
## (键集封闭 queue/busy)。禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/
## (键集封闭 queue/busy)+facts_inject(键集封闭 dig_streak/rider_tier_bonus/rider_tier_crit——
## 业务发射事实束运行态写面,combat_segment 每轮写 dig_streak/begin_battle 每战复位 rider 两键)。
## 禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/
## battle 两零读者成员迁入即删——同物异名同字典,桶引用经 counters_ctx 承载)。
## 效果端口件组建与转注(轴戊):_init 尾段建 EffectPort(六 Callable 绑本件对应方法)后对两业务类
## bind_port——端口不落本件成员(单向接线,本件零回持);业务类发射面全经端口路由,
## 计数器账面(counters_ctx)唯一宿主侧。
class_name BattleFxHost
extends RefCounted
@@ -29,6 +34,7 @@ var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零)
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
var reveal := {"queue": [], "busy": false} # reveal 束(queue=揭示效果待兑队列〔见血开窍/斩首学者/地质学家/跳格采掘共用——授予段末统一兑付,防轮 facts 迭代中追加重入〕;busy=兑付中标记——宿主 _on_cell_revealed 缓冲旁路判定,防二次缓冲二次兑付)
var facts_inject := {"dig_streak": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} # 业务发射事实束(键集封闭——原 damage.inject 运行态束迁宿主:dig_streak=combat_segment 每轮写;rider 两键=begin_battle 每战复位;charge_layers 不在束内=活读 action_economy)
func _init(bundle: Dictionary) -> void:
@@ -43,6 +49,18 @@ func _init(bundle: Dictionary) -> void:
facts_cb = bundle.facts_cb
engine = EffectEngine.new(db)
counters_ctx = EffectContext.new() # round/battle 两桶即 ctx 自带字典——编排器旧 fx_counters 两成员同物异名零读者,不再另立
# 效果端口件组建与转注(轴戊——组装期单点:先于任何发射,六 Callable 绑本件方法后对两业务类转注;
# 端口不落本件成员——单向接线,本件零回持;方法直引用形态同 bundle emit_cb/facts_cb 既有接线)
var port := EffectPort.new({
"emit_cb": fx_emit_biz,
"counter_read_cb": counter_read,
"counter_add_cb": counter_add,
"fact_cb": biz_fact,
"reveal_cb": enqueue_reveal,
"ac_cb": apply_ac_bonus,
})
bundle.damage.bind_port(port)
bundle.action_economy.bind_port(port)
# 揭示兑付回调挂接(宿主 _init 建两件后接线——构造序禁环)
@@ -55,6 +73,41 @@ func fx_emit(event: String, payload: Dictionary) -> Array:
return engine.dispatch(counters_ctx, event, payload, _fx_fact)
# 业务类发射路由(效果端口件 emit_cb——damage/action_economy 发射面统一出口;
# 事实回调=biz_fact 闭面五键,battle_index/round_index/seals_count/nest_count 禁进业务发射面)
func fx_emit_biz(event: String, payload: Dictionary) -> Array:
return engine.dispatch(counters_ctx, event, payload, biz_fact)
# 计数器账面路由(效果端口件 counter_read_cb——value_counter 层值读;账面唯一宿主侧)
func counter_read(counter_id: String) -> int:
return EffectEngine.counter(counters_ctx, counter_id)
# 计数器账面路由(效果端口件 counter_add_cb——施加单点;账面唯一宿主侧)
func counter_add(counter_id: String, value: int, set_form: bool) -> void:
EffectEngine.counter_add(counters_ctx, counter_id, value, set_form)
# 业务发射事实面(闭面五键——三 when 事实 {charge_layers 活读/dig_streak 轮内槽/hp_ratio 主角
# 血量比例算式逐字承 damage_controller 原式}∪ 两 rider 运行态读数 {facts_inject 现值};
# 其余事实一律 null——battle_index/round_index/seals_count/nest_count 禁进,扩面=行为变化)
func biz_fact(name: String) -> Variant:
match name:
"charge_layers":
return action_economy.charge_layers
"dig_streak":
return int(facts_inject["dig_streak"])
"hp_ratio":
var hu: Dictionary[Variant, Variant] = damage.units.hero_unit()
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
"rider_tier_bonus":
return int(facts_inject["rider_tier_bonus"])
"rider_tier_crit":
return bool(facts_inject["rider_tier_crit"])
return null
# 偏运权重查询(reward_roll 发射面——bias_rewards 词汇在本层收口,掷格域只见整数)。
# 三参形 dispatch(不带 facts:本事件订阅行 when 事实面不参与,命中集与缝拆前逐字同);
# 发射时点由掷格件在开流之后、权重装配之前调用;零命中=0=原行集零扰动。
+6 -9
View File
@@ -226,9 +226,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
"action_economy": action_economy, "status_effects": _status_effects, "units": _enemy_system, "emit_cb": _emit_row,
"fatal_cb": _fatal, "attune_safe_seen_cb": _attune_safe_seen_read, "pool_effective_cb": _pool_effective})
_fx_host.bind_grant(_reward_grant.reveal_redeem) # 构造序禁环:两件建齐后接线(fx 件对授予面走注入回调)
_damage.set_fx(_fx_host.engine, _fx_host.counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
action_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # dig_paid/charge_full 发射面
_reward_economy.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(reward_roll 发射归 fx 宿主)
# 业务类发射面接线归 host._init 尾段组装期完成(效果端口件 bind_port 转注——先于 begin_battle 任何发射)
_reward_economy.roller.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(掷格件缝拆形态随件迁移——reward_roll 发射归 fx 宿主)
minesweeper.cell_revealed.connect(_on_cell_revealed)
minesweeper.seal_judged.connect(_on_seal_judged)
minesweeper.round_end_board.connect(_on_round_end)
@@ -340,13 +339,11 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
var adv_bucket: Variant = enc.get("adv_bucket", null)
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
_damage.fx_calls["reveal"] = _fx_host.enqueue_reveal
_damage.fx_calls["ac"] = _fx_host.apply_ac_bonus
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
# 流派档位质变不设(奇物.md v1「不设流派标签、
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
_damage.inject["rider_tier_bonus"] = 0
_damage.inject["rider_tier_crit"] = false
# 不设档位机制」):rider_tier 两运行态复位恒零,散件单体效果=唯一形态
_fx_host.facts_inject["rider_tier_bonus"] = 0
_fx_host.facts_inject["rider_tier_crit"] = false
_facts.feat_first_dig_used = false
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
@@ -499,7 +496,7 @@ func _hits_of(hits: Array, kind: String) -> Array:
# aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
func set_treasure_unlocked(ids: Dictionary) -> void:
_reward_economy.set_treasure_unlocked(ids)
_reward_economy.roller.set_treasure_unlocked(ids)
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段)----
+2 -2
View File
@@ -77,13 +77,13 @@ func install(during_round := false) -> bool:
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
func _resolve_global_variant(board: BoardData) -> void:
board.board_variants.clear()
reward_economy.board_skew = 0
reward_economy.roller.board_skew = 0
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
String(encounter.get("node_type", "")))
if row.is_empty():
return
board.board_variants = [String(row.variant_id)]
reward_economy.board_skew = int(row.get("effect_value", 0))
reward_economy.roller.board_skew = int(row.get("effect_value", 0))
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
+26 -51
View File
@@ -41,19 +41,9 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(
var _seq := 0 # 流水号
var _halted := false # 主角倒下终止封印(死亡事件置位——flush 余单作废)
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
# 效果引擎(attack_commit/clamp_check/armor_roll/kill/damage_taken
# 发射+施加面:dispatch 返回命中,本类按组合规则施加)
var _fx: EffectEngine = null
var _ctx: EffectContext = null
# 战斗段运行态事实束(编排器每轮/每战注入——键集封闭,缺键读缺省即零扰动):
# dig_streak/charge_layers——facts 取数面(attack_commit 载荷 dig_streak;猎巢人 per/整备
# attack_bonus per/厚积薄发 when/铁壁 shield per 同读 charge_layers)——combat_segment 每轮写;
# rider_tier_bonus/rider_tier_crit——流派档位两运行态(magic 档一 rider+1/档二 rider 可暴击)
# ——begin_battle 每战写
var inject := {"dig_streak": 0, "charge_layers": 0, "rider_tier_bonus": 0, "rider_tier_crit": false}
# 效果引擎入站回调束(编排器 begin_battle 同点注入):reveal=揭示效果回调(kill 发射点 reveal_tiles
# 订阅面,入队授予段末兑付)/ac=ac_bonus 回调(石肤 AC 累积单点同步)
var fx_calls := {"reveal": Callable(), "ac": Callable()}
# 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由,
# 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑)
var _port: EffectPort = null
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
@@ -69,44 +59,33 @@ func set_hero_ac_override(v: int) -> void:
_hero_ac_override = v
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
_fx = fx
_ctx = ctx
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
func bind_port(port: EffectPort) -> void:
_port = port
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加)
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加;
# 路由经效果端口件,事实回调=biz_fact 闭面五键宿主侧)
func _fx_hits(event: String, payload: Dictionary) -> Array:
if _fx == null:
if _port == null:
return []
return _fx.dispatch(_ctx, event, payload, _fx_fact)
return _port.emit(event, payload)
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
# hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
# fx_calls.reveal=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
func _fx_fact(fact: String) -> Variant:
match fact:
"charge_layers":
return int(inject["charge_layers"])
"dig_streak":
return int(inject["dig_streak"])
"hp_ratio":
var hu: Dictionary[Variant, Variant] = units.hero_unit()
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
return null
# 未绑事实回落(缺省 null——int()/bool() 包裹后与原 inject 缺省值逐位同:int(null)=0/bool(null)=false)
func _fact_or(name: String) -> Variant:
return null if _port == null else _port.fact(name)
# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/
# per 取数(per 字段解析——载荷同名字段优先,回落端口事实面;add_ticket_rider/attack_bonus/
# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶)
func _per_value(name: String, payload: Dictionary) -> int:
if name == "":
return 1
if payload.has(name):
return int(payload[name])
if _fx_fact(name) != null:
return int(_fx_fact(name))
if _fact_or(name) != null:
return int(_fact_or(name))
return 0
@@ -169,7 +148,7 @@ func open_ticket(kind: String, source_class: String, display: String, spec: Dict
var ac_payload := {"atk": int(t.base_value),
"target": String(tgt.get("display_name", "")) if tgt != null else "",
"target_id": String(tgt.get("enemy_id", "")) if tgt != null else "",
"matchup": matchup_o, "dig_streak": int(inject["dig_streak"]), "nest_count": _nest_count()}
"matchup": matchup_o, "dig_streak": int(_fact_or("dig_streak") or 0), "nest_count": _nest_count()}
t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload)
t.context["fx_attack_payload"] = ac_payload
return t
@@ -361,13 +340,13 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
if String(h.get("kind", "")) == "attack_bonus":
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
contrib = EffectEngine.counter(_ctx, String(h.get("value_counter", "")))
contrib = _port.counter_read(String(h.get("value_counter", "")))
base_value += float(contrib)
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序;端口路由账面)
for h in ac_hits:
if String(h.get("kind", "")) == "counter_add":
var set_v = h.get("set", null)
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
if ctx["spike_ready"]:
for h in ac_hits:
@@ -397,7 +376,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
# rider 不改变下限语义(landed 含 rider 后不再过 floor)。
var hero_rider := int(inject["rider_tier_bonus"])
var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并)
for h in ac_hits:
if String(h.get("kind", "")) == "add_ticket_rider":
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"])
@@ -405,7 +384,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
contrib = mini(int(h.cap), contrib)
hero_rider += contrib
if hero_rider > 0:
if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]):
if bool(ctx["crit"]) and bool(_fact_or("rider_tier_crit") or false):
hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
@@ -421,9 +400,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void:
for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}):
if String(h.get("kind", "")) == "reveal_tiles":
var reveal_cb: Callable = fx_calls["reveal"]
if reveal_cb.is_valid():
reveal_cb.call() # 斩首学者——击杀揭示 1 格(编排器入队,授予段末兑付)
_port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付)
continue
if String(h.get("kind", "")) != "overflow":
continue
@@ -468,7 +445,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
var total: int = int(roll["total"])
# damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers),
# 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带)
var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index))
total = int(dt_res["total"])
units.apply_damage(hero, total)
@@ -525,12 +502,10 @@ func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary:
"damage_reduce":
total = maxi(0, total - int(h.get("value", 0)))
"ac_bonus":
var ac_cb: Callable = fx_calls["ac"]
if ac_cb.is_valid():
ac_cb.call(h)
_port.apply_ac(h)
"counter_add":
var set_v = h.get("set", null)
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
if shield > 0:
total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield)
return {"total": total, "shield": shield}
+71
View File
@@ -0,0 +1,71 @@
## 效果端口件:战斗业务类(DamageController/ActionEconomy)唯一的效果发射出口——收事件与载荷、
## 交效果引擎结算、取回命中批。六 Callable 路由宿主(BattleFxHost.fx_emit_biz/counter_read/
## counter_add/biz_fact/enqueue_reveal/apply_ac_bonus),经组装期 bind_port 单点注入。
## 闭面红线:事实回调=biz_fact 闭面五键(charge_layers/dig_streak/hp_ratio/rider_tier_bonus/
## rider_tier_crit),battle_index/round_index/seals_count/nest_count 禁进业务发射事实面。
## 单向接线:本件只持宿主注入的 Callable,禁持 BattleFxHost 对象引用(宿主 bundle 已持两业务类
## ——对象引用即成 host→damage→port→host 强引用环,两件均随每战新建=泄漏)。
## 未绑缺省:emit 返回空批/计数器读 0/计数器施加与揭示与 AC 回调 no-op/事实答 null——
## 与原业务类 _fx==null 零扰动守卫同语义(单测直构不接端口的既有夹具零扰动照跑)。
class_name EffectPort
extends RefCounted
var emit_cb: Callable # func(event: String, payload: Dictionary) -> Array——宿主 fx_emit_biz
var counter_read_cb: Callable # func(counter_id: String) -> int——宿主 counter_read
var counter_add_cb: Callable # func(counter_id: String, value: int, set_form: bool) -> void——宿主 counter_add
var fact_cb: Callable # func(name: String) -> Variant——宿主 biz_fact(伤害域闭面)
var reveal_cb: Callable # func() -> void——宿主 enqueue_reveal
var ac_cb: Callable # func(h: Dictionary) -> void——宿主 apply_ac_bonus
func _init(bundle: Dictionary) -> void:
for k in ["emit_cb", "counter_read_cb", "counter_add_cb", "fact_cb", "reveal_cb", "ac_cb"]:
assert(bundle.has(k), "[效果端口] 引用束缺键: %s" % k)
emit_cb = bundle.emit_cb
counter_read_cb = bundle.counter_read_cb
counter_add_cb = bundle.counter_add_cb
fact_cb = bundle.fact_cb
reveal_cb = bundle.reveal_cb
ac_cb = bundle.ac_cb
# 事件发射(收事件与载荷、交宿主路由效果引擎结算、取回命中批;未绑→空批)
func emit(event: String, payload: Dictionary) -> Array:
if not emit_cb.is_valid():
return []
return emit_cb.call(event, payload)
# 计数器层值读(value_counter 层值取数——宿主 counters_ctx 账面单点;未绑→0)
func counter_read(counter_id: String) -> int:
if not counter_read_cb.is_valid():
return 0
return int(counter_read_cb.call(counter_id))
# 计数器施加(命中行 counter_add 用后清零/计账——未绑→no-op)
func counter_add(counter_id: String, value: int, set_form: bool) -> void:
if not counter_add_cb.is_valid():
return
counter_add_cb.call(counter_id, value, set_form)
# 事实取值(闭面五键宿主侧单点——未绑→null)
func fact(name: String) -> Variant:
if not fact_cb.is_valid():
return null
return fact_cb.call(name)
# 揭示效果入队(kill 发射点 reveal_tiles 订阅面——授予段末兑付;未绑→no-op)
func reveal_effect() -> void:
if not reveal_cb.is_valid():
return
reveal_cb.call()
# AC 累积回调(damage_taken ac_bonus 石肤累积单点;未绑→no-op)
func apply_ac(h: Dictionary) -> void:
if not ac_cb.is_valid():
return
ac_cb.call(h)
+1
View File
@@ -0,0 +1 @@
uid://b50wf7iako1uy
+24 -220
View File
@@ -1,17 +1,26 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 格面资源身份层:install 期一次性预分配每格资源身份,授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。
## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁在本类引入授予期随机或重掷。
## 格面资源身份层(受理账面):授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。
## 摇号面已分家(architecture v1.29 轴戊):装配期格面资源摇号(池行装配/区间行 roll/权重合成)
## 归 RewardRoller(本件 _init 自建自持有,外部经 roller 组件出口直达);本类保留受理账面
## (grant/身份查询/快照/retire/polarity_row)与 roll_board 装配序(守卫→复位→委托→收纳→ready)。
## 衔接面=格→奖励身份映射批束(键集封闭 mapping/power_roll/clear_identity/clear_power——掷格件产出、
## 本件接收收纳)。
## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁引入授予期随机或重掷。
## 极性路由归战斗结算授予段——本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面。
class_name RewardEconomy
extends RefCounted
var db: ConfigDb
var rng: RngServiceCore
# 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入
# orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。
var fatal_handler: Callable = Callable()
# 属性 setter 同拍转注 roller(构造序:economy._init 不触该成员、编排器注入晚于 _init——
# roller 已在座时赋值即转注,掷格件违约封印与受理面同一通道)
var fatal_handler: Callable = Callable():
set(v):
fatal_handler = v
if roller != null:
roller.fatal_cb = v
var _phase := "unrolled" # unrolled → ready → retired
var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序
var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载)
var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号)
var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行)
@@ -19,19 +28,19 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返
var _clear_identity := ""
var _clear_granted := false
var _clear_snapshot := {}
# 偏运权重查询口(reward_roll 发射归 BattleFxHost 效果层——掷格域不持效果引擎/上下文;
# 参数=本次可掷格数、返回=偏运权重加算量、缺省空回调=恒 0=原行集零扰动)
var luck_bias_query: Callable = Callable()
var roller: RewardRoller # 掷格件(摇号面单点——自建自接线,BoardInstaller.generator 先例)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
db = db_ref
rng = rng_ref
roller = RewardRoller.new({"db": db_ref, "rng": rng_ref, "fatal_cb": fatal_handler})
# ---- install 期 roll(恰一次;Ready 相位再调拒绝)----
# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单)
# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单)。
# 装配序:相位守卫(E5,先行——违约路径零取号)→ 空清单 fatal(E1)→ 受理面复位五拍 →
# 委托掷格件摇号 → 收纳批束 → ready
func roll_board(eligible: Array) -> bool:
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
if _phase == "ready":
@@ -40,56 +49,20 @@ func roll_board(eligible: Array) -> bool:
if eligible.is_empty():
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
return false
_eligible = _normalize_eligible(eligible)
var list: Array[Dictionary] = _eligible
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
_power_roll.clear()
_clear_power = -1
_clear_granted = false
_clear_snapshot = {}
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
# reward_roll 发射归 BattleFxHost(偏运硬币 bias_rewards 订阅命中合计经注入查询口取回);
# 零命中/未接线=0=原行集零扰动
var luck_bias := 0
if luck_bias_query.is_valid():
luck_bias = luck_bias_query.call(list.size())
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array[Variant] = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {}
for e in list:
draft[e.cell] = weighted_pick(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1))
# ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份
var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1))
# ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号)
for cell in draft.keys():
var row: Dictionary[Variant, Variant] = _row_by_id(draft[cell])
if row.has("power_min"):
_power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max))
var clear_row: Dictionary[Variant, Variant] = _row_by_id(clear_rid)
if clear_row.has("power_min"):
_clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max))
_mapping = draft
_clear_identity = clear_rid
var batch := roller.roll(eligible)
_mapping = batch.mapping
_power_roll = batch.power_roll
_clear_identity = batch.clear_identity
_clear_power = batch.clear_power
_phase = "ready"
return true
## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
var seen := {}
var list: Array[Dictionary] = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c})
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
return list
# ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)----
# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段)
@@ -181,175 +154,6 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary:
return {}
# ---- F1 选行算子纯函数 ----
# 偏运硬币 N(reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动)
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
# 图级偏斜旋钮(battle_orchestrator 装板决议注入,全局型行
# effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入)
var board_skew: int = 0
# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type;
# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除)
const SKEW_DOWN_KINDS := ["heal", "defense"]
const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"]
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后
#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化)
func _biased_cell_rows(bias: int) -> Array:
var base: Array[Variant] = _skewed_cell_rows()
if bias <= 0:
return base
var out: Array[Variant] = []
for row in base:
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
var biased: Dictionary[Variant, Variant] = row.duplicate()
biased["weight"] = int(row.weight) + bias
out.append(biased)
else:
out.append(row)
return out
# 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族
# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。
# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。
func _skewed_cell_rows() -> Array:
if board_skew <= 0:
return _rollable_rows()
var base: Array[Variant] = _rollable_rows()
var families := _skew_families(base)
if families["down"].is_empty() or families["up"].is_empty():
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200)
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops))
var out := _apply_skew(base, families["down"], families["up"], drops, gains)
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
return out
## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰)
func _skew_families(base: Array) -> Dictionary:
var down: Array[Variant] = []
var up: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
if SKEW_DOWN_KINDS.has(k):
down.append(row)
elif SKEW_UP_KINDS.has(k):
up.append(row)
return {"down": down, "up": up}
## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移)
func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array:
var di := 0
var ui := 0
var out: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
var w := int(row.weight)
if SKEW_DOWN_KINDS.has(k):
w -= drops[di]
di += 1
elif SKEW_UP_KINDS.has(k):
w += gains[ui]
ui += 1
assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)")
var skewed: Dictionary[Variant, Variant] = row.duplicate()
skewed["weight"] = w
out.append(skewed)
return out
func drops_total(amounts: Array[int]) -> int:
var t := 0
for a in amounts:
t += a
return t
# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows
# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total
static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]:
var floors: Array[int] = []
var fracs: Array[int] = []
var used := 0
for row in rows:
var exact: int = int(row.weight) * numer
floors.append(int(exact / denom))
fracs.append(exact % denom)
used += int(exact / denom)
var order: Array[int] = []
for i in rows.size():
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool:
if fracs[a] != fracs[b]:
return fracs[a] > fracs[b]
return a < b)
var out: Array[int] = floors
var rest: int = total - used
assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)")
for j in rest:
out[order[j]] += 1
return out
func weighted_pick(rows: Array, r: int) -> String:
var total := _pool_weight(rows)
assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)")
var acc := 0
for row in rows:
acc += int(row.weight)
if r < acc:
return String(row.reward_id)
_fatal("weighted_pick 未命中(不应可达)")
return ""
# aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
var _treasure_unlocked := {}
func set_treasure_unlocked(ids: Dictionary) -> void:
_treasure_unlocked = ids
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array[Dictionary] = []
for row in db.run.reward.rows:
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
var iid := String(row.get("effect_id", ""))
if iid == "" or not _treasure_unlocked.has(iid):
continue
if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0:
out.append(row)
return out
func _pool_rows(pool_tag: String) -> Array:
var out: Array[Dictionary] = []
for row in db.run.reward.rows:
if row.pool_tag == pool_tag and int(row.weight) > 0:
out.append(row)
return out
func _pool_weight(rows: Array) -> int:
var w := 0
for row in rows:
w += int(row.weight)
return w
func _row_by_id(rid: String) -> Dictionary:
for row in db.run.reward.rows:
if row.reward_id == rid:
+245
View File
@@ -0,0 +1,245 @@
## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(偏运硬币/board_skew 两路投影)。
## 自 RewardEconomy 迁出掷格面(architecture v1.29 轴戊②);受理账面(grant/身份查询/快照/retire/
## polarity_row)留守 RewardEconomy——衔接面=格→奖励身份映射批束(本件产出、受理件接收)。
## 取号流声明:roll 每批恰开一 reward_roll 流(scope=每次 install 批不变),体序=normalize(行主序
## +去重,零 RNG)→ 开流 → luck_bias 查询 → 池行装配 → 身份批逐格取号 → 清图批尾 → 区间行追加取号
## ——取号序与取号次数与拆件前逐字同(同种子逐位复现红线)。
## 零回指声明:本件零回指 RewardEconomy(fatal 经构造注入 fatal_cb 链转宿主封印通道——BoardInstaller
## 协作件先例);随机只经注入 RngServiceCore(持 economy 传入的同一实例——流身份不变)。
class_name RewardRoller
extends RefCounted
var db: ConfigDb
var rng: RngServiceCore
var fatal_cb: Callable = Callable() # 协作件违约→宿主 fatal 封印(BoardInstaller 先例——economy fatal_handler 属性 setter 同拍转注)
var luck_bias_query: Callable = Callable() # 偏运合计查询口(reward_roll 发射归 BattleFxHost 效果层——缝拆形态随件迁移;缺省空回调=恒 0=原行集零扰动)
var board_skew: int = 0 # 图级偏斜旋钮(写点=BoardInstaller 装板决议,全局型行 effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入)
var _treasure_unlocked := {}
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type;
# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除)
const SKEW_DOWN_KINDS := ["heal", "defense"]
const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"]
func _init(bundle: Dictionary) -> void:
for k in ["db", "rng", "fatal_cb"]:
assert(bundle.has(k), "[掷格件] 引用束缺键: %s" % k)
db = bundle.db
rng = bundle.rng
fatal_cb = bundle.fatal_cb
# 资格清单摇号(economy.roll_board 装配序委托——守卫与受理复位留守 economy;本函数只承载摇号本体)。
# 批束键集封闭:{"mapping": Dictionary(cell→reward_id 行主序插入序), "power_roll": Dictionary(cell→int),
# "clear_identity": String, "clear_power": int}
func roll(eligible: Array) -> Dictionary[Variant, Variant]:
var list: Array[Dictionary] = _normalize_eligible(eligible)
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
# reward_roll 发射归 BattleFxHost(偏运硬币 bias_rewards 订阅命中合计经注入查询口取回);
# 零命中/未接线=0=原行集零扰动
var luck_bias := 0
if luck_bias_query.is_valid():
luck_bias = luck_bias_query.call(list.size())
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array[Variant] = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {}
for e in list:
draft[e.cell] = _weighted_pick_row(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)).reward_id
# ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份(选行算子返表行——就地取 reward_id,
# randi_range 求值位置与拆件前逐位同)
var clear_rid := String(_weighted_pick_row(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)).reward_id)
# ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号;行查就地内联——
# 局部变量预算内,取号序与取号次数零变化)
var power_roll := {}
for cell in draft.keys():
if _row_by_id(draft[cell]).has("power_min"):
power_roll[cell] = stream.randi_range(int(_row_by_id(draft[cell]).power_min), int(_row_by_id(draft[cell]).power_max))
var clear_power := -1
if _row_by_id(clear_rid).has("power_min"):
clear_power = stream.randi_range(int(_row_by_id(clear_rid).power_min), int(_row_by_id(clear_rid).power_max))
return {"mapping": draft, "power_roll": power_roll, "clear_identity": clear_rid, "clear_power": clear_power}
func set_treasure_unlocked(ids: Dictionary) -> void:
_treasure_unlocked = ids
## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
var seen := {}
var list: Array[Dictionary] = []
for e in eligible:
var c: Vector2i = e.cell
if seen.has(c):
push_warning("[掷格件] 资格清单重复格已去重: %s" % str(c))
continue
seen[c] = true
list.append({"cell": c})
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
return list
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后
#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化)
func _biased_cell_rows(bias: int) -> Array:
var base: Array[Variant] = _skewed_cell_rows()
if bias <= 0:
return base
var out: Array[Variant] = []
for row in base:
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
var biased: Dictionary[Variant, Variant] = row.duplicate()
biased["weight"] = int(row.weight) + bias
out.append(biased)
else:
out.append(row)
return out
# 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族
# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。
# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。
func _skewed_cell_rows() -> Array:
if board_skew <= 0:
return _rollable_rows()
var base: Array[Variant] = _rollable_rows()
var families := _skew_families(base)
if families["down"].is_empty() or families["up"].is_empty():
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200)
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops))
var out := _apply_skew(base, families["down"], families["up"], drops, gains)
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
return out
## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰)
func _skew_families(base: Array) -> Dictionary:
var down: Array[Variant] = []
var up: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
if SKEW_DOWN_KINDS.has(k):
down.append(row)
elif SKEW_UP_KINDS.has(k):
up.append(row)
return {"down": down, "up": up}
## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移)
func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array:
var di := 0
var ui := 0
var out: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
var w := int(row.weight)
if SKEW_DOWN_KINDS.has(k):
w -= drops[di]
di += 1
elif SKEW_UP_KINDS.has(k):
w += gains[ui]
ui += 1
assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)")
var skewed: Dictionary[Variant, Variant] = row.duplicate()
skewed["weight"] = w
out.append(skewed)
return out
func drops_total(amounts: Array[int]) -> int:
var t := 0
for a in amounts:
t += a
return t
# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows
# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total
static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]:
var floors: Array[int] = []
var fracs: Array[int] = []
var used := 0
for row in rows:
var exact: int = int(row.weight) * numer
floors.append(int(exact / denom))
fracs.append(exact % denom)
used += int(exact / denom)
var order: Array[int] = []
for i in rows.size():
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool:
if fracs[a] != fracs[b]:
return fracs[a] > fracs[b]
return a < b)
var out: Array[int] = floors
var rest: int = total - used
assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)")
for j in rest:
out[order[j]] += 1
return out
# 选行算子(weighted_pick 改造形态——算术逐字:acc 累加/断言/fatal 通道;返回表行,调用侧取
# row.reward_id;取号算术与次数零变化。单点=本件 _weighted_pick_row,economy 受理面行查留守 _row_by_id)
func _weighted_pick_row(rows: Array, r: int) -> Dictionary:
var total := _pool_weight(rows)
assert(r >= 0 and r < total, "选行算子取号越域(纯函数域守卫)")
var acc := 0
for row in rows:
acc += int(row.weight)
if r < acc:
return row
_fatal("选行算子未命中(不应可达)")
return {}
# aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array[Dictionary] = []
for row in db.run.reward.rows:
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
var iid := String(row.get("effect_id", ""))
if iid == "" or not _treasure_unlocked.has(iid):
continue
if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0:
out.append(row)
return out
func _pool_rows(pool_tag: String) -> Array:
var out: Array[Dictionary] = []
for row in db.run.reward.rows:
if row.pool_tag == pool_tag and int(row.weight) > 0:
out.append(row)
return out
func _pool_weight(rows: Array) -> int:
var w := 0
for row in rows:
w += int(row.weight)
return w
func _row_by_id(rid: String) -> Dictionary:
for row in db.run.reward.rows:
if row.reward_id == rid:
return row
return {}
func _fatal(msg: String) -> void:
push_error("[掷格件/fatal] " + msg)
if fatal_cb.is_valid():
fatal_cb.call(msg) # economy fatal_handler 同拍转注(封印传导,不绕第二中止通道)
assert(false, msg)
+1
View File
@@ -0,0 +1 @@
uid://ns1oyct41fhk
+4 -5
View File
@@ -31,10 +31,9 @@ func run(ri: int) -> void:
round_safe += 1
elif evt.kind == "mine":
round_mine = true
oc._damage.inject["dig_streak"] = 0 if round_mine else round_safe
# 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备
# 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新)
oc._damage.inject["charge_layers"] = oc.action_economy.charge_layers
oc.fx_host().facts_inject["dig_streak"] = 0 if round_mine else round_safe
# 蓄力层不再逐轮注入(效果端口件闭面——fact charge_layers 由宿主 biz_fact 活读 action_economy
# 现值:唯一写点=finish_round_stop 于上轮授予段尾,本轮授予段发射前无第二写点,活读等价)
_hero_strike(ri)
_enemy_actions(ri)
_board_deferred_settle(ri)
@@ -61,7 +60,7 @@ func _hero_strike(ri: int) -> void:
# ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位
# 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致)
for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"):
var segments := mini(int(h.get("cap", 2)), int(oc._damage.inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
var segments := mini(int(h.get("cap", 2)), int(oc.fx_host().facts_inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
for s in segments:
# source_class 用中性名;attack_segments 兑付通道保留泛用面
var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段",
+10
View File
@@ -14,11 +14,21 @@ const AUTOLOAD_PATH_RNG := "/root/RngService"
const AUTOLOAD_PATH_SAVE := "/root/SaveManager"
const AUTOLOAD_PATH_META := "/root/MetaProgression"
const UiAssembler := preload("res://presentation/ui_assembler.gd")
const Narrator := preload("res://presentation/battle_narrator.gd")
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
const DebugSessionScript := preload("res://presentation/debug_session.gd")
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族
const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件
const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件
const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件
const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件
const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
var db
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
+1 -1
View File
@@ -230,7 +230,7 @@ static func motion(dur: float) -> float:
# 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线)
const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png"
const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png"
const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H)
const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档)
const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(三态切换)
const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(三态切换)
# 敌种立绘(B 区站桩)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

+1
View File
@@ -196,6 +196,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/stage_background_test.gd", "expect": 0}, # B 区舞台背景三关变体与连续地面层(V1–V5/V8——ground_h 表单源/board_type 选图/缺图回退/地面几何/光柱退役)
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
{"path": "res://tests/unit/battle_settlement/effect_port_test.gd", "expect": 0}, # 效果端口件(效果端口与掷格分件批 V1–V3——端口单实例/业务类零引擎直连源码锚/事实面闭集两对照)
{"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入)
{"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入)
+3 -3
View File
@@ -314,7 +314,7 @@ fi
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
# 口径仿 S1/S2 的 grep 排除手法。
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd"
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
S8_MISS=0
for f in $S8_FILES; do
if [ ! -f "$f" ]; then
@@ -358,7 +358,7 @@ fi
if require_scan_face "S7" "$DIR_CORE"; then
# B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件,
# 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文)
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd"
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
S7_MISS=0
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
if [ ! -f "$f" ]; then
@@ -390,7 +390,7 @@ else
s7_scan B3 game/core/status_container.gd \
'notify_battle_start|apply_effect|query_active|decay_round|notify_battle_end'
s7_scan B4 game/core/reward_economy.gd \
'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase|weighted_pick'
'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase'
if [ -n "$S7_HITS" ]; then
echo "[static_scan] S7 FAIL: B2/B3/B4 API 符号越出 B1 文件集命中 $(printf '%s' "$S7_HITS" | grep -c .) 处(编排唯一性,B1 AC-43)"
printf '%s' "$S7_HITS"
@@ -0,0 +1,129 @@
# tests/unit/battle_settlement/effect_port_test.gd — 效果端口件验收宿主(效果端口与掷格分件批)
# 断言面 = 三套(需求卡片 §9 对照表行 1/2/3 载体):
# V1 端口单实例与真实发射路由(装配链同一实例+attack_commit 经真实战斗流命中可观测)
# V2 业务类零引擎直连(源码契约锚——两业务文件全文零 EffectEngine/EffectContext token)
# V3 伤害域事实面闭集(合成行阳性/阴性对照——载荷携带事实可达、宿主链事实不可达)
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态)。
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/effect_port_test.gd
extends SceneTree
const Kit := preload("res://tests/unit/test_kit.gd")
const ConfigDBScript := preload("res://core/config_db.gd")
const EffectEngineScript := preload("res://core/effect_engine.gd")
const TEST_SEED := 42 # 夹具种子(基建值非玩法数值——同种子两跑对比的隔离前提)
const LAYER_COUNTER := "swap_charge_layers" # 整备军备蓄力层计数器(feats.tres charge_rotation 行)
var _db
func _init() -> void:
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
if load_ok:
_smoke_effect_port_wiring()
_test_business_classes_engine_free()
_test_biz_fact_face_closed()
quit(Kit.finish("effect_port_test"))
# ---- 夹具 ----
func _enc(feats: Array) -> Dictionary:
return {"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp),
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [],
"battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}}
func _orch(seed_v: int):
var rng = load("res://core/rng_service.gd").new()
rng.set_master_seed(seed_v)
return load("res://core/battle_orchestrator.gd").new(_db, rng)
func _hero_attack_value(rows: Array) -> int:
for r in rows:
if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "hero_attack":
return int(r.get("value", 0))
return -1
# ---- V1 端口单实例与真实发射路由 ----
func _smoke_effect_port_wiring() -> void:
print("--- 效果端口:装配链单实例+真实发射路由 ---")
# 充能跑(整备军备在册——attack_commit 订阅行 attack_bonus per 蓄力层计数器)
var orch_c = _orch(TEST_SEED)
var rows_c: Array = []
orch_c.step_row.connect(func(row): rows_c.append(row))
Kit.check("V1 begin_battle 受理", orch_c.begin_battle(_enc(["charge_rotation"])))
# 端口同一性(内部字段直查=受控例外:端口同一性无公开查询面,对照表行 1 注明理由)
var port_d = orch_c.fx_host().damage._port
var port_a = orch_c.action_economy._port
Kit.check("V1 端口在座(damage/action_economy 双绑)", port_d != null and port_a != null)
Kit.check("V1 端口单实例(两业务类持同一 EffectPort)", port_d == port_a)
# 蓄力层预置 3(调试注入两层·数据面粒度——计数器账面直写,与 refresh_feats_test V8 同法)
EffectEngineScript.counter_add(orch_c.fx_host().counters_ctx, LAYER_COUNTER, 3)
# 最小回合:停手即轮末结算(主角先手攻击在战斗段同步兑付——零 await 同步链)
orch_c.minesweeper.settle_batch([{"action": "stop"}])
var v_c := _hero_attack_value(rows_c)
Kit.check("V1 attack_commit 命中批可观测:主角攻击行在流", v_c >= 0)
Kit.check("V1 命中批经端口路由全处理(attack_bonus 消费行+counter_add set-0 行——层账 3→0)",
EffectEngineScript.counter(orch_c.fx_host().counters_ctx, LAYER_COUNTER) == 0)
# 对照跑(同种子同 enc 零预置——attack_commit 订阅行 when 不通过=零命中零扰动)
var orch_0 = _orch(TEST_SEED)
var rows_0: Array = []
orch_0.step_row.connect(func(row): rows_0.append(row))
orch_0.begin_battle(_enc(["charge_rotation"]))
orch_0.minesweeper.settle_batch([{"action": "stop"}])
var v_0 := _hero_attack_value(rows_0)
Kit.check("V1 对照跑攻击行在流", v_0 >= 0)
Kit.check("V1 delta 读数非零(蓄力层加算进入主角攻击基值)", v_c - v_0 >= 1)
# ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)----
func _test_business_classes_engine_free() -> void:
print("--- 效果端口:业务类零引擎直连源码锚 ---")
for path in ["res://core/damage_controller.gd", "res://core/action_economy.gd"]:
var src := FileAccess.get_file_as_string(path)
Kit.check("V2 扫描面非空前置:%s" % path, src.length() > 0)
Kit.check("V2 全文零引擎直连 token(EffectEngine/EffectContext):%s" % path,
not src.contains("EffectEngine") and not src.contains("EffectContext"))
# ---- V3 伤害域事实面闭集(合成行两对照——damage_taken 真实单据流)----
func _test_biz_fact_face_closed() -> void:
print("--- 效果端口:业务发射事实面闭集 ---")
var orch = _orch(TEST_SEED)
Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([])))
# 合成行直改数据面(调试注入两层·数据面粒度):effect 段逐字抄现役铁壁 shield 表行(kind/value 零推表);
# rebuild 走持有 id 过滤真实表行——合成行经直改表行进入装载域(订阅面换代为合成行独占,确定性)
var pos_id := "probe_shield_pos"
var neg_id := "probe_shield_neg"
_db.crossrun.feats.rows.append({"feat_id": pos_id, "display_name": "探针盾甲甲", "rarity": "common",
"desc": "V3 阳性对照(hp_ratio 载荷携带)", "triggers": [{"on": "damage_taken",
"when": [{"fact": "hp_ratio", "op": "lte", "value": 100}], "do": {"kind": "shield", "value": 1}}]})
_db.crossrun.feats.rows.append({"feat_id": neg_id, "display_name": "探针盾甲乙", "rarity": "common",
"desc": "V3 阴性对照(battle_index 闭面外)", "triggers": [{"on": "damage_taken",
"when": [{"fact": "battle_index", "op": "eq", "value": 1}], "do": {"kind": "shield", "value": 1}}]})
var ctrl = orch.fx_host().damage
# 阳性对照(载荷携带事实——harness 能见命中):合成行独占订阅面(rebuild 持有 id 过滤——
# 只挂阳性行),board_flat_hero 真实单据流,base_value 足量避开伤害下限保底
orch.fx_host().rebuild_subscriptions([pos_id], [], [])
var hp0 := int(ctrl.units.hero_unit().get("hp", 0))
var t_a: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测甲",
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
ctrl.flush_submit(t_a)
var hp1 := int(ctrl.units.hero_unit().get("hp", 0))
Kit.check("V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9)", hp0 - hp1 == 9)
# 阴性对照(闭面外事实——宿主链答而业务面不答):订阅面换代为阴性合成行独占,同路径零减量=F10 现状形态守卫
orch.fx_host().rebuild_subscriptions([neg_id], [], [])
var t_b: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测乙",
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
ctrl.flush_submit(t_b)
var hp2 := int(ctrl.units.hero_unit().get("hp", 0))
Kit.check("V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩)", hp1 - hp2 == 10)
@@ -0,0 +1 @@
uid://b11nytp21w58b
@@ -221,27 +221,27 @@ func _smoke_global_variant() -> void:
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
var eco = load("res://core/reward_economy.gd").new(_db, load("res://core/rng_service.gd").new())
var base_rows: Array = eco._rollable_rows()
var base_sum: int = eco._pool_weight(base_rows)
var base_rows: Array = eco.roller._rollable_rows()
var base_sum: int = eco.roller._pool_weight(base_rows)
Kit.check("偏斜基线:正向池现行合计 77(挖掘风险批口径)", base_sum == 77)
eco.board_skew = 0
var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
eco.roller.board_skew = 0
var off_rows: Array = eco.roller._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
Kit.check("off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)", off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows))
eco.board_skew = 30
var on_rows: Array = eco._biased_cell_rows(0)
Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco._pool_weight(on_rows) == 77)
eco.roller.board_skew = 30
var on_rows: Array = eco.roller._biased_cell_rows(0)
Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco.roller._pool_weight(on_rows) == 77)
var wmap := _rows_weight_map(on_rows)
Kit.check("白名单豁免:weaken/gp/action 三行偏斜后逐位不动", wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2)
Kit.check("生存族下调:heal 4→3 / defense 11→7(v=30 构造例)", wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7)
Kit.check("进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)", wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9)
eco.board_skew = 30
var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
eco.roller.board_skew = 30
var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
var cw := _rows_weight_map(combo_rows)
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco._pool_weight(combo_rows) == 77 + 4 * 2)
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco.roller._pool_weight(combo_rows) == 77 + 4 * 2)
Kit.check("合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)", cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11)
eco.board_skew = 50
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0))
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
eco.roller.board_skew = 50
var v50: Dictionary = _rows_weight_map(eco.roller._biased_cell_rows(0))
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco.roller._pool_weight(eco.roller._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
# wr10 偏运判定收口+注入接线(p5 缝拆)——判定面夹具白盒装载(现网 game/data/tables/ 无产出
# bias_rewards 的数据行,先例=tree_rules_v11_test 读 _triggers);命中数前置写在消息里防空扫描面假绿。
var wrng = load("res://core/rng_service.gd").new()
@@ -256,13 +256,13 @@ func _smoke_global_variant() -> void:
Kit.check("wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" % [empty_bias, hit_rows, loaded_bias],
empty_bias == 0 and hit_rows == 1 and loaded_bias == 2)
Kit.check("wr10②接线面:组装点注入的查询口有效且与宿主查询口同源",
woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for)
woc._reward_economy.board_skew = 0
woc._reward_economy.roller.luck_bias_query.is_valid() and woc._reward_economy.roller.luck_bias_query == wfx.luck_bias_for)
woc._reward_economy.roller.board_skew = 0
# 取值面防御:未接线时按「取不到偏运」计红而非崩在空回调上(撤销证明⑤态下本套件其余断言仍须跑完)
var qb: Callable = woc._reward_economy.luck_bias_query
var qb: Callable = woc._reward_economy.roller.luck_bias_query
var wbias: int = int(qb.call(4)) if qb.is_valid() else -1
Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))],
wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2)
Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias))],
wbias == 2 and woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias)) == 77 + 4 * 2)
# 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照)
var nrng = load("res://core/rng_service.gd").new()
nrng.set_master_seed(42)
@@ -1,75 +0,0 @@
# 工程改动报告——工程清理批(2026-11-02)
接收方:待办清单 7 条目(本批完成整条删除——palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 四色墨断言/断言提取器 check_quiet/run_all_gates 02 传参〔已完成漏删〕/main.gd 死 preload)。
## 一、批定位
工程改动批(用户发令直改,不写卡)。worktree `eng-cleanup`,基点 7e9e57fd,基点即 master 尖端。七件:六件本批落地+一件核查确认已完成(run_all_gates 02 传参)。
## 二、逐件落地
### 件 1 palette_check 基线建档+双门机检化
- 基线目录 `game/tests/golden/palette_baseline/`(图像白名单在册目录)两张条带 PNG:`ui_tokens.png` 92 色(ui_kit.gd 全量 Color8 常量唯一值快照)+ `art_snapshot.png` 1274 色(现役素材 128 张 PNG 唯一色并集快照);并集 1346 色=双门②「新增色 vs 既有色板」的既有色板机检承载物。
- 新件 `tools/gen_palette_baseline.py`:基线建档与刷新器(token 提取剔除注释行;素材目录零 PNG 拒绝生成空快照)。
- `tools/palette_check.py` 零代码改动——头注补基线构成与维护纪律、退出码描述校正(单张超 32 色是 WARN 非 FAIL)。
- 闸机接线:`tools/run_all_gates.sh` 新增 **05b 色板闸**(code 域;--quick 与影响面选闸 SKIP;素材齐盘预检之后段执行)——扫 `game/assets/art` 全目录对基线判新增色近色红。
- 口径边界(呈报):门①语义检查属设计面人工核对(§4.2 色相语义判域),不入本器执法面;48 token 计数上限不接闸——ui_kit 现役唯一色值 92 与「48 上限」的成员名单口径差归 art-bible §4.1 owner 裁定,本批不自裁。
- 维护纪律:素材批收尾跑 `python tools/gen_palette_baseline.py` 重生成全套并随批提交——新色过门后须入基线,否则下批扫描把已过门的新色再报新增;两张 PNG 缺一即漏判。
### 件 2 extract_checks.py 补 check_quiet 静默通道
- `CALL_RE` → `check(?:_quiet)?\(`、`DEF_RE` 同扩(func check / func check_quiet 定义行双拦)、`collect_call` 按实际匹配长度偏移;check_quiet 通道契约 cond 在前,条目 quiet 列记 `channel:check_quiet`。单趟正则无重叠匹配,结构上不双计。
- 自证三重(当轮实跑):①双计等式——旧两正则匹配数之和 2155 == 新 CALL_RE 2155(全 tests 面);②新旧版条目对拍 1719 → 2133,差 414 条 check_quiet 入账(grep 调用行实数 416,差 2=定义行与注释行不入账,方向自洽);③`--extract` 生成临时清单后 `--diff` 往返对账 PASS 零差异(标签/cond/计数三对齐)。
- 验证集清单已随验证体系整改阶段 2 退役,`--diff` 无存档参照——对账自证以临时清单往返承载,语义等同。
- 随批换代 `design/engineering/test-conventions.md` §5.2 工具面局限备案句(2026-10-27 立的「断言消息不进扫描面——验收尾缀 grep 对该通道失效」已消除,留存会漂移);文档设计面仅此一句现状描述更新,申报。
### 件 3 意图徽标四色墨机检断言
- `tools/render_sample_sheet_v3.py` `run_screen_checks` 增 **S1b**:意图徽标四色墨(攻击/增益/减益/蓄力)对 PANEL_DATA 垫牌(#E9D9B8,与绘制矩形 fill 同源)≥4.5:1 色值对表断言,四墨值即脚本意图色表四值,零新增色。
- 当轮真实读数:攻击 5.62/增益 5.10/减益 4.97/蓄力 5.21——与独立审计复算区间 4.97–5.62 逐位一致。
- 样张机检 31/31 全绿。V10 改前基线对拍图属 artwork(不入 git)环境件,worktree 用 `--save-baseline` 重建(本批零素材零绘制变更,基线等值)。
### 件 4 main.gd 拆件遗留死 preload 清理
- 逐条 grep 复核(登记纪律):死码 **10 条**——登记 8 条,复核多出 2 条(三场景 preload 中 AdventureScreenScene 登记时另有消费面、UiAssembler 登记时在役;现全零)。UiAssembler 唯一非定义出现=组合根接线段注释提及类名(非常量使用)。
- 7 具名(TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript 等)消费面已迁 `main_ui_build.gd:9-13`(拆件自持 preload)——main.gd 旧常量=拆件残留重复声明;三场景消费面=system_test.gd `load()` 与 main.tscn,删常量不触。
- 删除 10 条 const preload,保留 Narrator/MainUiBuild/MainAdventureFlow/MainOverlayFlow/MainDebugFlow 五条在役。
### 件 5 过时注释换代
- `game/presentation/views/ui_kit.gd` TEX_HERO_PORTRAIT 行注释「48px 档」→「素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H」(迭代 48 显示档批现行口径)。
- `tools/path_check.py` 头注删「· 立绘定档批 2026-10-07」日期批注记(变更叙事黑名单类);47 行键集指名核实现行仍在役(standee_asset_check 现行 import STANDEE_DISPLAY_H),不改。
### 件 6 ui_kit CAMP_HEADER_BAND 令牌退役评估
现役消费面 4 处逐一核过:`level_map_screen.gd:107/144`(关卡屏头带与地图条)、`shop_overlay_view.gd:49`(商店头带)、`ui_kit.gd` KeystoneBadge 工厂(钥石徽描边——消费方=营地覆盖层+状态面板钥石余额行)。
**结论**:「退役删除」不成立——4 处在用且全是活面板绘制面;营地域柜台头带语义岗已随迭代 47 四柜台覆盖层退役,令牌现役值 #6E4526 只剩材质棕底色语义(木色头带/徽章描边)。**处置=改名中性化**(脱 CAMP_ 前缀,名随现役语义,如 WOOD_BAND 一族)归 ui_kit 令牌批顺带;改名零值变化零渲染变化,届时逐点核后动。维持原登记口径「退役归 ui_kit 令牌批顺带处置、非独立强删」。
### 件 7 run_all_gates 02 关联档传参(核查确认已完成)
`d32ff342`(工程小件批·件 1)已落两案中的「解析器补逗号切分」案:`run_all.gd` 选跑解析补 `split(",")`,闸脚本逗号串传参与之配套,孤立验证四套件全绿。本批零改动,条目属完成漏删。
## 三、待办清单
删 7 条(整条删除,状态机出口):palette_check 基线建档/CAMP_HEADER_BAND 评估/ui_kit 立绘注释/迭代 43 移交小件(①能力清单补行已随 2026-10-31 写卡批落位、②四色墨断言本批落)/断言提取器改版/run_all_gates 02 传参(d32ff342 已完成)/main.gd 死 preload。流水线闸复验 PASS(33 条目/49 总表行)。
## 四、窄验读数(worktree 内当轮实跑)
| 验证 | 读数 |
|---|---|
| palette_check 带基线扫 art 全目录 | 128 张 0 红 0 新增色 |
| presentation 单元套件(main.gd/ui_kit.gd 编译面) | 17 suites 全绿 |
| 冒烟 run_smoke | 15 项 0 失败,三闸全过 |
| 样张 render_sample_sheet_v3 | 31/31 全绿(含新 S1b) |
| path_check | 0 处问题 |
| extract_checks | 双计等式过/1719→2133/往返对账 PASS |
| pipeline_gate | PASS(33 条目/49 总表行/交叉核 3 链) |
全量回归归合回后本地 CI(post-merge 钩子自动跑,含新 05b 色板闸)。
## 五、边界与移交
- 48 token 计数上限的机检承载与门①语义检查不属本批(口径归 art-bible owner),呈报待裁。
- 05b 色板闸的基线维护义务(素材批收尾刷新)已写进 palette_check.py 头注与 gen_palette_baseline.py 头注,闸机注释同步点名。
+1
View File
@@ -45,6 +45,7 @@ STATUS_VALUES = ("已覆盖", "部分覆盖", "未覆盖")
# 验收标准节豁免:无可写验收面的在册件(逐件附理由;新豁免须经用户裁)
ACCEPT_EXEMPT = {
"锻造工坊.md": "工程化系统,验收面由天赋树与元进度两篇承接,本批迁移实现事实后复核",
"生涯.md": "P6 后置未实装,无现成可观测面;实装批补验收标准节",
}
+19 -24
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口对账)
机器提取宿主脚本的全部静态断言调用点(每个调用点记录:所在文件、所在函数、
机器提取宿主脚本的全部静态 check( 调用点(每个调用点记录:所在文件、所在函数、
行号、标签字符串字面量原文、cond 表达式原文),提供两种模式:
用法:
@@ -19,11 +19,10 @@ r"""extract_checks.py — 断言静态提取器(验证集清单生成与出口
断言,对账时以「新增」出现(非迁移面),出口判定以显式模式为准。
提取口径:
- 静态断言调用点,三通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)、
单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前)、静默收集通道
Kit.check_quiet(cond, label)(cond 在前,条目 quiet 列记 channel:check_quiet);
`_check(` 等同名前缀调用非标准通道,不取。
- 调用点可跨行:从调用名起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
- 静态 check( 调用点,两种通道都认:smoke/系统宿主的 check(cond, label)(cond 在前)
与单元/集成宿主的 Kit.check("label", cond)(点前缀=标签在前);`_check(` 等同名
前缀调用非标准通道,不取。
- 调用点可跨行:从 `check(` 起括号配平归集完整实参文本(() [] {} 三种括号都计深度);
字符串字面量内的括号/逗号/井号、行尾注释、转义序列均不参与结构判定(码位掩码单趟预扫)。
- cond 摘要=cond 原文跨行折叠为单行(连续空白并一空格);标签原文=标签实参里的字符串
字面量逐字保真(含引号与转义序列原样,禁改写);标签实参若带格式化/拼接后缀
@@ -65,8 +64,8 @@ def default_sources():
files.extend(sorted(p.rglob("*.gd")))
return sorted(files)
CALL_RE = re.compile(r"check(?:_quiet)?\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check(?:_quiet)?\s*\(")
CALL_RE = re.compile(r"check\(")
DEF_RE = re.compile(r"^\s*(static\s+)?func\s+check\s*\(")
FUNC_RE = re.compile(r"^(static\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)")
LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"')
OPENERS = "([{"
@@ -189,9 +188,9 @@ def split_args(body):
return args
def collect_call(joined, start, call_len):
"""从调用名起点起括号配平归集调用。返回 (body, end_pos);不配平返回 None。"""
i = start + call_len
def collect_call(joined, start):
"""从 check( 的 start 起括号配平归集调用。返回 (body, end_pos);不配平返回 None。"""
i = start + len("check(")
depth = 1
in_str = False
parts = []
@@ -229,7 +228,7 @@ def collect_call(joined, start, call_len):
def extract(src_path: Path):
"""提取单文件全部静态断言调用点(check/check_quiet)。返回(条目列表, 函数表),条目保持源码顺序。"""
"""提取单文件全部静态 check( 调用点。返回(条目列表, 函数表),条目保持源码顺序。"""
src = src_path.read_text(encoding="utf-8")
lines = src.split("\n")
funcs = func_table(lines)
@@ -248,32 +247,28 @@ def extract(src_path: Path):
entries = []
for m in CALL_RE.finditer(joined):
if not mask[m.start()]:
continue # 字符串字面量内 / 注释里的调用名字样
continue # 字符串字面量内 / 注释里的 check( 字样
ln = line_of(m.start())
if DEF_RE.match(lines[ln - 1]):
continue # func check / func check_quiet 定义行
continue # func check 定义行
prev = joined[m.start() - 1] if m.start() > 0 else ""
if prev and (prev.isalnum() or prev == "_"):
continue # `_check(` 等同名前缀——非标准通道
is_quiet = m.group(0).startswith("check_quiet")
if is_quiet:
label_first = False # Kit.check_quiet(cond, label)——cond 在前
else:
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
body, end_pos = collect_call(joined, m.start(), len(m.group(0)))
label_first = prev == "." # Kit.check("label", cond)——点前缀=标签在前
body, end_pos = collect_call(joined, m.start())
if body is None:
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的断言调用括号不配平到文件尾——先查解析缺口"
raise SystemExit("[extract_checks] FAIL:%s 行 %d 起的 check( 括号不配平到文件尾——先查解析缺口"
% (src_path.name, ln))
args = split_args(body)
if len(args) not in (2, 3):
raise SystemExit(
"[extract_checks] FAIL:%s 行 %d 的断言调用拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
"[extract_checks] FAIL:%s 行 %d 的 check( 拆出 %d 个实参(预期 2/3)——解析缺口,先查再跑"
% (src_path.name, ln, len(args)))
if label_first:
label_expr, cond = args[0], args[1]
else:
cond, label_expr = args[0], args[1]
quiet = "channel:check_quiet" if is_quiet else (args[2] if len(args) == 3 else "")
quiet = args[2] if len(args) == 3 else ""
lm = LITERAL_RE.search(label_expr)
if lm is None:
continue # 标签实参无字面量(wrapper 直传变量——既有套件先例 reward_overlay_test _req)
@@ -341,7 +336,7 @@ def render_list(entries, funcs_by_src, src_names, carry=None) -> str:
out.append("# 验证集清单(断言快照·冻结)")
out.append("")
out.append("> 生成日期:%s · 基点 commit:`%s` · 源:%s" % (date.today().isoformat(), base_commit(), "、".join(src_names)))
out.append("> 提取口径:静态 `check(`/`check_quiet(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历")
out.append("> 提取口径:静态 `check(` 调用点,括号配平归集(可跨行);缺省源=三宿主遍历")
out.append("> (unit/**+integration/**+system_test.gd)。本基点实测 %d 个静态调用点;行级" % len(entries))
out.append("> `grep -c \"check(\"` 是另一口径(含 `func check(` 定义行与注释行),两口径各自浮动,")
out.append("> 禁互相换算(tests/README「断言计数双口径」)。运行时计数大于静态计数=循环展开条目。")
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""色板基线生成器——palette_check 双门②基线的建档与刷新件。
产出两张 1×N 条带 PNG 落 game/tests/golden/palette_baseline/(图像白名单在册目录):
ui_tokens.png = ui_kit.gd 全量 Color8 常量唯一色值(token 板现役值快照)
art_snapshot.png = 现役素材(game/assets/art 递归)全量唯一色快照(像素板+各屏环境子板)
palette_check --baseline 取目录下全部 PNG 的唯一色并集为「既有色板」;基线缺一张
即漏判——基线刷新必须重生成全套。维护纪律:素材批收尾跑一次本器刷新并随批提交
(新色过门②后须入基线,否则下批扫描把已过门的新色再报新增)。
用法:
python tools/gen_palette_baseline.py [--repo <仓库根>]
退出码: 0 成功 / 2 前置缺位(ui_kit.gd 或素材目录缺 / 素材零 PNG)
"""
import argparse
import os
import re
import sys
from PIL import Image
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from palette_check import palette_of # 排除全透明的唯一色提取——与检查器同口径
def strip_comments(text):
"""剔 GDScript 行注释与字符串字面量,防注释里举的色值被误收进 token 快照。"""
out = []
for line in text.split("\n"):
i = line.find("#")
if i != -1:
line = line[:i]
out.append(line)
return "\n".join(out)
def token_cols(ui_kit_path):
"""ui_kit.gd 全量 Color8 唯一色值集合。"""
text = strip_comments(open(ui_kit_path, encoding="utf-8").read())
vals = re.findall(r"Color8\((\d+),\s*(\d+),\s*(\d+)\)", text)
return sorted(set((int(r), int(g), int(b)) for r, g, b in vals))
def scan_dirs(roots):
"""递归扫 PNG,唯一色并集(含像素位置首见序不保——并集无序)。"""
cols = set()
count = 0
for root in roots:
for dirpath, _, names in os.walk(root):
for fn in sorted(names):
if fn.lower().endswith(".png"):
c, _n = palette_of(os.path.join(dirpath, fn))
cols |= c
count += 1
return cols, count
def write_strip(cols, out_path):
"""1×N 条带 PNG(一色一像素列)。"""
cols = sorted(cols)
im = Image.new("RGB", (len(cols), 1))
for i, c in enumerate(cols):
im.putpixel((i, 0), c)
im.save(out_path)
return len(cols)
def main():
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
ap = argparse.ArgumentParser(description="色板基线生成器(token∪素材快照)")
ap.add_argument("--repo", default=".", help="仓库根(含 game/ 与 tools/)")
args = ap.parse_args()
ui_kit = os.path.join(args.repo, "game", "presentation", "views", "ui_kit.gd")
artdir = os.path.join(args.repo, "game", "assets", "art")
outdir = os.path.join(args.repo, "game", "tests", "golden", "palette_baseline")
if not os.path.isfile(ui_kit):
print("[gen_palette_baseline] FAIL:ui_kit.gd 缺:%s" % ui_kit)
return 2
if not os.path.isdir(artdir):
print("[gen_palette_baseline] FAIL:素材目录缺:%s" % artdir)
return 2
os.makedirs(outdir, exist_ok=True)
tcols = token_cols(ui_kit)
n_t = write_strip(tcols, os.path.join(outdir, "ui_tokens.png"))
acols, npng = scan_dirs([artdir])
if npng == 0:
print("[gen_palette_baseline] FAIL:素材目录零 PNG(素材未同步)——拒绝生成空快照")
return 2
n_a = write_strip(acols, os.path.join(outdir, "art_snapshot.png"))
union = set(tcols) | set(acols)
print("[gen_palette_baseline] 已生成:ui_tokens.png %d 色 / art_snapshot.png %d 色"
"(素材 %d 张 PNG);基线并集 %d 色 → %s"
% (n_t, n_a, npng, len(union), outdir))
return 0
if __name__ == "__main__":
sys.exit(main())
+2 -11
View File
@@ -17,21 +17,12 @@
另:必须排除全透明像素 (0,0,0,0)(同standee_asset_check 的坑)。
基线(双门②「新增色 vs 既有色板」的既有色板机检承载物):
目录 = game/tests/golden/palette_baseline/,两张条带 PNG(1×N,一色一像素列):
ui_tokens.png = ui_kit.gd 全量 Color8 token 快照(token 板现役值)
art_snapshot.png = 现役素材全量唯一色快照(像素板+各屏环境子板)
两张并集=「既有色板」。缺一张即漏判——基线刷新必须重生成全套。
门①语义检查属设计面人工核对(§4.2 色相语义判域),不在本器执法面。
用法:
# 主用法(闸机 05b 色板闸承载):扫目录,新增色相对 --baseline 判定
# 主用法:扫目录,新增色相对 --baseline 判定
python tools/palette_check.py <目录> --baseline <基线目录>
# 无基线:只出读数(单张色数/ 近色对计数),不判红
python tools/palette_check.py <目录>
# 基线建档与刷新:重扫现役素材∪token 重生成基线条带(素材批收尾动作,随批提交)
python tools/gen_palette_baseline.py
退出码: 0 通过 / 1 有红灯(仅新增色近色不过门②时)
退出码: 0 通过 / 1 有红灯(仅新增色违规或超单张上限时)
"""
import os
import sys
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""B3 路径核对器(圣经 §8.3② 挂点实现)。
"""B3 路径核对器(圣经 §8.3② 挂点实现 · 立绘定档批 2026-10-07)。
职责:扫描 asset-path-map.md 声明的全部路径 + 代码侧素材常量,
报「悬空(声明存在但盘上无)/ 退役未清(仍引用已退役命名)」。
-12
View File
@@ -1209,18 +1209,6 @@ def run_screen_checks(scr, zones, board_origin, frames_strip, frames_meta, s6_im
weakest = min(ratios, key=lambda r: r[1])
check("S1", "整屏①全部文字对直接背景 ≥4.5:1(WCAG AA)", all(r[1] >= 4.5 for r in ratios),
"最弱 %s=%.2f;全部 %s" % (weakest[0], weakest[1], ratios))
# S1b 意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(色值对表——镜像实装同值;垫牌=ui_kit PANEL_DATA #E9D9B8,
# 与 draw_standee 徽标垫矩形 fill 同源;四墨=意图色表 damage/buff/debuff/charge 四值)
panel_data = (233, 217, 184)
intent_inks = [("攻击墨", (150, 44, 36)), ("增益墨", (30, 90, 150)),
("减益墨", (122, 66, 148)), ("蓄力墨", (116, 80, 11))]
iratios = [(n, round((max(wcag_lum(a), wcag_lum(panel_data)) + 0.05)
/ (min(wcag_lum(a), wcag_lum(panel_data)) + 0.05), 2))
for n, a in intent_inks]
iweak = min(iratios, key=lambda r: r[1])
check("S1b", "意图徽标四色墨对 PANEL_DATA 垫牌 ≥4.5:1(WCAG AA·色值对表)",
all(r[1] >= 4.5 for r in iratios),
"最弱 %s=%.2f;全部 %s" % (iweak[0], iweak[1], iratios))
# S2 整屏②明度三档带 + 世界底层两子带(§2;舞台=实测区域读数,格底=实测板面;WCAG 0–1 域)
stage_l = _region_wcag(scr, (300, 190, 600, 220))
bx, by = board_origin
-8
View File
@@ -413,14 +413,6 @@ if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then
else
gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME"
fi
# ── 5b. 色板闸(§4.1 新色双门②:素材新增色 vs 基线〔token∪素材快照〕近色判红;
# 门①语义检查属设计面人工核对不入机检;基线刷新=tools/gen_palette_baseline.py,素材批收尾动作)──
if [ $QUICK -eq 1 ] || [ $NONBEHAVIOR -eq 1 ]; then
NAMES+=("05b 色板闸"); EXITS+=("SKIP"); LOGS+=("")
echo ""; echo "[gates] 05b 色板闸 → SKIP(--quick 或 影响面选闸:素材色板面不随测量面改动开闸)"
else
gate "05b 色板闸" "palette.log" "$PY" "$REPO/tools/palette_check.py" "$GAME/assets/art" --baseline "$REPO/game/tests/golden/palette_baseline"
fi
fi
# ══ 文档域(6–8e)══
if in_domain docs; then
+59 -8
View File
@@ -2,6 +2,22 @@
本清单是流水线的唯一登记处:以后才做的事在此登记一条,随流水线段推进改状态,做完整条删除(条目记进度,不是各段的开工前置);各系统文档的「待办」节只留一行指路存根,不再自建条目。条目格式(标题=登记元数据含可选优先级,正文=实况/缺口/改动面/状态四属性)与状态机以 [doc-conventions §12.1](design/engineering/doc-conventions.md) 为权威。
#### palette_check 基线建档与双门机检化(2026-11-02 登记,来源=批次移交)
**实况**:证据=迭代 47 实现报告 §7.2(已按吸收即死退役,git 提交 3443890d 可考)——卡④ V10 素材级读数:15 张素材 palette_check 只读模式 0 红灯 0 新增色;但 48 令牌 baseline 目录在库缺位(机检无对照基线),§4.1 两道门(语义检查+RGB 三通道差)的双门机检化缺承载。归工程改动批:建 baseline 目录+闸件双门判据落地。
**改动面**:工具代码
**状态**:待执行
#### ui_kit CAMP_HEADER_BAND 令牌退役评估(2026-11-02 登记,来源=批次移交)
**实况**:证据=迭代 47 审计记档 2 订正在案(git 提交 3443890d 可考)——营地域柜台头带消费面已随迭代 47 四柜台覆盖层退役注销(ui_contrast 令牌表四行随形态注销),现役消费面仅剩 keystone_badge 工厂与关卡屏/商店两处非营地域用途。处置口径(实现批备案):退役归 ui_kit 令牌批顺带处置、非独立强删;届时逐点核消费面后删令牌或改名。
**改动面**:游戏代码
**状态**:待执行
#### hobgoblin 内容三件套补齐(2026-11-01 登记,来源=审计发现)
**实况**:敌人表 hobgoblin 行(寻址键 (1, battle, 3))数值完整在役、战斗正常运行,但内容三件套未交付——立绘、敌种语义模板、敌人系统.md 敌种表行均缺,呈现面因语义模板未成文无定义可依。登记凭据=[敌人系统](design/gdd/systems/战斗循环/敌人系统.md) v1.1「hobgoblin 缺口登记」节(2026-10-13 迭代 24 反哺④)。
@@ -22,6 +38,22 @@
**状态**:待文档设计
#### ui_kit.gd 立绘注释陈旧顺带换代(2026-11-02 登记,来源=批次移交)
**实况**:证据=迭代 48 实现报告§六与验收审计观察项(已退役,git 提交 4c94eee3 可考)——`ui_kit.gd` TEX_HERO_PORTRAIT 行注释仍写「48px 档」(现行=原稿分辨率+显示档缩放),`path_check.py` 一处注释表述也已改指外;零行为,随下次触碰该两件的批次顺带换代(顺带件,不独立出批)。
**改动面**:游戏代码
**状态**:待执行
#### 迭代 43 批后移交小件——能力清单补行+四色墨断言(2026-10-30 登记,来源=批次移交)
**实况**:证据=迭代 43 反哺点清单 9/13(已按吸收即死退役,git 历史可考)。逐条:①**能力清单补行**——game/tests/README.md 能力清单无战斗一屏样张批 V5–V9 代表用例行(卡片成文早于清单 2026-10-29 改版定稿),归写卡段随下一写卡会话按 test-conventions §8.3 补行(能力=站桩呈现定值与素材绑定;代表用例=`game/tests/unit/presentation/standee_present_values_test.gd:_init`,真实入口=`UiKit.load_tex()`/`StandeeBody.name_line_probe()`),并同步覆盖域表「战斗表现.md」行。②**意图徽标四色墨补机检断言**——样张脚本 S1 文字对比为绘图用色值对表,意图徽标四色墨(对 PANEL_DATA 垫牌)未入 CHECKS;独立审计复算 4.97–5.62 全 ≥4.5 事实成立仅缺机检断言,`tools/render_sample_sheet_v3.py` CHECKS 补一条对表断言(values 全在册,零新增色)。**2026-10-31 写卡会话落位**:①能力清单补行已随「站桩名字行提档与地台色合并批」卡片成文同批入表(能力行+覆盖域表战斗表现.md 行);②四色墨断言仍待执行。
**改动面**:游戏代码+工具代码
**状态**:待执行
#### 架构审查优化讨论——全库架构审计读数的逐条裁决与整改分批(2026-10-29 登记,来源=审计发现)
**实况**:2026-10-29 全库架构静态审计完成,读数见 reports/审计-全库架构审查-2026-10-29.md(七节:总评/健康面/P0/P1/P2/待裁点/取证口径;零测试执行零引擎运行,全部结论带 文件:行号)。健康面已逐条核干净:Core 零 UI 八项判据全零命中、core 内 RNG 单点 100% 成立、字符串式信号连接全库 0 处、core 层零超长函数、19 表 ↔ 19 schema 一一对应、无循环 preload。问题集中在三条「写了没闸守」的纪律被实际破坏:①数值单一事实源被 18 处硬编码击穿,最危险两处是 `presentation/main.gd:787` 与 `:790` 把 `rap_refresh_cost=8` 与 `refresh_cap_per_round=2` 写死进提示文案(紧邻 783 行是正确读表的),表改文案不改、玩家直接受错误数字;另有 4 组代码自标「机制结构常量/产出约束」的字面量(`node_map_generator.gd:12` `MAX_WIDTH`、`action_economy.gd:14` `CHARGE_LAYER_CAP`、`board_data.gd:9` `SPAWN_AREA_CELLS`、`node_map_generator.gd:83-94` 域字面量)是否入表待裁。②`game/sim/` 与 `game/core/` 四组平行实现——扫雷推理引擎双份(`board_generator.gd` `measure_inferability` 族 vs `auto_player.gd` `_deduce` 族,输入集口径不同:前者含 safe_set oracle、后者 no-peek)、伤害掷点期望重算(`damage_probe.gd:196-201`)、档位排序与驱动循环双份(`level_autoplay.gd` vs `treasure_item_table.gd:44` 与 `battle_driver.gd:17`)、FakeDB 伪造表喂生成器——直接威胁「模拟读数 = 游戏真实数值」这一数值验证前提。③RNG 单点纪律在 core 外失守(sim 4 处/probe 1 处/tests 3 处自建 `RandomNumberGenerator`,均显式定种、确定性尚存),而 `static_scan.sh` 的 S1/S2 扫描面只含 `DIR_CORE` + `DIR_PRES`。守卫面另有三缺口:S8 是 9 文件显式清单(新增结算文件忘登记=静默不扫,缺位自检只防删改不防增)、S9 类型闸与 S13 质量闸扫描面止步于 core+data+presentation(sim/probe/tests 三层除 S11/S12 外零闸)、S13 三处豁免常态化到最大户文件(`battle_orchestrator`/`main.gd`/`tune_sim_controller`);另有四个 core 文件零测试点名(`attune_loadout`/`battle_fx_host`/`meta_state_store`/`slot_store`)。P2 级:`core/settlement/` 五段类直读写编排器私有成员 20+ 处(`oc._damage.*`/`oc._enemy_system.*`,GDScript 无编译期检查、改名静默断裂);架构文档三处漂移(`architecture.md:110` 登记的 `core/hero_ledger.gd` 盘上不存在且 `hero_ledger_test.gd:171` 显式断言它必须不存在、`:135` 行数过期、EC8 热重载门后 `ConfigDb.reload()` 零调用方且走 `load()` 拿不到磁盘新值);class_name 级双向依赖(`core/config_db` → `data/schemas/board_gen_table` → `core/board_data.SPAWN_AREA_CELLS`)与 `NODE_TYPES` 词表双份;`MetaProgression` 是唯一用 `get_node("/root/...")` 取依赖、唯一跨 Autoload 直调的单例。
@@ -55,18 +87,29 @@
**状态**:文档设计已闭合
#### 断言提取器改版——覆盖 Kit.check_quiet 静默通道(2026-10-29 登记,来源=会话提及)
**实况**:`tools/extract_checks.py:67` 的 `CALL_RE = check\(` 只认即时通道,静默收集通道 `Kit.check_quiet(cond, desc)` 静态站点实测 407 个在提取口径外(证据=`grep -rn "check_quiet(" game/tests --include="*.gd"` 实数;test-conventions §5.2 已备案该工具局限)。能力清单以 `文件:函数` 为锚,不受影响;但要把断言面全账纳入机器对账(重建账本或全量断言面审计),须先改版提取器:识别 `check_quiet(` 并按反参序(cond 在前)提取标签与 cond。改版后须以全量套件跑一遍 `--diff` 自证提取面覆盖、且 `Kit.check(` 与 `check_quiet(` 不双计。
**改动面**:工具代码
**状态**:待执行
#### 能力清单覆盖缺口补齐——六类无断言面或设计未定项(2026-10-29 登记,来源=会话提及)
**实况**:实际情况=2026-11-02 设计讨论闭合——④除名(用户裁:死亡灰化/碎裂属纯表现锦上添花,探针+人工目检已是合规终态,呈现与 battle.md 承诺行零触碰);范围收窄:④除名后余五类,其中②多语言半边(P1 功能缺口,归 owner 文档待设计问题清单)、③音频(P1 后置)、⑤数值落带半边(归 M4 数值批)三块延期不在本条,可写卡面=①增益状态结算链、②减动效开关、⑤机制门控、⑥锻造工坊结构四处。关联文档:
- `game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立,缺口证据)
- `design/gdd/systems/跨局循环/锻造工坊.md` v0.11(2026-11-02,验收标准节九条判据落文——⑥写卡输入合同)
- `design/gdd/systems/战斗循环/增益状态.md` v1.1(2026-10-26,①结算链规则定稿)
- `design/gdd/systems/冒险循环/难度曲线与Boss.md` v1.2(⑤机制门控规则定稿;数值带 provisional 归 M4)
- `design/gdd/systems/表现与支撑/设置与本地化.md` v1.1(②减动效开关与呈现承诺现行口径)
**实况**:证据=`game/tests/README.md` 覆盖域表 25 篇对照行(2026-10-29 立)。以下部分覆盖/未覆盖项作为缺口指针,逐项:①增益状态——只验契约词表与布尔受理,叠层/衰减/清除结算链无断言;②设置与本地化——减动效开关本体仅系统测试与冒烟覆盖、无单元/集成断言,多语言切换面未建;③音频——MVP 无音频,P1 后置无断言面;④战斗表现——死亡灰化与减动效演出档只有探针与人工目检;⑤难度曲线与Boss——难度曲线整链与 Boss 出场门控无专项断言;⑥锻造工坊——价格梯仅长剑路线逐级验,GDD 无验收标准节(`capability_audit` 验收标准豁免在册)。教学引导与生涯两篇的缺口归各自在册条目(「教学形态未定」「「生涯」界面——冒险中数据回看的唯一承接处」),不在本条。补齐时同批刷新覆盖域表与能力行,新增断言按 test-conventions §8.4 绑真实入口。
**改动面**:游戏代码+工具代码
**改动面**:游戏代码
**状态**:文档设计已闭合+待写卡
**状态**:待执行
#### run_all_gates 02 关联档多套件词传参修复(2026-10-31 登记,来源=批次移交)
**实况**:证据=iter45 实现报告 §5 遗留观察(已按吸收即死退役,git 历史可考)——`tools/run_all_gates.sh` 02 关联档以逗号连接多套件词传单个 `--only=a,b` 参数,而 `game/tests/run_all.gd` 选跑解析器不切逗号(多词整串做子串匹配→零命中 quit(2));iter45 施工时以多 `--only=` 旗标形态绕过。合回后 CI 跑全量档不经此路径不受影响,只挡开发期关联档窄跑。修法两案待裁:闸脚本改传多旗标,或解析器补逗号切分。
**改动面**:工具代码
**状态**:待执行
#### 战斗域结构深拆批——效果发射端口+掷格/受理分件(2026-10-31 重立,来源=批次移交)
@@ -83,6 +126,14 @@
**状态**:文档设计已闭合+已写卡+已编迭代 49
#### main.gd 拆件遗留死 preload 清理(2026-10-31 登记,来源=批次移交)
**实况**:证据=iter44 反哺点 11(已按吸收即死退役,git 历史可考;实现报告实现决定记录同载)——迭代 44 卡① 组合根注册面批拆件后,main.gd 遗留零使用 preload 常量 8 条(UiAssembler/TuneSimController/DebugSessionScript/TreasureInteractController/CurioInteractController/CampShopPresenter/BoardResolversScript/三场景 preload 中 main 零使用者三件);卡① 施工时仅清理与迁移直接相关的 ModalStack/RowPacer 两条,其余属存量债不扩范围,归工程改动直改批顺路清理。清理前须逐条 grep 复核零使用(布局批与 iter45 合回后消费面或有变化)。
**改动面**:游戏代码
**状态**:待执行
#### 数值标定窗口总挂账——原九条数值类条目合并(2026-10-28 登记,来源=会话提及)
**实况**:2026-10-28 用户令把九条数值类挂账合并为一条:削弱档位的最终数值/迭代 10 奖励重构联动欠账/调参批数值点评登记/三方向各自的解锁清单与定价/GP 经济换算失衡/天赋树实装批登记欠账余两件/商店独立刷新机制两参数/基线通关率越带加难杠杆扫描/成长系数节奏——原九条同步删除,内容全量迁入汇总文档,git 历史可考(本提交)。共同背景:M4 数值批卡已随 2026-10-16 删卡令退役;2026-10-18 用户裁=各条留待办清单、未设计未启动、不另行归属,未来数值标定窗口重开时重走设计段再写卡——本条沿用该裁定,重启条件=用户发令开窗口。合并动机=九条共享同一重启条件与同一执行形态(窗口内一次 MC 批量统一覆盖各条对比需求:全冒险期望+奖励期望),分散登记徒增维护面。逐条挂账明细、裁定史与相关文档见汇总文档。