# 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() # 持有数(迭代 10 奖励重构批——同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有) func holding_count() -> int: var n := 0 for rec in _items: if not bool(rec.get("used", false)): n += 1 return n # 同调位守卫谓词(2026-10-02 修复批——四处手抄收口单点:战斗结算授予路由 / 关卡授予 / 三选一双渠道): # holding = 调用方渠道的当前实际持有数(库存渠道传 holding_count()、通关渠道传跨关携带数、 # 节点渠道传库存+携带合计);读数源 = db.adventure.transitional_sync_slots(负值钳 0 口径同前)。 # 返回再入 n 件是否仍在位内。挂 static:谓词纯算术不依赖实例态——通关渠道(跨关携带)没有 # AuxItems 实例可用,实例方法形态不成立;db 参数不标类型(同 _db 桩表注入纪律)。 static func can_accept(n: int, holding: int, db) -> bool: return holding + n <= maxi(0, int(db.adventure.transitional_sync_slots)) # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) func holding_records() -> Array: var out: Array = [] for rec in _items: if not bool(rec.get("used", false)): out.append({"handle": int(rec.get("handle", 0)), "item_id": String(rec.get("item_id", ""))}) return out # 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含) func discard_item(handle: int) -> bool: for i in _items.size(): if int(_items[i].get("handle", -1)) == handle and not bool(_items[i].get("used", false)): _items.remove_at(i) return true return false # 板级零信息可用性预判(图标置灰/入口禁用判据——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: return {} if _db.has_method("aux_item_row"): return _db.aux_item_row(item_id) # 表访问收口(2026-10-02——正式面走 ConfigDB 访问器) return _db.aux_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役) # 不标具体类型原因:单元测试注入 StubDB 桩表(aux_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注 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() # ---- 断点快照(迭代 8 卡①——关卡域库存随停点入档/回局)---- # 快照面 = {item_id, source_class, origin_cell:[x,y], round_index, used}(JSON 安全形——Vector2i 转 # 数组、键全 String);handle 不入档(受理序句柄,回局时按序重排)。叠加层集不入快照(战斗域 # 随图消亡——停点处无战斗在飞,天然空集)。 func snapshot_inventory() -> Array: var out: Array = [] for rec in _items: var cell: Vector2i = rec.get("origin_cell", Vector2i(-1, -1)) out.append({ "item_id": String(rec.get("item_id", "")), "source_class": String(rec.get("source_class", "")), "origin_cell": [cell.x, cell.y], "round_index": int(rec.get("round_index", 0)), "used": bool(rec.get("used", false)), }) return out func restore_inventory(items: Array) -> void: _items.clear() _handle_seq = 0 for row in items: _handle_seq += 1 var cell_arr: Array = row.get("origin_cell", [-1, -1]) _items.append({ "handle": _handle_seq, "item_id": String(row.get("item_id", "")), "source_class": String(row.get("source_class", "")), "origin_cell": Vector2i(int(cell_arr[0]), int(cell_arr[1])), "round_index": int(row.get("round_index", 0)), "used": bool(row.get("used", false)), }) retire_overlays() # ---- 内部 ---- 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, }