Files
gplan-2/game/core/treasure_items.gd
T

234 lines
9.5 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()
# 持有数(同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有)
# 同调位守卫谓词(读取源 transitional_sync_slots 过渡键已随表退役;
# 容量闸正主 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏——
# 冒烟「过渡键读取点归零」断言覆盖)
# 持有件记录(奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标)
# 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含)
# 板级零信息可用性预判(图标置灰/入口禁用判据——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,
}