Files
gplan-2/game/core/treasure_items.gd
T
DKChan 1b9736f065 fix(iter37①): T1 core 域缺陷修——单据归属判定/数值入表/公式单点/信号契约载体统一
- d1 伤害控制器来源敌作废判定 `==`→`is_same`(同内容异实例不再误废;夹具双敌两断言〔wd1〕)
- d11 shop 表新增 relic_base_price/spend_discount 两 knob+域检+ConfigDB 访问器+level_runner 读表;
  level_shop_economy_test 三表值断言+relic 现价读表与迁移前字面量逐位等价〔wd11〕
- d16a HeroTable.display_name+域检+访问器+unit_container 读表;spawn 真实链断言〔wd16〕
- d14 board_generator 目标雷数公式单点 target_mine_count(两调用点改引)+legality 去 db 参数(两调用点同批改)〔wd14〕
- d15 蓄力层上限 CHARGE_LAYER_CAP 常量(两处字面量+注释随动)
- d16b unit_container/status_container 六处 Dictionary 取值显式 int() 包裹(读侧包)
- d10 minesweeper 两信号按 §2.1 三要素补契约(载荷键集按发射位并集实查)+全库 28 处信号注释载体 #→##

撤销证明:回退 d1 hunk → 同容异实例断言红(A 单据被 B 死亡误废),还原复绿;
回退 d10 契约 → 契约在位扫描 undocumented=2 红,还原归零。等价类申报(d11/d14/d15/d16)见实现报告。
代拍板:#1 target_mine_count 签名(照接口块)、#2 契约措辞与载荷键并集(实查发射位)、#4 int() 读侧包写法。
2026-10-25 03:35:55 +08:00

226 lines
8.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 秘宝关卡域库存+整图叠加层:全程零随机零特判,同操作序列重放库存与叠加层投影逐位相同(垂直切片验证:道具全程确定性)。
## Used 不可逆——consume 后保留只读,枚举序=受理序;库存关卡域跨战斗延续、关卡末清空,叠加层随图消亡(清空编排归调用方,本类只供 clear/reset/retire_overlays)。
## 零特判纪律:本类不写任何其他系统状态,排雷玩法代码零本系统分支;相位检查归战斗编排 use_treasure——validate_use 是纯谓词零消耗。
class_name TreasureItems
extends RefCounted
var _items: Array[Variant] = [] # [{handle, item_id, used}]——受理序;Used 保留只读
var _handle_seq := 0 # 受理序句柄(单调递增——零随机)
var _reveal_cells := {} # 真视集:Vector2i → true(危险格标记——封印仍走既有 seal 路径)
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[Variant] = []
for rec in _items:
var row: Dictionary[Variant, Variant] = 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[Variant, Variant] = _item_row(item_id)
if row.is_empty():
return false
var kinds: Array[Variant] = 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("treasure_item_row"):
return _db.treasure_item_row(item_id) # 表访问收口(正式面走 ConfigDB 访问器)
return _db.treasure_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役)
# 不标具体类型原因:单元测试注入 StubDB 桩表(treasure_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注
var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表)
func _init(db_ref = null) -> void:
_db = db_ref
# ---- 使用校验(战斗结算 use_treasure 编排第二步——纯谓词+投影,零消耗零副作用)----
# 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[Variant, Variant] = _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[Variant] = row.get("effect_kinds", [])
var resolved: Array[Variant] = []
if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"):
# 空间效果(真视 reveal_tiles/天眼复合 safe_dig 入空间解析;
# 真视窄域护栏 REVEAL_KINDS 同守):
if target.x < 0 or target.y < 0 or target.x >= int(board_size.x) or target.y >= int(board_size.y):
return _reject("目标越板")
if String(row.get("target", "")) == "cell":
# 单格档(线香真视单格;≤3×3 窄域契约内的点目标)
resolved.append(target)
else:
# 3×3 窗口 ∩ 板内格(边缘受理——护栏只缩不扩)
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()
# ---- 断点快照(关卡域库存随停点入档/回局)----
# 快照面 = {item_id, source_class, origin_cell:[x,y], round_index, used}(JSON 安全形——Vector2i 转
# 数组、键全 String);handle 不入档(受理序句柄,回局时按序重排)。叠加层集不入快照(战斗域
# 随图消亡——停点处无战斗在飞,天然空集)。
func snapshot_inventory() -> Array:
var out: Array[Variant] = []
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[Variant] = 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}
# 探针出口(断言面:库存序/叠加层集/拒绝原因)
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,
}