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:
commit
fd39a58aee
35 files changed
+1990
-19
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://uvt473an5yxh
|
||||
@@ -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})
|
||||
|
||||
@@ -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}。
|
||||
|
||||
@@ -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 对话)----
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 挖掘期撞上推理断头时,玩家能否从掉落池捡到魔法道具、在关键时刻花掉买回情报或行动点(P0)
|
||||
# Date: 2026-09-29(迭代 7 T1 数据面——魔法道具内容批;设计权威 design/gdd/systems/排雷循环/魔法道具.md v0.4+
|
||||
# + registry aux_items 六行 mvp_content;2026-09-29 用户拍板:①乙 探宝罗盘退役留痕 ④甲 显影尘/地穴全图卷失义件
|
||||
# 六行全建零投放「已解锁·能力封存中」)
|
||||
# 数据配置 AuxItemTable schema(魔法道具 回填 owner)。
|
||||
# 行 = Dictionary,键 item_id(Rule 4 豁免:定义表——registry aux_items Rule 4 豁免先例)。
|
||||
# 七字段(registry fields 全量):item_id 主键 / display_name / dimension_tags ⊆ 五维 非空 /
|
||||
# effect_kinds ⊆ {mine_reveal, reward_preview, ap_grant}(活性校验见下)/ target ∈ {none, area_3x3, board_wide} /
|
||||
# magnitude [0,999](仅 ap_grant 行消费,加载校验 ≥1)/ use_phase ∈ {active_dig}。
|
||||
# additive 两标记(迭代 7 T1——拍板①④的表内表达,非 registry fields 预登记字段的扩列随批登记):
|
||||
# retired = 退役留痕行(loot_compass——未开格类型微光常显覆盖预读价值,2026-09-29 用户拍板退役;
|
||||
# 行名保留=meta.tres workshop_items 六件词表恰全校验兼容,存档 item_unlocks 旧键无害;
|
||||
# 免活性校验、不入投放校验、不入 F2 覆盖面、奖励节点/掉落池排除)
|
||||
# deferred = 效果失义待重定行(probe_dust/catacomb_scroll——原显影半边随雾格移除失义,效果与目标
|
||||
# 随魔法道具整体重设计归 Alpha(2026-09-20 用户拍板);免 effect_kinds 非空校验、不入投放与 F2)
|
||||
# 承重护栏(魔法道具 GDD §结构冻结,schema 校验强制不靠运行时拦截):
|
||||
# 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷);
|
||||
# board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行,
|
||||
# 显影载体待 Alpha 重定后落位)。
|
||||
class_name AuxItemTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array = [] # Array[Dictionary] 键 item_id
|
||||
|
||||
const DIMENSIONS := ["info", "risk", "action", "plan", "counter"]
|
||||
const EFFECT_KINDS := ["mine_reveal", "reward_preview", "ap_grant"]
|
||||
const TARGETS := ["none", "area_3x3", "board_wide"]
|
||||
const USE_PHASES := ["active_dig"]
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array = []
|
||||
if rows.is_empty():
|
||||
# 空表合法态(2026-09-17 用户拍板——切片期无道具内容;F2 覆盖检查仅对非空清单执行)
|
||||
return errs
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var iid: String = row.get("item_id", "")
|
||||
if iid == "":
|
||||
errs.append({"category": "domain", "message": "item_id 空"})
|
||||
continue
|
||||
if seen.has(iid):
|
||||
errs.append({"category": "dup_key", "message": "item_id 重复: %s" % iid})
|
||||
continue
|
||||
seen[iid] = true
|
||||
if String(row.get("display_name", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % iid})
|
||||
var retired: bool = bool(row.get("retired", false))
|
||||
var deferred: bool = bool(row.get("deferred", false))
|
||||
# dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留)
|
||||
var tags: Array = row.get("dimension_tags", [])
|
||||
if tags.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid})
|
||||
for tag in tags:
|
||||
if not DIMENSIONS.has(String(tag)):
|
||||
errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]})
|
||||
var kinds: Array = row.get("effect_kinds", [])
|
||||
var target: String = String(row.get("target", "none"))
|
||||
if not TARGETS.has(target):
|
||||
errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]})
|
||||
var phase: String = String(row.get("use_phase", "active_dig"))
|
||||
if not USE_PHASES.has(phase):
|
||||
errs.append({"category": "enum", "message": "%s use_phase 出域 %s: %s" % [iid, str(USE_PHASES), phase]})
|
||||
if retired or deferred:
|
||||
# 退役/失义行:免 effect_kinds 非空与承重护栏校验(登记面保留,不入投放与 F2)
|
||||
continue
|
||||
# ---- 活行校验 ----
|
||||
if kinds.is_empty():
|
||||
errs.append({"category": "domain", "message": "%s effect_kinds 空(活行;失义件应标 deferred)" % iid})
|
||||
var kind_seen := {}
|
||||
for k in kinds:
|
||||
var kind: String = String(k)
|
||||
if not EFFECT_KINDS.has(kind):
|
||||
errs.append({"category": "enum", "message": "%s effect_kind 出域 %s: %s" % [iid, str(EFFECT_KINDS), kind]})
|
||||
if kind_seen.has(kind):
|
||||
errs.append({"category": "dup_key", "message": "%s effect_kind 重复: %s" % [iid, kind]})
|
||||
kind_seen[kind] = true
|
||||
# 承重护栏①真视窄域:mine_reveal 活行 target 必须 area_3x3(一次最多 3×3,禁全图看雷)
|
||||
if kind_seen.has("mine_reveal") and target != "area_3x3":
|
||||
errs.append({"category": "load_bearing", "message": "%s 真视窄域护栏:mine_reveal 行 target 必须 area_3x3,实得 %s(魔法道具 GDD 结构冻结)" % [iid, target]})
|
||||
# 承重护栏②board_wide 仅恢复推理类:全图域行禁替推理效果
|
||||
if target == "board_wide" and kind_seen.has("mine_reveal"):
|
||||
errs.append({"category": "load_bearing", "message": "%s board_wide 行 effect_kinds 不得含 mine_reveal(全图域仅恢复推理原料类——Alpha 重定后落位)" % iid})
|
||||
# magnitude:仅 ap_grant 行消费(≥1);非 ap_grant 活行恒 0
|
||||
var mag: int = int(row.get("magnitude", 0))
|
||||
if mag < 0 or mag > 999:
|
||||
errs.append({"category": "domain", "message": "%s magnitude 越域 [0,999]: %d" % [iid, mag]})
|
||||
if kind_seen.has("ap_grant") and mag < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s ap_grant 行 magnitude 应 ≥1,实得 %d" % [iid, mag]})
|
||||
if not kind_seen.has("ap_grant") and mag != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s 非 ap_grant 活行 magnitude 应 0,实得 %d" % [iid, mag]})
|
||||
return errs
|
||||
|
||||
|
||||
# 活行词表(非 retired 非 deferred)——投放校验/奖励节点池/F2 覆盖面的唯一取数口径
|
||||
func active_rows() -> Array:
|
||||
var out: Array = []
|
||||
for row in rows:
|
||||
if not bool(row.get("retired", false)) and not bool(row.get("deferred", false)):
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
func row_by_id(item_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if String(row.get("item_id", "")) == item_id:
|
||||
return row
|
||||
return {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dfhjthkhkvsbj
|
||||
@@ -12,7 +12,8 @@ extends Resource
|
||||
|
||||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||||
|
||||
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"] # 九型封闭(格面资源九型批 2026-09-20 八→九,词表 v1.1)——清单外加载拒绝
|
||||
const CELL_TYPES := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp", "aux_item"] # 十型(迭代 7 T1 魔法道具内容批扩值——registry aux_items 词表;aux 行 effect_id = item_id 双表校验键)
|
||||
const AUX_ITEM_TYPE := "aux_item" # aux 行标识(掉落投放行:挖开→战斗结算 grant 路由关卡域库存,不入增益容器)
|
||||
const POLARITIES := ["positive", "negative"]
|
||||
const POOL_TAGS := ["cell", "clear_default"] # cell = 格面资源单一池;clear_<biome> 命名空间预留拒绝
|
||||
const CELL_TYPE_KEYS := ["attack", "defense", "heal", "blast", "weaken", "pierce", "magic", "action", "gp"] # 字典完整性校验轴(类型 × 极性 = 十八格)
|
||||
@@ -43,7 +44,19 @@ func validate() -> Array:
|
||||
errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]})
|
||||
var atk_type: String = String(row.get("attack_type", ""))
|
||||
if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type):
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]})
|
||||
errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]}) # aux 投放行专属校验(迭代 7 T1):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层
|
||||
# 对 aux_items 词表校验——reward_dangling_aux)/无 power 无区间(数值语义不复用,道具效果读 AuxItemTable)
|
||||
if ctype == AUX_ITEM_TYPE:
|
||||
# 负向行 = 雷格极性字典占位(雷安同池摇号下雷格可摇中 aux 身份——负路由零兑付兜底,
|
||||
# effect_id 空/power 0/weight 0 不入池;2026-09-29 T1)
|
||||
if pol == "negative":
|
||||
if String(row.get("effect_id", "")) != "" or int(row.get("power", 0)) != 0 or int(row.get("weight", 0)) != 0:
|
||||
errs.append({"category": "cross_field", "message": "%s aux 负占位行仅允许 空effect_id/0power/0weight" % rid})
|
||||
continue
|
||||
if String(row.get("effect_id", "")) == "":
|
||||
errs.append({"category": "domain", "message": "%s aux 正行 effect_id 空(应=item_id)" % rid})
|
||||
if int(row.get("power", 0)) != 0 or row.has("power_min"):
|
||||
errs.append({"category": "cross_field", "message": "%s aux 正行禁 power/区间数值(道具数值读 AuxItemTable)" % rid})
|
||||
var pool: String = row.get("pool_tag", "")
|
||||
var pool_reserved: bool = pool.begins_with("clear_") and pool != "clear_default"
|
||||
if not POOL_TAGS.has(pool) and not pool_reserved:
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
[gd_resource type="Resource" script_class="AuxItemTable" load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://data/schemas/aux_item_table.gd" id="1"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
rows = [{
|
||||
"item_id": "probe_dust",
|
||||
"display_name": "显影尘",
|
||||
"dimension_tags": ["info", "counter"],
|
||||
"effect_kinds": [],
|
||||
"target": "none",
|
||||
"magnitude": 0,
|
||||
"use_phase": "active_dig",
|
||||
"deferred": true
|
||||
}, {
|
||||
"item_id": "sapper_order",
|
||||
"display_name": "工兵令",
|
||||
"dimension_tags": ["action"],
|
||||
"effect_kinds": ["ap_grant"],
|
||||
"target": "none",
|
||||
"magnitude": 2,
|
||||
"use_phase": "active_dig"
|
||||
}, {
|
||||
"item_id": "loot_compass",
|
||||
"display_name": "探宝罗盘",
|
||||
"dimension_tags": ["plan"],
|
||||
"effect_kinds": ["reward_preview"],
|
||||
"target": "area_3x3",
|
||||
"magnitude": 0,
|
||||
"use_phase": "active_dig",
|
||||
"retired": true
|
||||
}, {
|
||||
"item_id": "nest_pendulum",
|
||||
"display_name": "探巢摆钟",
|
||||
"dimension_tags": ["risk"],
|
||||
"effect_kinds": ["mine_reveal"],
|
||||
"target": "area_3x3",
|
||||
"magnitude": 0,
|
||||
"use_phase": "active_dig"
|
||||
}, {
|
||||
"item_id": "catacomb_scroll",
|
||||
"display_name": "地穴全图卷",
|
||||
"dimension_tags": ["info", "plan"],
|
||||
"effect_kinds": [],
|
||||
"target": "board_wide",
|
||||
"magnitude": 0,
|
||||
"use_phase": "active_dig",
|
||||
"deferred": true
|
||||
}, {
|
||||
"item_id": "blast_kit",
|
||||
"display_name": "爆破工兵包",
|
||||
"dimension_tags": ["action", "risk"],
|
||||
"effect_kinds": ["ap_grant", "mine_reveal"],
|
||||
"target": "area_3x3",
|
||||
"magnitude": 4,
|
||||
"use_phase": "active_dig"
|
||||
}]
|
||||
@@ -200,6 +200,49 @@ rows = [{
|
||||
"weight": 0,
|
||||
"pool_tag": "cell",
|
||||
"tier": 1
|
||||
}, {
|
||||
|
||||
"reward_id": "aux_sapper_order",
|
||||
"cell_type": "aux_item",
|
||||
"polarity": "positive",
|
||||
"effect_id": "sapper_order",
|
||||
"display_name": "工兵令",
|
||||
"power": 0,
|
||||
"weight": 5,
|
||||
"pool_tag": "cell",
|
||||
"tier": 2
|
||||
}, {
|
||||
|
||||
"reward_id": "aux_nest_pendulum",
|
||||
"cell_type": "aux_item",
|
||||
"polarity": "positive",
|
||||
"effect_id": "nest_pendulum",
|
||||
"display_name": "探巢摆钟",
|
||||
"power": 0,
|
||||
"weight": 4,
|
||||
"pool_tag": "cell",
|
||||
"tier": 2
|
||||
}, {
|
||||
|
||||
"reward_id": "aux_blast_kit",
|
||||
"cell_type": "aux_item",
|
||||
"polarity": "positive",
|
||||
"effect_id": "blast_kit",
|
||||
"display_name": "爆破工兵包",
|
||||
"power": 0,
|
||||
"weight": 1,
|
||||
"pool_tag": "cell",
|
||||
"tier": 3
|
||||
}, {
|
||||
"reward_id": "aux_none",
|
||||
"cell_type": "aux_item",
|
||||
"polarity": "negative",
|
||||
"effect_id": "",
|
||||
"display_name": "惊扰的遗落物",
|
||||
"power": 0,
|
||||
"weight": 0,
|
||||
"pool_tag": "cell",
|
||||
"tier": 1
|
||||
}]
|
||||
|
||||
; 格面资源批(2026-09-15 用户拍板——registry reward_table 条目为现行 schema 事实源):
|
||||
|
||||
+147
-2
@@ -67,6 +67,8 @@ var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施
|
||||
var _adventure_gp_view := 0 # GP 冒险钱包呈现侧影子(迭代 6 T7——终局面板 GP 行取数面,
|
||||
# wallet_changed gain/buy 载荷=入账后总额 level_runner.gd:482/539)
|
||||
var _adventure_gp_final := 0 # 收束清零拍暂存的最终合计("clear" 到达时自影子转存)
|
||||
var _aux_use_selection := 0 # 魔法道具选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取)
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——工坊解锁×活行交集,冒险开始组装)
|
||||
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
|
||||
var tune_panel: TunePanel
|
||||
|
||||
@@ -115,6 +117,7 @@ func _build_ui() -> void:
|
||||
settlement_fx = battle_ui.settlement_fx
|
||||
log_view.review_requested.connect(_open_review_from_band)
|
||||
board_zone.action_requested.connect(_on_board_action)
|
||||
info_panel.aux_slot_clicked.connect(_on_aux_slot_clicked) # 迭代 7 T4——两步交互第一步
|
||||
board_zone.stop_pressed.connect(_on_stop_pressed)
|
||||
board_zone.refresh_confirmed.connect(_on_refresh_confirmed)
|
||||
board_zone.legend_toggled.connect(_on_legend_toggled)
|
||||
@@ -147,6 +150,7 @@ func _build_ui() -> void:
|
||||
legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay))
|
||||
review_overlay.closed.connect(_on_modal_closed.bind(review_overlay))
|
||||
node_overlay.continue_pressed.connect(_on_node_continue)
|
||||
# 迭代 7 T5——item_chosen 连接在奖励分支首唤时防重接线(上方)
|
||||
level_screen.route_chosen.connect(_on_route_chosen)
|
||||
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||||
@@ -248,7 +252,7 @@ func _start_adventure() -> void:
|
||||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
# 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签
|
||||
var first_script: bool = not save_manager.is_first_adventure_done()
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script}):
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux_unlocked_set()}):
|
||||
_fatal_screen("冒险启动失败——见输出日志")
|
||||
return
|
||||
|
||||
@@ -282,7 +286,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
log_view.append_bb(Narrator.node_banner(runner, node, t))
|
||||
elif t == "reward":
|
||||
_open_modal(node_overlay)
|
||||
node_overlay.show_reward()
|
||||
node_overlay.item_chosen.connect(_on_reward_item_chosen) if not node_overlay.item_chosen.is_connected(_on_reward_item_chosen) else null
|
||||
node_overlay.show_reward_items(_reward_node_choices())
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
elif t == "rest":
|
||||
var healed: int = int(info.get("healed", 0))
|
||||
@@ -304,6 +310,8 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
# 大奖励兑付(非末关通关恰一次):复用 _inter_overlay(label + 确认按钮,零新增交互范式);
|
||||
# 文案装配归 Narrator
|
||||
func _on_big_reward(reward: Dictionary) -> void:
|
||||
if int(reward.get("aux_cleared", 0)) > 0:
|
||||
board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(reward.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
|
||||
_inter_label.text = Narrator.big_reward_text(reward)
|
||||
_inter_overlay.visible = true
|
||||
_refresh_presentations()
|
||||
@@ -722,6 +730,136 @@ func _on_shop_leave() -> void:
|
||||
runner.continue_after_node()
|
||||
|
||||
|
||||
# ---- 魔法道具 HUD 接线(迭代 7 T4——库存条+两步交互+叠加层呈现;数据源=runner 级库存只读面)----
|
||||
|
||||
# ---- 奖励节点三选一(迭代 7 T5——拍板②丙:奖励屏=小构筑点;「节点奖励=功能道具正式渠道」兑现)----
|
||||
# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集)
|
||||
func _aux_unlocked_set() -> Dictionary:
|
||||
var out := {}
|
||||
if meta == null:
|
||||
return out
|
||||
for row in db.aux_active_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if meta.save.item_unlocked(iid):
|
||||
out[iid] = true
|
||||
return out
|
||||
|
||||
|
||||
|
||||
# 摇取面:活行 ∩ 工坊解锁集(拍板③甲——解锁进投放池同源口径);池 ≤3 全列(当前三件=选一即全列),
|
||||
# 池扩充后加权摇三(确定性摇取:读 seed 流外——呈现侧零 RNG 消费纪律 → 全列前 3 按注册序)。
|
||||
func _reward_node_choices() -> Array:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return []
|
||||
var out: Array = []
|
||||
for row in db.aux_active_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if not _aux_unlocked.has(iid):
|
||||
continue # 未解锁不入奖励池(与掉落投放同门控)
|
||||
out.append({
|
||||
"item_id": iid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"glyph": String(row.get("display_name", "?")).left(1),
|
||||
})
|
||||
if out.size() >= 3:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# 选择兑付:入关卡域库存(来源归因 reward_node)→ 收屏 → 兑付确认回选路(零自动推进同款)
|
||||
func _on_reward_item_chosen(item_id: String) -> void:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return
|
||||
runner.grant_aux_item(item_id, "reward_node")
|
||||
node_overlay.close_reward()
|
||||
_refresh_aux_slots()
|
||||
board_zone.toast.show_toast("获得 %s(入关卡背包)" % String(db.aux_item_row(item_id).get("display_name", item_id)), "info")
|
||||
_on_node_continue()
|
||||
|
||||
|
||||
# 槽点击(第一步):AP 类(target=none)点击即用(无目标);空间类进/出选中态(toggle 取消)。
|
||||
func _on_aux_slot_clicked(slot_handle: int) -> void:
|
||||
if settlement == null or runner == null or runner.aux_inventory == null:
|
||||
return
|
||||
if slot_handle == 0:
|
||||
_aux_use_selection = 0
|
||||
_refresh_aux_slots()
|
||||
return
|
||||
var rec: Dictionary = {}
|
||||
for r in _aux_inventory_projection():
|
||||
if int(r.get("handle", 0)) == slot_handle:
|
||||
rec = r
|
||||
break
|
||||
if rec.is_empty() or bool(rec.get("used", false)):
|
||||
return
|
||||
var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", "")))
|
||||
if String(arow.get("target", "none")) == "none":
|
||||
_attempt_aux_use(slot_handle, Vector2i(-9, -9)) # AP 类:无目标即用
|
||||
return
|
||||
if not bool(rec.get("usable", true)):
|
||||
board_zone.toast.show_toast("当前无可预读的未开格", "warn")
|
||||
return
|
||||
_aux_use_selection = 0 if _aux_use_selection == slot_handle else slot_handle # toggle 取消
|
||||
_refresh_aux_slots()
|
||||
if _aux_use_selection != 0:
|
||||
board_zone.toast.show_toast("%s:点击目标格使用(9 宫窗口),再点道具取消" % String(rec.get("name", "")), "info")
|
||||
|
||||
|
||||
# 使用受理(第二步;core 侧 use_aux 四步编排——拒绝零消耗 toast 可辨认)
|
||||
func _attempt_aux_use(handle: int, cell: Vector2i) -> void:
|
||||
if settlement == null:
|
||||
return
|
||||
var v: Dictionary = settlement.use_aux(handle, cell)
|
||||
if not bool(v.ok):
|
||||
board_zone.toast.show_toast(String(v.get("reason", "无法使用")), "warn")
|
||||
return
|
||||
_aux_use_selection = 0
|
||||
_refresh_aux_slots()
|
||||
_refresh_aux_overlays()
|
||||
var arow: Dictionary = db.aux_item_row(String(v.get("item_id", "")))
|
||||
board_zone.toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info")
|
||||
|
||||
|
||||
# 库存投影(C 区四槽取数面——字形=名首字;usable=板级零信息预判回调接排雷玩法只读面)
|
||||
func _aux_inventory_projection() -> Array:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return []
|
||||
var ms = settlement.minesweeper if settlement != null else null
|
||||
var cb := Callable()
|
||||
if ms != null:
|
||||
cb = func(_c: Vector2i) -> bool:
|
||||
for y in ms.current_board().height:
|
||||
for x in ms.current_board().width:
|
||||
if not ms.is_revealed(Vector2i(x, y)):
|
||||
return true
|
||||
return false
|
||||
var out: Array = []
|
||||
for rec in runner.aux_inventory.query_inventory(cb):
|
||||
var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", "")))
|
||||
var dn := String(arow.get("display_name", ""))
|
||||
out.append({
|
||||
"handle": int(rec.get("handle", 0)),
|
||||
"item_id": String(rec.get("item_id", "")),
|
||||
"glyph": dn.left(1) if dn != "" else "?",
|
||||
"name": dn,
|
||||
"used": bool(rec.get("used", false)),
|
||||
"usable": bool(rec.get("usable", true)),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
func _refresh_aux_slots() -> void:
|
||||
info_panel.set_aux_inventory(_aux_inventory_projection(), _aux_use_selection)
|
||||
|
||||
|
||||
# 叠加层投影注入(use 成功/三清后——棋盘标记与预读徽章重算)
|
||||
func _refresh_aux_overlays() -> void:
|
||||
if runner == null or runner.aux_inventory == null or board_view == null:
|
||||
return
|
||||
var ov: Dictionary = runner.aux_inventory.query_overlays()
|
||||
board_view.set_aux_overlays(ov.get("mine_reveal", []), ov.get("reward_preview", []))
|
||||
|
||||
|
||||
func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void:
|
||||
# wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷
|
||||
logger.log_wallet(event, gp, extra)
|
||||
@@ -742,9 +880,16 @@ func _on_board_action(cell: Vector2i, kind: String) -> void:
|
||||
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
|
||||
if _input_locked():
|
||||
return
|
||||
# 魔法道具选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格;
|
||||
# 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗)
|
||||
if _aux_use_selection != 0 and kind == "dig":
|
||||
_attempt_aux_use(_aux_use_selection, cell)
|
||||
return
|
||||
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}])
|
||||
var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted)
|
||||
logger.log_action(kind, cell, accepted)
|
||||
if runner != null and runner.aux_inventory != null:
|
||||
_refresh_aux_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查)
|
||||
_handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {})
|
||||
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
|
||||
# runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固)
|
||||
|
||||
@@ -82,7 +82,15 @@ var tex_flag: Texture2D = null # ④ 旗
|
||||
|
||||
# 变异角标(迭代 6 T6——程序化,tex_mutation 纹带槽留给变异纹带六型自产批):
|
||||
# "flat_damage" = 未开格伤害型变异,左下角危险红三角(开格有代价的策略纹理标记)
|
||||
var mutation_mark := "":
|
||||
# 叠加层标记(迭代 7 T4——魔法道具板级叠加层的格面呈现;派生信息层零回写):
|
||||
# "mine_reveal" = 真视格(危险红内缩框——「这格是雷」的确定决策标记,插旗照常计费);
|
||||
# "reward_preview" 格不复用本字段——预读徽章走 badge_glyph 注入(board_view refresh 注入 REWARD_BADGE 字形)
|
||||
var aux_mark := "":
|
||||
set(v):
|
||||
if aux_mark != v:
|
||||
aux_mark = v
|
||||
queue_redraw()
|
||||
var mutation_mark := ":":
|
||||
set(v):
|
||||
if mutation_mark != v:
|
||||
mutation_mark = v
|
||||
@@ -175,6 +183,11 @@ func _draw() -> void:
|
||||
draw_texture_rect(tex_mutation, rect, false)
|
||||
# ②' 变异角标(迭代 6 T6):左下角危险红三角(危险红=「开此格有代价」语义位,与雷族同源;
|
||||
# 尺寸随格边比例——灰色可辨辅助=红对砖面明度差,禁喧宾)
|
||||
# ②" 魔法道具叠加层(迭代 7 T4):真视格=危险红内缩框(策略纹理——确定决策输入不喧宾)
|
||||
if aux_mark == "mine_reveal":
|
||||
var inset := edge * 0.14
|
||||
draw_rect(Rect2(inset, inset, edge - inset * 2.0, edge - inset * 2.0),
|
||||
Color(UiKit.RED_MINE, 0.9), false, 2.0)
|
||||
if mutation_mark == "flat_damage":
|
||||
var mw := edge * 0.28
|
||||
var mpts := PackedVector2Array([Vector2(0, edge - mw), Vector2(0, edge), Vector2(mw, edge)])
|
||||
|
||||
@@ -98,6 +98,8 @@ var _minesweeper = null # 排雷玩法 只读查询引用(refresh
|
||||
var _identity_cb := Callable() # 地图战斗资源 身份解析回调(组合根注入只读面)——Vector2i → Dictionary/null
|
||||
var _mask_cb := Callable() # 地形变异 数字遮蔽回调(组合根注入只读面)——Vector2i → bool(true = 该格数字不显示)
|
||||
var _vmark_cb := Callable() # 地形变异 角标回调(迭代 6 T6 只读面)——Vector2i → String("flat_damage"=伤害型变异未开格;""=无)
|
||||
var _aux_reveal := {} # 魔法道具 真视集(迭代 7 T4——Vector2i → true;组合根 set_aux_overlays 注入投影)
|
||||
var _aux_preview := {} # 魔法道具 预读集(同上——未开格徽章呈现挂点)
|
||||
var _diggable := {} # 可挖域(8 向相邻未开格)——呼吸微光作用集
|
||||
var _breath_t := 0.0
|
||||
var _contact_unit: Node2D = null # 接触带承载单元(=英雄格 CellUnit;rebuild 随清场复位)
|
||||
@@ -193,6 +195,16 @@ func set_variant_mask_resolver(cb: Callable) -> void:
|
||||
func set_variant_mark_resolver(cb: Callable) -> void:
|
||||
_vmark_cb = cb
|
||||
|
||||
# 魔法道具叠加层投影注入(迭代 7 T4——组合根在 use 成功/三清后调;refresh 消费)
|
||||
func set_aux_overlays(reveal_cells: Array, preview_cells: Array) -> void:
|
||||
_aux_reveal.clear()
|
||||
_aux_preview.clear()
|
||||
for c in reveal_cells:
|
||||
_aux_reveal[Vector2i(c)] = true
|
||||
for c in preview_cells:
|
||||
_aux_preview[Vector2i(c)] = true
|
||||
refresh()
|
||||
|
||||
|
||||
# SubViewport 主题/字体显式注入挂点(Phase B 两舞台复用——SubViewport 不自动继承主场景 Control 主题;
|
||||
# 本项目无自定义主题,缺省 = 引擎回退字体,与旧 Button 渲染同源)
|
||||
@@ -722,6 +734,16 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
|
||||
# 地面投影:北邻墙格在本已开格北缘投渐隐暗带(ITB 同款高度线索——带宽按格边比例常量)
|
||||
if north_in and not _minesweeper.is_revealed(north_n):
|
||||
u.proj_band = u.edge * WALL_PROJECTION_FRAC
|
||||
# ②( 魔法道具叠加层(迭代 7 T4):真视格=红内缩框;预读格=奖励身份徽章(identity_resolver 既有
|
||||
# 只读面取字形——零数据复制,badge 位复用);已开格标记自然退位(refresh 逐拍重算)
|
||||
if not revealed:
|
||||
u.aux_mark = "mine_reveal" if _aux_reveal.has(c) else ""
|
||||
if _aux_preview.has(c) and glyph == "":
|
||||
var pid = _identity_cb.call(c) if _identity_cb.is_valid() else null
|
||||
if pid != null and REWARD_BADGE.has(String(pid.get("cell_type", ""))):
|
||||
glyph = String(REWARD_BADGE[String(pid.get("cell_type", ""))])
|
||||
digit_col = COL_GOLD
|
||||
u.badge_glyph = glyph
|
||||
# ②' 变异角标(迭代 6 T6):未开格带伤害型变异 → 危险红角标(已开=威胁已兑现标记退位);
|
||||
# 回调缺位 = 无标记(程序化角标,tex_mutation 纹带槽留给变异纹带自产批)
|
||||
if not revealed and _vmark_cb.is_valid():
|
||||
|
||||
@@ -138,8 +138,20 @@ func refresh_camp(data: Dictionary) -> void:
|
||||
var prog := int(row.get("progress", 0))
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
|
||||
card.status_l.text = "已解锁" if unlocked else "未解锁"
|
||||
card.status_l.add_theme_color_override("font_color", UiKit.GOLD if unlocked else UiKit.WARM_FAINT)
|
||||
# 迭代 7 T5——失义件(显影尘/地穴全图卷 Alpha 重定)与退役件(探宝罗盘 2026-09-29 拍板①乙)的呈现口径
|
||||
var st_text := "未解锁"
|
||||
var st_col: Color = UiKit.WARM_FAINT
|
||||
if bool(row.get("retired", false)):
|
||||
st_text = "已退役"
|
||||
st_col = UiKit.WARM_FAINT
|
||||
elif bool(row.get("deferred", false)) and unlocked:
|
||||
st_text = "已解锁 · 能力封存中"
|
||||
st_col = UiKit.GOLD
|
||||
elif unlocked:
|
||||
st_text = "已解锁"
|
||||
st_col = UiKit.GOLD
|
||||
card.status_l.text = st_text
|
||||
card.status_l.add_theme_color_override("font_color", st_col)
|
||||
|
||||
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
|
||||
@@ -14,7 +14,11 @@ const COL_INK := UiKit.INK
|
||||
|
||||
var _atk_label: Label
|
||||
var _armor_label: Label
|
||||
var item_slots: HBoxContainer # 魔法道具槽(预留——4 空槽占位;数量/交互形态待魔法道具批定稿)
|
||||
signal aux_slot_clicked(handle: int) # 迭代 7 T4——槽点击(组合根消费:AP 类即用/空间类进选中态)
|
||||
var item_slots: HBoxContainer # 魔法道具槽(迭代 7 T4 接线——库存条常读+两步交互第一步)
|
||||
var _slot_panels: Array = [] # 槽 PanelContainer 引用(选中高亮/置灰重刷)
|
||||
var _selected_handle := 0 # 选中态(0=无;组合根回写维持单源)
|
||||
var _slot_handles: Array = [] # 槽位→handle 映射(set_aux_inventory 刷新)
|
||||
var _bonus_section: VBoxContainer # 本关强化(预留——空态隐藏,接线归大奖励/锻造呈现批)
|
||||
var _bonus_list: VBoxContainer
|
||||
|
||||
@@ -54,8 +58,13 @@ func _init() -> void:
|
||||
slot.custom_minimum_size = Vector2(48, 48)
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4)))
|
||||
slot.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
var idx := i
|
||||
slot.gui_input.connect(func(ev: InputEvent):
|
||||
if ev is InputEventMouseButton and ev.pressed and ev.button_index == MOUSE_BUTTON_LEFT:
|
||||
# 空槽/越界槽点击=取消选中(handle 0);有效槽发其 handle(组合根判 AP 即用/进选中态)
|
||||
aux_slot_clicked.emit(_slot_handles[idx] if idx < _slot_handles.size() else 0))
|
||||
item_slots.add_child(slot)
|
||||
_slot_panels.append(slot)
|
||||
|
||||
# 本关强化(预留):锻造/大奖励/关卡奖励节点的属性提升——中循环数据因改变战斗数学准入;
|
||||
# 与战斗内状态效果(B 区站桩,随战斗消散)分区,防「战斗结束被清」误读;空态整节隐藏
|
||||
@@ -110,3 +119,40 @@ func _data_line() -> Label:
|
||||
l.clip_text = true
|
||||
l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
|
||||
return l
|
||||
|
||||
# ---- 魔法道具库存条(迭代 7 T4——C 区四槽常读+两步交互第一步)----
|
||||
# 投影行 = {handle, glyph, name, used, usable}(组合根组装:字形=道具名首字;数据源 runner 级库存只读面)。
|
||||
# 空槽=虚位;used=暗态「已用」保留占位(Used 不可逆——受理序展示语义);不可用=置灰(板级零信息);
|
||||
# selected=金框高亮(两步交互第一步的可见态)。tooltip=全名+维度语义。
|
||||
func set_aux_inventory(items: Array, selected_handle: int) -> void:
|
||||
_selected_handle = selected_handle
|
||||
_slot_handles.clear()
|
||||
for i in 4:
|
||||
var slot: PanelContainer = _slot_panels[i]
|
||||
for c in slot.get_children():
|
||||
c.queue_free()
|
||||
if i >= items.size():
|
||||
_slot_handles.append(0)
|
||||
slot.tooltip_text = "魔法道具槽(空)"
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4)))
|
||||
continue
|
||||
var rec: Dictionary = items[i]
|
||||
_slot_handles.append(int(rec.get("handle", 0)))
|
||||
var used := bool(rec.get("used", false))
|
||||
var usable := bool(rec.get("usable", true))
|
||||
var selected := selected_handle != 0 and int(rec.get("handle", 0)) == selected_handle
|
||||
var glyph := UiKit.label(String(rec.get("glyph", "?")), 20, true)
|
||||
glyph.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
glyph.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||
if used:
|
||||
glyph.add_theme_color_override("font_color", UiKit.STONE_FAINT)
|
||||
elif not usable:
|
||||
glyph.add_theme_color_override("font_color", UiKit.STONE)
|
||||
slot.add_child(glyph)
|
||||
var border_col: Color = UiKit.GOLD if selected else UiKit.PANEL_BORDER
|
||||
slot.add_theme_stylebox_override("panel",
|
||||
UiKit.panel_flat(UiKit.PANEL_DATA, border_col, 0, 1 if not selected else 2, Vector4(4, 4, 4, 4)))
|
||||
slot.tooltip_text = "%s%s" % [String(rec.get("name", "")), "(已用)" if used else ("(不可用——当前无有效目标)" if not usable else "")]
|
||||
@@ -6,9 +6,11 @@ class_name NodeOverlay
|
||||
extends Control
|
||||
|
||||
signal continue_pressed
|
||||
signal item_chosen(item_id: String) # 迭代 7 T5——奖励节点三选一兑付(拍板②丙:奖励屏=小构筑点)
|
||||
|
||||
var _label: Label
|
||||
var _btn: Button
|
||||
var _choice_box: HBoxContainer # 道具三选一按钮区(show_reward_items 时呈现)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -28,6 +30,10 @@ func _init() -> void:
|
||||
box.add_child(_label)
|
||||
_btn = UiKit.button("继续")
|
||||
_btn.pressed.connect(_emit_continue_pressed)
|
||||
_choice_box = HBoxContainer.new()
|
||||
_choice_box.add_theme_constant_override("separation", 12)
|
||||
_choice_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(_choice_box)
|
||||
box.add_child(_btn)
|
||||
visible = false
|
||||
|
||||
@@ -46,6 +52,12 @@ func show_rest(healed: int, hp_after: int, max_hp: int) -> void:
|
||||
visible = true
|
||||
|
||||
|
||||
# 选择完成收屏(组合根在 item_chosen 处理毕调——入库存+确认后回选路由同拍)
|
||||
func close_reward() -> void:
|
||||
_choice_box.visible = false
|
||||
_btn.visible = true
|
||||
visible = false
|
||||
|
||||
# 探针复用:以真实按钮路径触发继续
|
||||
func press_continue() -> void:
|
||||
_btn.pressed.emit()
|
||||
|
||||
+12
-6
@@ -2009,12 +2009,14 @@ func _smoke_negative_dictionary() -> void:
|
||||
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
|
||||
# 数值压缩批(2026-09-17):heal 权重 15→3 后单板五型齐备不再必然(无 heal 板概率 ≈1/3)——
|
||||
# 改多板扫描,取第一块五型齐备的板做行为断言。
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
var settlement = null
|
||||
var board = null
|
||||
var by_type := {} # cell_type → 首个非封锁格(query_identity 雷安同池——雷格可查)
|
||||
var rows: Array = []
|
||||
for seed_i in range(6):
|
||||
# 每板独立 rng 实例(迭代 7 T1 修正——复用实例重播种触发 set_master_seed 的 _counter==0 守卫;
|
||||
# 原版第一板即 break 掩盖了该违例,aux 供给挤占后多板扫描暴露)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260919 + seed_i)
|
||||
settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
rows = []
|
||||
@@ -2032,9 +2034,13 @@ func _smoke_negative_dictionary() -> void:
|
||||
if r0.has(c) or by_type.size() == 5:
|
||||
continue
|
||||
var ident = settlement.query_reward_identity(c)
|
||||
if ident != null and not by_type.has(String(ident.cell_type)):
|
||||
# aux 身份不入五型轴(魔法道具内容批 2026-09-29——五型齐备=格面资源历史断言轴,
|
||||
# aux 供给挤占不改变本断言语义;扫描排除 aux 身份格)
|
||||
if ident != null and String(ident.cell_type) != RewardTable.AUX_ITEM_TYPE and not by_type.has(String(ident.cell_type)):
|
||||
by_type[String(ident.cell_type)] = c
|
||||
if by_type.size() == 5:
|
||||
# break 条件=percent_map 三型齐备(迭代 7 T1——aux 供给挤占下 weaken/defense 收集时机漂移,
|
||||
# 直验可达性优先;五型多样性 check 保留不动)
|
||||
if by_type.has("attack") and by_type.has("defense") and by_type.has("weaken"):
|
||||
break
|
||||
check(by_type.size() == 5, "扫描 6 板内出现五型齐备板(%s)" % str(by_type.keys()))
|
||||
# attack/defense/weaken:负向字典行走 apply_effect(off_guard 2026-09-18 换装 ac 轴——兑付形态不变)
|
||||
@@ -3226,8 +3232,8 @@ func _all_entry_boss_paths(nodes: Array) -> Array:
|
||||
|
||||
func _smoke_nine_types_gp_stream() -> void:
|
||||
print("--- 格面资源九型 + gp_drop 流(奖励字典 18 格 + 黄金向量)---")
|
||||
check(RewardTable.CELL_TYPES.size() == 9 and RewardTable.CELL_TYPES.has("gp"),
|
||||
"格面资源九型封闭(第九型 gp——九型批 2026-09-20,词表 v1.1)")
|
||||
check(RewardTable.CELL_TYPES.size() == 10 and RewardTable.CELL_TYPES.has("gp") and RewardTable.CELL_TYPES.has("aux_item"),
|
||||
"格面资源十型封闭(第九型 gp 九型批 2026-09-20 / 第十型 aux_item 魔法道具内容批 2026-09-29——词表 v1.2)")
|
||||
var cell_rows := 0
|
||||
var pos_weight := 0
|
||||
var gp_pos := -1
|
||||
@@ -3241,7 +3247,7 @@ func _smoke_nine_types_gp_stream() -> void:
|
||||
gp_pos = int(row.weight)
|
||||
if String(row.get("reward_id", "")) == "gp_lose":
|
||||
gp_neg = int(row.weight)
|
||||
check(cell_rows == 18, "奖励字典 18 格(九型 × 正负——清图槽 clear_default 独立批尾不计)")
|
||||
check(cell_rows == 22, "奖励字典 22 行(九型×正负 18 格 + aux 正三投放/负一占位——魔法道具内容批 2026-09-29;清图槽独立批尾不计)")
|
||||
# 正向权重结构校验(配置推导,2026-09-23 重构):正向 cell 行权重 ≥1(必在池)、负向行权重恒 0(不入池)、
|
||||
# 合计即配置自身的和(改表自动跟随,不再硬编码合计值)。
|
||||
var pos_all_in_pool := true
|
||||
|
||||
@@ -29,7 +29,10 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/example_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/data_config/config_db_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/data_config/config_db_fatal_oob_test.gd", "expect": 1},
|
||||
{"path": "res://tests/unit/data_config/aux_target_violation_test.gd", "expect": 1}, # 迭代 7 T1——真视窄域护栏
|
||||
{"path": "res://tests/unit/data_config/reward_dangling_aux_test.gd", "expect": 1}, # 迭代 7 T1——aux 双表校验
|
||||
{"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/core/aux_items_test.gd", "expect": 0}, # 迭代 7 T7——魔法道具结构冻结断言面
|
||||
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/contracts/system_contracts_test.gd", "expect": 0},
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 魔法道具 AuxItems 类单测(迭代 7 T7——结构冻结断言面;设计权威 魔法道具.md + registry aux_items)。
|
||||
# 覆盖:授予归因 / Used 不可逆 / 真视恒可用 / 预读点击级零信息拒绝 / 3×3 边缘窗口∩板 /
|
||||
# 叠加层三清(retire_overlays 不动库存)/ 关卡末 reset / 零随机(同操作序列重放两遍投影逐位)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/aux_items_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const AuxItemsScript := preload("res://core/aux_items.gd")
|
||||
|
||||
var _problems: Array = []
|
||||
|
||||
|
||||
func check(cond: bool, msg: String) -> void:
|
||||
if not cond:
|
||||
_problems.append(msg)
|
||||
|
||||
|
||||
class StubTable:
|
||||
var rows := {}
|
||||
func row_by_id(id: String) -> Dictionary:
|
||||
return rows.get(id, {})
|
||||
func active_rows() -> Array:
|
||||
return []
|
||||
|
||||
|
||||
class StubDB:
|
||||
var aux_items: StubTable
|
||||
|
||||
|
||||
func _stub_db() -> StubDB:
|
||||
var t := StubTable.new()
|
||||
t.rows = {
|
||||
"sapper_order": {"item_id": "sapper_order", "display_name": "工兵令", "dimension_tags": ["action"],
|
||||
"effect_kinds": ["ap_grant"], "target": "none", "magnitude": 2, "use_phase": "active_dig"},
|
||||
"nest_pendulum": {"item_id": "nest_pendulum", "display_name": "探巢摆钟", "dimension_tags": ["risk"],
|
||||
"effect_kinds": ["mine_reveal"], "target": "area_3x3", "magnitude": 0, "use_phase": "active_dig"},
|
||||
"loot_compass": {"item_id": "loot_compass", "display_name": "探宝罗盘", "dimension_tags": ["plan"],
|
||||
"effect_kinds": ["reward_preview"], "target": "area_3x3", "magnitude": 0, "use_phase": "active_dig"},
|
||||
}
|
||||
var db := StubDB.new()
|
||||
db.aux_items = t
|
||||
return db
|
||||
|
||||
|
||||
func _new_inv() -> Object:
|
||||
return AuxItemsScript.new(_stub_db())
|
||||
|
||||
|
||||
func _unrevealed(_c: Vector2i) -> bool:
|
||||
return true # 测试板全未开(预读恒有信息——点击级零信息另用假回调测)
|
||||
|
||||
|
||||
func _all_revealed(_c: Vector2i) -> bool:
|
||||
return false # 预读点击级零信息(窗口全已开)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# ① 授予归因+受理序+Used 不可逆
|
||||
var inv = _new_inv()
|
||||
var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2)
|
||||
var r2: Dictionary = inv.add_item("nest_pendulum", "flag_hit", Vector2i(5, 6), 3)
|
||||
check(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)")
|
||||
check(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2,
|
||||
"授予来源可归因(source/cell/round)")
|
||||
var inv_proj: Array = inv.query_inventory(Callable())
|
||||
check(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用")
|
||||
check(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执")
|
||||
check(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)")
|
||||
check(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)")
|
||||
|
||||
# ② 真视恒可用+边缘窗口∩板(3×3 中心贴角——角 4 格)
|
||||
var v_ok: Dictionary = inv.validate_use(int(r2.handle), Vector2i(0, 0), Vector2i(10, 10), Callable())
|
||||
check(bool(v_ok.ok) and v_ok.resolved.size() == 4,
|
||||
"真视边缘窗口=窗口∩板(角 4 格——护栏只缩不扩)")
|
||||
# 预读点击级零信息:中心板内但回调恒 false(全已开)→拒绝零消耗
|
||||
var inv2 = _new_inv()
|
||||
var rp: Dictionary = inv2.add_item("loot_compass", "safe_cell", Vector2i(1, 1), 1)
|
||||
var v_zero: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(5, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1,
|
||||
"预读点击级零信息拒绝(不消耗——库存不变)")
|
||||
# 目标越板
|
||||
var v_oob: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(12, 5), Vector2i(10, 10), Callable())
|
||||
check(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝")
|
||||
|
||||
# ③ 叠加层+三清(retire 不动库存;reset 全清)
|
||||
inv2.apply_overlay("mine_reveal", v_ok.resolved)
|
||||
inv2.apply_overlay("reward_preview", [Vector2i(5, 5)])
|
||||
var ov: Dictionary = inv2.query_overlays()
|
||||
check(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)")
|
||||
inv2.retire_overlays()
|
||||
check(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1,
|
||||
"换图三清:叠加层消亡+库存不动(关卡域)")
|
||||
inv2.reset()
|
||||
check(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)")
|
||||
|
||||
# ④ 零随机重放:同操作序列两遍,投影逐位相同
|
||||
var replay_a: Array = []
|
||||
var replay_b: Array = []
|
||||
for rep in 2:
|
||||
var inv3 = _new_inv()
|
||||
var h1: Dictionary = inv3.add_item("sapper_order", "safe_cell", Vector2i(2, 3), 1)
|
||||
inv3.validate_use(int(h1.handle), Vector2i(4, 4), Vector2i(10, 10), Callable())
|
||||
inv3.apply_overlay("mine_reveal", [Vector2i(4, 4)])
|
||||
inv3.consume(int(h1.handle))
|
||||
if rep == 0:
|
||||
replay_a = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
else:
|
||||
replay_b = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()]
|
||||
check(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同")
|
||||
|
||||
# ---- 汇总 ----
|
||||
if _problems.is_empty():
|
||||
print("[aux_items_test] PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过")
|
||||
quit(0)
|
||||
else:
|
||||
for p in _problems:
|
||||
push_error("[aux_items_test] FAIL — " + p)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqx67xrrbvp82
|
||||
@@ -0,0 +1,76 @@
|
||||
# 致命类用例(独立进程,期望退出码 1)
|
||||
# aux_target_violation——真视窄域护栏(魔法道具 GDD 结构冻结,迭代 7 T1):mine_reveal 行 target
|
||||
# 越域至 board_wide 时,AuxItemTable.validate 必须报 load_bearing 错、load_tables 整体拒绝。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/data_config/aux_target_violation_test.gd
|
||||
# 退出码协议:1 = 校验正确拒绝(预期);2 = 违规表被放行(回归信号)。
|
||||
# 时序注:纯加载期校验——不触运行期 fatal 通道,断言 load_tables 返回 false + 诊断在案即 quit(1)。
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_target_violation"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
var err := dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f)
|
||||
if err != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/aux_items.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return false
|
||||
var s := f.get_as_text()
|
||||
f.close()
|
||||
# 探巢摆钟(mine_reveal 活行)target area_3x3 → board_wide:双护栏同时命中
|
||||
var bad := s.replace('"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\r\n"display_name": "探巢摆钟",')
|
||||
if bad == s:
|
||||
# 换行形态回退(LF)
|
||||
bad = s.replace('"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",', '"item_id": "nest_pendulum",\n"display_name": "探巢摆钟",')
|
||||
# 直接改 target:摆钟行是全表唯一的 mine_reveal 单维行——把它的 area_3x3 改 board_wide
|
||||
# (罗盘 retired 行同值 area_3x3 但 retired 免校验——替换须定位摆钟块内)
|
||||
var blocks := s.split("}, {")
|
||||
var hit := false
|
||||
for i in blocks.size():
|
||||
if '"item_id": "nest_pendulum"' in blocks[i]:
|
||||
blocks[i] = blocks[i].replace('"target": "area_3x3"', '"target": "board_wide"')
|
||||
hit = true
|
||||
s = "}, {".join(blocks)
|
||||
var w := FileAccess.open(path, FileAccess.WRITE)
|
||||
if w == null or not hit:
|
||||
return false
|
||||
w.store_string(s)
|
||||
w.close()
|
||||
return true
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
push_error("[aux_target_violation_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
var db = ConfigDBScript.new()
|
||||
var ok: bool = db.load_tables(BAD_DIR, false)
|
||||
if ok:
|
||||
push_error("[aux_target_violation_test] 违规表被放行——真视窄域护栏失守(回归)")
|
||||
quit(2)
|
||||
return
|
||||
if db.diagnostics_count("error") >= 1:
|
||||
print("[aux_target_violation_test] 护栏触发(load_bearing 诊断在案,加载被拒)")
|
||||
quit(1)
|
||||
return
|
||||
push_error("[aux_target_violation_test] 加载被拒但无 load_bearing 诊断——护栏通道静默化(回归)")
|
||||
quit(2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c2ufdoduixynq
|
||||
@@ -0,0 +1,69 @@
|
||||
# 致命类用例(独立进程,期望退出码 1)
|
||||
# reward_dangling_aux——RewardTable aux 投放行双表校验(迭代 7 T1):aux 行 effect_id 引用 retired
|
||||
# 道具(loot_compass——2026-09-29 拍板①乙退役)时,config_db 跨表校验必须报 reward_dangling_aux、
|
||||
# load_tables 整体拒绝(退役件/失义件不得出现在掉落投放面)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/data_config/reward_dangling_aux_test.gd
|
||||
# 退出码协议:1 = 校验正确拒绝(预期);2 = 违规表被放行(回归信号)。
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_DIR := "user://aux_fixture_dangling_aux"
|
||||
|
||||
|
||||
func _copy_tables() -> bool:
|
||||
DirAccess.make_dir_recursive_absolute(BAD_DIR)
|
||||
var dir := DirAccess.open(SRC_DIR)
|
||||
if dir == null:
|
||||
return false
|
||||
dir.list_dir_begin()
|
||||
var f := dir.get_next()
|
||||
while f != "":
|
||||
if not dir.current_is_dir():
|
||||
if dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) != OK:
|
||||
return false
|
||||
f = dir.get_next()
|
||||
return true
|
||||
|
||||
|
||||
func _inject_violation() -> bool:
|
||||
var path := BAD_DIR + "/reward.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
return false
|
||||
var s := f.get_as_text()
|
||||
f.close()
|
||||
# 工兵令投放行 → 引用已退役的 loot_compass:双表校验须拦(retired 不入投放面)
|
||||
var blocks: PackedStringArray = s.split("}, {")
|
||||
var hit := false
|
||||
for i in blocks.size():
|
||||
if '"reward_id": "aux_sapper_order"' in blocks[i]:
|
||||
blocks[i] = blocks[i].replace('"effect_id": "sapper_order"', '"effect_id": "loot_compass"')
|
||||
hit = true
|
||||
s = "}, {".join(blocks)
|
||||
var w := FileAccess.open(path, FileAccess.WRITE)
|
||||
if w == null or not hit:
|
||||
return false
|
||||
w.store_string(s)
|
||||
w.close()
|
||||
return true
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _copy_tables() or not _inject_violation():
|
||||
push_error("[reward_dangling_aux_test] 夹具目录准备失败")
|
||||
quit(2)
|
||||
return
|
||||
var db = ConfigDBScript.new()
|
||||
var ok: bool = db.load_tables(BAD_DIR, false)
|
||||
if ok:
|
||||
push_error("[reward_dangling_aux_test] 违规表被放行——aux 双表校验失守(回归)")
|
||||
quit(2)
|
||||
return
|
||||
if db.diagnostics_count("error") >= 1:
|
||||
print("[reward_dangling_aux_test] 双表校验触发(reward_dangling_aux 诊断在案,加载被拒)")
|
||||
quit(1)
|
||||
return
|
||||
push_error("[reward_dangling_aux_test] 加载被拒但无 reward_dangling_aux 诊断——校验通道静默化(回归)")
|
||||
quit(2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0hyi6tlsne3y
|
||||
Reference in new issue
Block a user