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

229 lines
9.2 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
# 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()
# ---- 断点快照(迭代 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,
}