merge: M-3迭代7魔法道具内容+元进度数值标定 T0-T8全落——AuxItemTable六行+掉落投放(解锁门控空集零投放)+库存四步受理+三选一奖励节点+HUD两步交互+叠加层呈现+工坊封存呈现+换算率复核读数(6/9/1 vs 实测1.7/4.1/0.0 U1呈报);重闸全绿(冒烟577/0+五探针+单测12套+doc三闸熵增零新增+机检11/11+MC剥additive 0/400逐位);出口=T9目检(检查单已交);台账259-263

# Conflicts:
#	game/presentation/views/node_overlay.gd
This commit is contained in:
DKChan committed 2026-09-29 08:59:31 +08:00
commit fd39a58aee
35 files changed
+1990 -19

No files matched your search

+11
View File
@@ -134,6 +134,17 @@ func notify_settlement_done(next_round_mode: String) -> bool:
# 下轮行动点修正排队(格面资源八型批 2026-09-20):action 型正负行结算期调用,同轮多次授予累加;
# 相位无关(授予发生在结算段,此时本类可能处于 ACTIVE/WAITING 任一相位)
# 第 9 API(registry 行动经济×① 接口漂移预授权——迭代 7 T2 魔法道具内容批落位):ap_grant 道具的
# 当轮行动点加量(GDD「加行动点效果给当轮」——区别于 queue_ap_delta 的下轮排队语义)。Active 受理、
# 无上限域、零随机;使用道具本身零扣费(成本在获取时已付——捡道具格不出战斗奖励)。
func grant_ap(amount: int) -> bool:
if amount <= 0 or _phase != PHASE_ACTIVE:
_reject("grant_ap")
return false
_ap += amount
return true
func queue_ap_delta(delta: int) -> void:
_pending_ap_delta += delta
+6
View File
@@ -64,6 +64,8 @@ var _last_level_payload := {} # 最近一次关卡结束载荷(失
var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用)
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
var _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面)
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面)
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
@@ -93,6 +95,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
_pending_next_level = false
_base_override = _normalize_override(params.get("hero_override", {}))
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} # additive(迭代 7 T5)
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
state = "running"
return _start_current_level()
@@ -188,6 +191,7 @@ func _on_level_finished(payload: Dictionary) -> void:
if result != "victory":
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
return
_last_aux_cleared = int(payload.get("aux_cleared", 0)) # additive(迭代 7 T4——末关 summary 取数面)
if level_index < levels_total:
# 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约)
var hp_bonus: int = int(db.adventure.level_clear_hp_bonus)
@@ -202,6 +206,7 @@ func _on_level_finished(payload: Dictionary) -> void:
"new_max_hp": int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus["max_hp"]),
"keystone_gained": int(db.adventure.keystone_per_level),
"keystone_total": keystone,
"aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数)
})
return
# 末关通关:钥石照记、不发上限提升(没有下一战可受益)——冒险完成结算
@@ -218,6 +223,7 @@ func _settle(result: String) -> void:
"stopped_at": 0 if result == "completed" else level_index,
"keystone_total": keystone,
"master_seed": master_seed,
"aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 big_reward 载荷)
}
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
+192
View File
@@ -0,0 +1,192 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 道具从掉落到使用的全程零随机零特判——同操作序列重放两遍,库存与叠加层投影逐位相同(P0)
# Date: 2026-09-29(迭代 7 T2 core 骨架——魔法道具内容批;设计权威 design/gdd/systems/排雷循环/魔法道具.md
# + registry aux_items api 8 入口/load_bearing_rules/use_orchestration)
# 魔法道具 关卡域库存 + 板级叠加层(自持派生信息层)。
# 结构冻结照抄(违反即返工):
# 零随机(零 RNG 流消费——信息只来自 排雷玩法 恒真数字/地图战斗资源 Ready 映射/行动经济 账本三既有事实源);
# 库存关卡域(跨轮跨图跨战斗延续、关卡末清空——清空编排归 战斗结算/关卡层,本类只供 clear/reset);
# 叠加层随图消亡(换图/弃图刷新/战斗结束三清——编排归 战斗结算 retire_overlays);
# Used 不可逆(consume 后保留只读——枚举序=受理序);
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空);点击级零信息不消耗(拒绝零消耗——预读窗口全已开);
# 零特判(排雷玩法 代码零本系统分支——本类不写任何其他系统状态)。
# 负空间:无自动使用入口/无重掷/无事件订阅/无 RNG 直连/无 排雷玩法/地图战斗资源 写入口;
# 相位检查归 战斗结算 use_aux 编排第一步(本类不管相位——validate_use 是纯谓词+投影)。
# 呈现取数面:query_inventory(C 区槽位)/query_overlays(棋盘叠加层标记)——只读投影,零数据复制
# (预读徽章数据源=战斗结算 query_reward_identity 既有读面,本类只记「哪些格要额外显示」)。
class_name AuxItems
extends RefCounted
var _items: Array = [] # [{handle, item_id, used}]——受理序;Used 保留只读
var _handle_seq := 0 # 受理序句柄(单调递增——零随机)
var _reveal_cells := {} # 真视集:Vector2i → true(惩罚格标记——插旗仍走既有 flag 路径)
var _preview_cells := {} # 预读集:Vector2i → true(奖励身份徽章呈现挂点)
var _last_reject := "" # 诊断面(最近一次拒绝原因——呈现反馈取数)
# ---- 生命周期 ----
# 关卡开始/关卡末清空(clear_inventory 幂等;关卡末清空前取 inventory_count() 快照做可见反馈)
func reset() -> void:
_items.clear()
_handle_seq = 0
retire_overlays()
# 授予(战斗结算 grant 路由——来源可归因:source_class+cell+round_index)
func add_item(item_id: String, source_class: String, cell: Vector2i, round_index: int) -> Dictionary:
_handle_seq += 1
var rec := {
"handle": _handle_seq,
"item_id": item_id,
"source_class": source_class,
"origin_cell": cell,
"round_index": round_index,
"used": false,
}
_items.append(rec)
return rec.duplicate()
# ---- 库存读面(C 区槽位常读)----
# 枚举序=受理序;Used 保留只读;逐件含 usable 板级零信息预判(调用方喂板面回调)
func query_inventory(board_has_preview_info: Callable) -> Array:
var out: Array = []
for rec in _items:
var row: Dictionary = rec.duplicate()
row["usable"] = _usable_now(String(rec.item_id), board_has_preview_info)
out.append(row)
return out
func inventory_count() -> int:
return _items.size()
# 板级零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级):
# 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用);
# 纯预读行 = 板上还有未开格才可用。
func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool:
var row: Dictionary = _item_row(item_id)
if row.is_empty():
return false
var kinds: Array = row.get("effect_kinds", [])
if kinds.has("mine_reveal") or kinds.has("ap_grant"):
return true
return bool(board_has_preview_info.call()) if board_has_preview_info.is_valid() else true
func _item_row(item_id: String) -> Dictionary:
if _db != null and _db.aux_items != null:
return _db.aux_items.row_by_id(item_id)
return {}
var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表)
func _init(db_ref = null) -> void:
_db = db_ref
# ---- 使用校验(战斗结算 use_aux 编排第二步——纯谓词+投影,零消耗零副作用)----
# target = 目标格(空间行=3×3 窗口中心;ap 行忽略)。board_size = 板维度;
# cell_unrevealed_cb = Callable(Vector2i) -> bool(排雷玩法 只读面——预读窗口信息判据)。
# 返回 {ok, reason, resolved: Array[Vector2i], item_id, kinds}——ok=false 时 reason 可呈现。
func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unrevealed_cb: Callable) -> Dictionary:
var rec := _rec_by_handle(handle)
if rec.is_empty():
return _reject("句柄不存在")
if bool(rec.used):
return _reject("道具已使用(Used 不可逆)")
var row: Dictionary = _item_row(String(rec.item_id))
if row.is_empty() or bool(row.get("retired", false)) or bool(row.get("deferred", false)):
return _reject("道具未激活")
var kinds: Array = row.get("effect_kinds", [])
var resolved: Array = []
if kinds.has("mine_reveal") or kinds.has("reward_preview"):
# 空间效果:3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
return _reject("目标越板")
for dy in range(-1, 2):
for dx in range(-1, 2):
var c := target + Vector2i(dx, dy)
if c.x >= 0 and c.y >= 0 and c.x < int(board_size.x) and c.y < int(board_size.y):
resolved.append(c)
# 点击级零信息:纯预读行窗口全已开 = 没东西可看(拒绝零消耗);
# 真视恒可用(零雷窗口=「这 9 格没雷」信息非空——GDD 裁定)
if kinds.has("reward_preview") and not kinds.has("mine_reveal"):
var any_unrevealed := false
if cell_unrevealed_cb.is_valid():
for c in resolved:
if bool(cell_unrevealed_cb.call(c)):
any_unrevealed = true
break
if not any_unrevealed:
return _reject("窗口内无可预读的未开格")
return {
"ok": true,
"reason": "",
"resolved": resolved,
"item_id": String(rec.item_id),
"kinds": kinds,
"magnitude": int(row.get("magnitude", 0)),
}
# ---- 叠加层(战斗结算逐 kind 应用——空间先、ap 后的序在编排侧钉死)----
func apply_overlay(kind: String, cells: Array) -> void:
var store := _reveal_cells if kind == "mine_reveal" else _preview_cells
for c in cells:
store[Vector2i(c)] = true
# 三清只读投影(棋盘叠加层标记渲染——真视集/预读集;随图消亡由 retire_overlays 承载)
func query_overlays() -> Dictionary:
return {
"mine_reveal": _reveal_cells.keys(),
"reward_preview": _preview_cells.keys(),
}
# 换图/弃图刷新/战斗结束——叠加层清空(已花道具不返还;库存不动——库存关卡域)
func retire_overlays() -> void:
_reveal_cells.clear()
_preview_cells.clear()
# ---- 消耗(编排第四步——Used 不可逆+快照回执)----
func consume(handle: int) -> Dictionary:
var rec := _rec_by_handle(handle)
if rec.is_empty() or bool(rec.used):
return {}
rec.used = true
return rec.duplicate()
# ---- 内部 ----
func _rec_by_handle(handle: int) -> Dictionary:
for rec in _items:
if int(rec.handle) == handle:
return rec
return {}
func _reject(reason: String) -> Dictionary:
_last_reject = reason
return {"ok": false, "reason": reason, "resolved": [], "item_id": "", "kinds": [], "magnitude": 0}
# 探针出口(T7 收拢——断言面:库存序/叠加层集/拒绝原因)
func probe() -> Dictionary:
return {
"count": _items.size(),
"used": _items.filter(func(r): return bool(r.used)).size(),
"reveal_cells": _reveal_cells.size(),
"preview_cells": _preview_cells.size(),
"last_reject": _last_reject,
}
+1
View File
@@ -0,0 +1 @@
uid://uvt473an5yxh
+66 -1
View File
@@ -50,6 +50,8 @@ var action_economy: ActionEconomy
var _enemy_system: UnitContainer
var _status_effects: StatusContainer
var _reward_economy: RewardEconomy
var aux_inventory = null # AuxItems(迭代 7 T3——LevelRunner 注入的关卡域库存引用;
# 每战新建本类,库存不归本类所有——注入面)
var _damage: DamageController # 伤害控制器(伤害管线单据化批 2026-09-24——单据唯一兑付管线)
var encounter: Dictionary[String, Variant] = {}
@@ -170,6 +172,8 @@ func _install_new_board(during_round := false) -> bool:
_board_from_midround_refresh = during_round # 轮内刷新装板 = 刷新图(清图奖励不发);常规换图/开战装板 = false
_board_seq += 1
board_changed.emit()
if aux_inventory != null:
aux_inventory.retire_overlays() # 迭代 7 T3——叠加层随图消亡(换图/刷新图装板点;已花道具不返还)
return true
@@ -351,7 +355,67 @@ func _settle() -> void:
# 「动作 × 格型」矩阵的执行面:snap = 缓冲期冻结身份快照(cell_type = 该格装板期摇定的类型身份),
# polarity 由动作决定(开安全格/标中旗 = positive,开雷格/标错旗 = negative),
# 兑付行 = 奖励字典(类型, 极性)行。返回 true = percent 行实际生效(flat 行恒 false——兑付结果由行协议承载)。
# ---- 魔法道具(迭代 7 T2/T3——库存授予路由 + use_aux 四步受理编排;结构冻结见魔法道具 GDD)----
# 授予路由 aux 分支:正=入库存(发行「获得」行——逐行归因纪律);负=零兑付占位行。
func _grant_aux(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int) -> bool:
if polarity != "positive" or aux_inventory == null:
# 负路由占位行=effect 型零兑付(雷安同池下标错旗/雷格摇中 aux 身份的负向字典行——逐行归因纪律:
# 该格资源的负向兑付存在且为零;effect_id 空不入增益容器)
_emit_row("effect", source_class, cell, ri, "道具随雷沉没(惩罚格不入库存)", 0,
{"effect_id": "", "aux_sink": String(snap.get("effect_id", ""))}, "")
return false
var iid := String(snap.get("effect_id", ""))
var arow: Dictionary = db.aux_item_row(iid)
var rec: Dictionary = aux_inventory.add_item(iid, source_class, cell, ri)
_emit_row("effect", source_class, cell, ri, "获得 %s" % String(arow.get("display_name", iid)), 1,
{"aux_item": iid, "handle": int(rec.get("handle", 0))}, "")
return true
# use_aux 四步受理(registry use_orchestration 规范序——唯一调用权归本类;序即原子:
# ① 相位 gate(Active——行动经济 phase getter)→ ② validate_use(目标校验读 排雷玩法 只读面,零消耗)→
# ③ 逐 kind 应用(顺序钉死:空间先、ap_grant 后——effect_kinds 列表序非运行时语义)→ ④ consume。
# 拒绝全部发生在 ①②(后两步合法输入下无失败路径);使用零 AP 成本(不经 try_spend)。
# 结算明细无道具行(GDD——效果即时可见:overlay 呈现+AP 条刷新;AP 过账本 grant_ap 走 _reject 面)。
func use_aux(handle: int, target: Vector2i) -> Dictionary:
if aux_inventory == null:
return {"ok": false, "reason": "库存未接线"}
if action_economy.phase != action_economy.PHASE_ACTIVE:
return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"}
var board = _minesweeper_ref()
var v: Dictionary = aux_inventory.validate_use(handle, target,
Vector2(board.width, board.height),
func(c: Vector2i): return not board.is_revealed(c))
if not bool(v.ok):
return v # ② 拒绝零消耗
var kinds: Array = v.kinds
if kinds.has("mine_reveal"):
aux_inventory.apply_overlay("mine_reveal", v.resolved)
if kinds.has("reward_preview"):
aux_inventory.apply_overlay("reward_preview", v.resolved)
if kinds.has("ap_grant"):
if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
var rec: Dictionary = aux_inventory.consume(handle)
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
# aux 解锁门控转发(迭代 7 T5——LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
func set_aux_unlocked(ids: Dictionary) -> void:
_reward_economy.set_aux_unlocked(ids)
func _minesweeper_ref() -> Variant:
return minesweeper
func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool:
# 迭代 7 T3——aux 投放行授予路由(魔法道具 GDD:授予路由唯一调用方=战斗结算):正路由(挖开/标中)
# → 道具入关卡域库存(来源可归因);负路由(雷格/标错旗摇中 aux 身份——雷安同池)→ 零兑付
# 「道具随雷沉没」(雷格奖励沉没既有语义);清图批追加份同路由(清图翻倍语义对 aux 同样成立)。
if String(snap.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
return _grant_aux(snap, polarity, source_class, cell, ri)
var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity)
if row.is_empty():
_fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity])
@@ -717,7 +781,8 @@ func _battle_over(result: String) -> void:
_reward_economy.retire_mapping()
_enemy_system.despawn_all()
_status_effects.notify_battle_end()
action_economy.notify_combat_end() # → Idle,RAP 清零
if aux_inventory != null:
aux_inventory.retire_overlays() # 迭代 7 T3——叠加层随战斗结束清空(库存不动——关卡域跨战斗) action_economy.notify_combat_end() # → Idle,RAP 清零
db.set_reload_gate(false)
_reset_round_facts()
battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped})
+55
View File
@@ -23,6 +23,7 @@ const TABLE_MANIFEST := {
"difficulty": "difficulty.tres",
"shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费)
"meta": "meta.tres", # 元进度实现批 2026-09-21(MetaTable 四行族——schema 与表同批落籍)
"aux_items": "aux_items.tres", # 迭代 7 T1 魔法道具内容批(AuxItemTable 六行——定义表 Rule 4 豁免;罗盘 retired/失义件 deferred 标记见 schema 头注)
"adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点)
}
@@ -42,6 +43,7 @@ var difficulty: DifficultyTable
var shop: ShopTable = null
# 元进度实现批(2026-09-21):MetaTable 类型化持用(schema 与表同批落籍,无 shop 先例的落籍间隙问题)
var meta: MetaTable
var aux_items: AuxItemTable # 迭代 7 T1 类型化持用(消费面走 aux_* 访问器)
var adventure: AdventureTable
var is_ready := false
@@ -126,6 +128,7 @@ func load_tables(root: String, collect_only: bool) -> bool:
difficulty = loaded.get("difficulty")
shop = loaded.get("shop")
meta = loaded.get("meta")
aux_items = loaded.get("aux_items")
adventure = loaded.get("adventure")
is_ready = not has_errors
return not has_errors
@@ -144,9 +147,43 @@ func _cross_validate(loaded: Dictionary) -> void:
# GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键
# (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。
for row in rt.rows:
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面
var eff: String = row.get("effect_id", "")
if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff):
push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff])
# ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id,
# 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役
# 后仍投放(连带错误),同拦。
var aux_live := {}
var aux_tab = loaded.get("aux_items")
if aux_tab != null:
for arow in aux_tab.active_rows():
aux_live[String(arow.get("item_id", ""))] = true
for row in rt.rows:
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE:
continue
var aux_eff: String = String(row.get("effect_id", ""))
if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验
continue
if not aux_live.has(aux_eff):
push_diag("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"])
# ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter}
# = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减
if aux_tab != null:
var covered := {}
for row in rt.rows:
if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE or int(row.get("weight", 0)) <= 0:
continue
var arow: Dictionary = aux_tab.row_by_id(String(row.get("effect_id", "")))
for tag in arow.get("dimension_tags", []):
covered[String(tag)] = true
var f2_exempt := {"plan": true, "info": true, "counter": true} # 豁免面(随 Alpha 重定/天赋批消减)
for d in AuxItemTable.DIMENSIONS:
if f2_exempt.has(d):
continue
if not covered.has(d):
push_diag("error", "dimension_coverage", "F2 投放维度覆盖缺 %s(豁免面外零活行投放)" % d)
# ② SettlementTable.effect_pool_map 悬空
for eff in st_tab.effect_pool_map.keys():
if not effect_ids.has(eff):
@@ -347,6 +384,24 @@ func status_effect_row(effect_id: String) -> Dictionary:
# ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)----
# ── 魔法道具访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)──
# 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径
func aux_active_rows() -> Array:
return aux_items.active_rows() if aux_items != null else []
func aux_item_row(item_id: String) -> Dictionary:
return aux_items.row_by_id(item_id) if aux_items != null else {}
# 活行词表(掉落投放 effect_id 白名单)
func aux_live_ids() -> Dictionary:
var out := {}
for row in aux_active_rows():
out[String(row.get("item_id", ""))] = true
return out
# 行族四轴(数据契约,行集归 shop.tres):gp_drops {tier, mean, roll_min, roll_max} ·
# level_gp_bonus {level_index, bonus} · products {product_id, display_name, attr, flat_value,
# base_price, price_increment} · price_level_bonus {level_index, bonus}。
+28 -1
View File
@@ -62,6 +62,9 @@ var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化
var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零)
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
var _settlement = null # 当前战斗编排器(关卡结束置 null)
var aux_inventory = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续,
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放)
# 关卡末清空+快照计数入 level_finished 载荷)
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
var hero_hp_pct := 0
@@ -364,6 +367,11 @@ func _enter_node(node: Dictionary) -> bool:
func _start_battle(node: Dictionary) -> bool:
_settlement = BattleOrchestrator.new(db, rng)
if aux_inventory == null:
aux_inventory = AuxItems.new(db) # 惰性建——关卡域单实例(跨战斗延续)
_settlement.aux_inventory = aux_inventory # 迭代 7 T3——库存注入(授予路由+use_aux 编排消费面)
if _settlement != null:
_settlement.set_aux_unlocked(_aux_unlocked) # 迭代 7 T5——解锁门控下传(装板摇号前)
# core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长
# RefCounted 生命周期乃至成环:settlement 的信号持捕获 runner 的 lambda,runner 又持 settlement)
_settlement.step_row.connect(_forward_step_row)
@@ -408,8 +416,13 @@ func _on_battle_finished(payload: Dictionary) -> void:
close_wallet() # 单关模式:关卡收束即冒险收束(GP 清零);冒险模式归 AdventureRunner._settle
_running = false
_settlement = null
var aux_cleared := 0
if aux_inventory != null:
aux_cleared = aux_inventory.inventory_count() # 快照计数(清空前——「剩余道具随关卡结束清空」可见反馈取数)
aux_inventory.reset() # 迭代 7 T3——关卡末库存清空(无论输赢,幂等)
level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index,
"node_id": _node_id(node), "node_type": String(node.get("type", ""))})
"node_id": _node_id(node), "node_type": String(node.get("type", "")),
"aux_cleared": aux_cleared}) # additive 扩签(不参与派生——node_map 扩签先例)
return
# 接口漂移清偿(关卡节点地图 v1.0 ⑦):战斗胜利不再自动推进——确认后进选路口
_settlement = null
@@ -462,6 +475,20 @@ func pick_next_node(node_id: int) -> bool:
func current_battle():
return _settlement
# aux 解锁门控集注入(迭代 7 T5——AdventureRunner 自 params 传递)
func set_aux_unlocked(ids: Dictionary) -> void:
_aux_unlocked = ids
if _settlement != null:
_settlement.set_aux_unlocked(ids)
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)
func grant_aux_item(item_id: String, source: String) -> Dictionary:
if aux_inventory == null:
aux_inventory = AuxItems.new(db)
return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
return _settlement
# ---- 玩家输入转发(呈现层只与 runner 对话)----
+3
View File
@@ -86,12 +86,15 @@ func workshop_status() -> Array:
var out: Array[Dictionary] = []
for row in db.meta_workshop_items():
var iid := String(row.get("item_id", ""))
var arow: Dictionary = db.aux_item_row(iid) # 迭代 7 T5——失义/退役状态面(呈现「能力封存/已退役」)
out.append({
"item_id": iid,
"display_name": String(row.get("display_name", "")),
"threshold": int(row.get("clear_threshold", 0)),
"unlocked": save.item_unlocked(iid),
"progress": _workshop_progress(String(row.get("channel", "total_clears"))),
"deferred": bool(arow.get("deferred", false)),
"retired": bool(arow.get("retired", false)),
})
return out
+13
View File
@@ -204,10 +204,23 @@ func weighted_pick(rows: Array, r: int) -> String:
return ""
# aux 解锁门控集(迭代 7 T5——拍板③甲「解锁→进投放池」:未解锁 aux 行不入摇号池。
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
var _aux_unlocked := {}
func set_aux_unlocked(ids: Dictionary) -> void:
_aux_unlocked = ids
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array[Dictionary] = []
for row in db.reward.rows:
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
var iid := String(row.get("effect_id", ""))
if iid == "" or not _aux_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