- T1 表与schema:attunement_growth.tres 谓词表(m1/m2/m3+Σgrant≤slot_cap 校验+slots() 求值口径)+道具表扩列(form/charges/slot_cost/unlock_requires/effective_rounds/charge_n+effect_kinds 九kind+KEY_WHITELIST 零裸flat护栏)+七同调行 - T2 save v4(boss_kill_count 收束入账+attunement_equipped+attune_refresh_level+断点 attunement_charges+迁移链 v3→v4) - T3 Attunement 求值服务(纯函数零 await 零 RNG) - T4 营地装配覆盖层(工坊扩容程序占位+Y 升级行 attune_refresh_upgrade 行族) - T5 被动六件接线+装配注入链+刷新 X/Y 双源(BonusPickOverlay 刷新钮,autoplay 默认不刷) - T6 掘进蓄能全链+摆钟能力面(骑库存面差额注入+断点充能键);炸格 P5 甲影响评估破面大→用户裁拆后批,blast_kit deferred 留位 - T7 退役面五件(BONUS_IDS 除名 aux_item+transitional_sync_slots 过渡键退役=迭代10欠账③清偿+授予路/四容量闸拆除+nest_pendulum 换装 active_charged) - T8 冒烟六新断言(645/0)+MC attune 开关+贪心画像分层 off/on 各 N=60 双策略(on 画像功率显著带平移归调参批;off 新锚=对拍正主留盘主区 sim-data,81000 降批入口基准) - T9 连带修订九件(魔法道具定位重写/元进度v1.6/地图战斗资源v0.16/行动经济v0.7/05不做v1.3/关卡节点地图v1.4/node-reward v2.2/registry 三处/06 收口+炸格拆后批新条) - T10 重闸全绿:冒烟 645/0+熵增 PASS+doc 三闸零新增+MC off 复跑同种子逐位一致 PASS;出口人闸后移迭代 12(用户 2026-10-03 令)
275 lines
11 KiB
GDScript
275 lines
11 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# Validation Question: 每个非封锁格是否都携带可兑的格面资源身份,且同种子同操作逐位回放
|
||
# Date: 2026-09-10(2026-09-15 格面资源批结构性重写:库存/供给轴下线,格面资源身份面纯化)
|
||
# 地图战斗资源:格面资源的身份层——install 期身份预分配(roll_board 恰一次、消费 reward_roll 封闭流
|
||
# 恰一 scope 批:格批 + 清图批尾)· 授予期零随机 · Ready 后只读、无重掷 · 换图映射消亡。
|
||
# 安全格与雷格一视同仁(同一类型池、同一权重分布——雷安同池);封锁格不入摇号清单。
|
||
# 极性不在资源内:「动作 × 格型」矩阵的字典定义归 GDD、路由执行归 战斗结算 授予段
|
||
# (本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面)。
|
||
# 切片登记:取号约定 = randi_range(0, W-1)(实现期 ADR 事实,切片内恒定);
|
||
# 区间行 power roll 在身份批内追加取号(伤害量值区间批——快照 power = roll 值,授予期零随机不变)。
|
||
# 实现 GDD:design/gdd/systems/战斗循环/地图战斗资源.md(v0.4 格面资源批——registry 追溯锚)
|
||
class_name RewardEconomy
|
||
extends RefCounted
|
||
|
||
var db: ConfigDb
|
||
var rng: RngServiceCore
|
||
var _phase := "unrolled" # unrolled → ready → retired
|
||
var _eligible: Array[Dictionary] = [] # [{cell}] 行主序规范序
|
||
var _mapping := {} # Vector2i → reward_id(正向行身份——格面资源类型由 cell_type 承载)
|
||
var _power_roll := {} # Vector2i → 区间行 roll 值(身份批内追加取号)
|
||
var _clear_power := -1 # 清图槽区间行 roll 值(-1 = 定值行)
|
||
var _granted := {} # Vector2i → 快照(重复受理幂等返回)
|
||
var _clear_identity := ""
|
||
var _clear_granted := false
|
||
var _clear_snapshot := {}
|
||
|
||
|
||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||
db = db_ref
|
||
rng = rng_ref
|
||
|
||
|
||
# ---- install 期 roll(恰一次;Ready 相位再调拒绝)----
|
||
|
||
# eligible = 全部非封锁格(安全格 + 雷格,战斗结算 install 编排供给;封锁格不入清单)
|
||
func roll_board(eligible: Array) -> bool:
|
||
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
|
||
if _phase == "ready":
|
||
push_warning("[地图战斗资源] roll_board 相位守卫拒绝(E5——double-roll 编排违约显式暴露)")
|
||
return false
|
||
if eligible.is_empty():
|
||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
|
||
return false
|
||
# 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key——2026-10-02 修复批)
|
||
var seen := {}
|
||
var list: Array[Dictionary] = []
|
||
for e in eligible:
|
||
var c: Vector2i = e.cell
|
||
if seen.has(c):
|
||
push_warning("[地图战斗资源] 资格清单重复格已去重: %s" % str(c))
|
||
continue
|
||
seen[c] = true
|
||
list.append({"cell": c})
|
||
list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell))
|
||
_eligible = list
|
||
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
|
||
_power_roll.clear()
|
||
_clear_power = -1
|
||
_clear_granted = false
|
||
_clear_snapshot = {}
|
||
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
|
||
var cell_rows: Array = _biased_cell_rows() # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰)
|
||
var clear_rows: Array = _pool_rows("clear_default")
|
||
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
|
||
var draft := {}
|
||
for e in list:
|
||
draft[e.cell] = weighted_pick(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1))
|
||
# ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份
|
||
var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1))
|
||
# ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号)
|
||
for cell in draft.keys():
|
||
var row: Dictionary = _row_by_id(draft[cell])
|
||
if row.has("power_min"):
|
||
_power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max))
|
||
var clear_row: Dictionary = _row_by_id(clear_rid)
|
||
if clear_row.has("power_min"):
|
||
_clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max))
|
||
_mapping = draft
|
||
_clear_identity = clear_rid
|
||
_phase = "ready"
|
||
return true
|
||
|
||
|
||
# ---- 查询面(Ready 只读;未开格恒可查——魔法道具 桥接结构前提)----
|
||
|
||
# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段)
|
||
func phase() -> String:
|
||
return _phase
|
||
|
||
|
||
# 静默映射查询(批量工具用——零 E19 诊断;语义 = query_identity 非 null 的布尔投影)
|
||
func is_mapped(cell: Vector2i) -> bool:
|
||
return _phase == "ready" and _mapping.has(cell)
|
||
|
||
|
||
func query_identity(cell: Vector2i):
|
||
if _phase != "ready":
|
||
_diag("query_identity 非 Ready 相位")
|
||
return null
|
||
var rid: String = _mapping.get(cell, "")
|
||
if rid == "":
|
||
_diag("query_identity 清单外坐标(E19——未分配非错误态;封锁格不入清单)")
|
||
return null
|
||
var row: Dictionary = _row_by_id(rid)
|
||
return {"reward_id": rid, "cell_type": String(row.cell_type), "tier": int(row.tier)}
|
||
|
||
|
||
# 身份完整快照(轮内刷新批次隔离用——facts 缓冲期捕获,授予期 mapping 可能已换批):
|
||
# 返回与 grant 相同结构的快照(cell_type/display_name/power/effect_id/stacks 齐备),
|
||
# 供 战斗结算 冻结授予直通
|
||
func query_full_snapshot(cell: Vector2i):
|
||
if _phase != "ready":
|
||
_diag("query_full_snapshot 非 Ready 相位")
|
||
return null
|
||
var rid: String = _mapping.get(cell, "")
|
||
if rid == "":
|
||
return null
|
||
return _snapshot(_row_by_id(rid), int(_power_roll.get(cell, -1)))
|
||
|
||
|
||
# ---- 受理面(战斗结算 编排期调用;E6 收口 2026-09-13:失败统一返回 null)----
|
||
|
||
func grant(cell: Vector2i):
|
||
if _phase != "ready":
|
||
_diag("grant 非 Ready 相位(E4/E7)")
|
||
return null
|
||
if not _mapping.has(cell):
|
||
_diag("grant 清单外坐标(E19)")
|
||
return null
|
||
if _granted.has(cell):
|
||
_diag("grant 同格重复受理(E18 幂等兜底——受理恰一次归 战斗结算)")
|
||
return _granted[cell]
|
||
var snap := _snapshot(_row_by_id(_mapping[cell]), int(_power_roll.get(cell, -1)))
|
||
_granted[cell] = snap
|
||
return snap
|
||
|
||
|
||
func grant_clear():
|
||
if _phase != "ready":
|
||
_diag("grant_clear 非 Ready 相位")
|
||
return null
|
||
if _clear_granted:
|
||
_diag("grant_clear 同批次重调(E18 幂等兜底)")
|
||
return _clear_snapshot
|
||
var row: Dictionary = _row_by_id(_clear_identity)
|
||
_clear_snapshot = _snapshot(row, _clear_power)
|
||
_clear_granted = true
|
||
return _clear_snapshot
|
||
|
||
|
||
func retire_mapping() -> bool:
|
||
if _phase != "ready":
|
||
_diag("retire_mapping 非 Ready 相位(幂等受理族)")
|
||
return false
|
||
_phase = "retired"
|
||
_mapping.clear()
|
||
_power_roll.clear()
|
||
_clear_power = -1
|
||
_clear_identity = ""
|
||
return true
|
||
|
||
|
||
# ---- 极性路由字典面(战斗结算 授予段消费):(类型, 极性)→ 格面池字典行(拷贝出口)----
|
||
|
||
func polarity_row(cell_type: String, polarity: String) -> Dictionary:
|
||
for row in db.reward.rows:
|
||
if String(row.get("pool_tag", "")) == "cell" \
|
||
and String(row.get("cell_type", "")) == cell_type \
|
||
and String(row.get("polarity", "")) == polarity:
|
||
return row.duplicate() # E6:表行值拷贝出口——消费方禁改配置源
|
||
_fatal("极性字典缺行(八型×正负齐备为加载前提): %s %s" % [cell_type, polarity])
|
||
return {}
|
||
|
||
|
||
# ---- F1 选行算子纯函数 ----
|
||
|
||
# 偏运硬币 N(迭代 11 同调批 T5——战斗结算 begin_battle 注入;0 = 未装配零扰动)
|
||
var luck_bias: int = 0
|
||
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
|
||
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
|
||
|
||
|
||
# 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
|
||
func _biased_cell_rows() -> Array:
|
||
if luck_bias <= 0:
|
||
return _rollable_rows()
|
||
var out: Array = []
|
||
for row in _rollable_rows():
|
||
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
|
||
var biased: Dictionary = row.duplicate()
|
||
biased["weight"] = int(row.weight) + luck_bias
|
||
out.append(biased)
|
||
else:
|
||
out.append(row)
|
||
return out
|
||
|
||
|
||
func weighted_pick(rows: Array, r: int) -> String:
|
||
var total := _pool_weight(rows)
|
||
assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)")
|
||
var acc := 0
|
||
for row in rows:
|
||
acc += int(row.weight)
|
||
if r < acc:
|
||
return String(row.reward_id)
|
||
_fatal("weighted_pick 未命中(不应可达)")
|
||
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
|
||
|
||
|
||
func _pool_rows(pool_tag: String) -> Array:
|
||
var out: Array[Dictionary] = []
|
||
for row in db.reward.rows:
|
||
if row.pool_tag == pool_tag and int(row.weight) > 0:
|
||
out.append(row)
|
||
return out
|
||
|
||
|
||
func _pool_weight(rows: Array) -> int:
|
||
var w := 0
|
||
for row in rows:
|
||
w += int(row.weight)
|
||
return w
|
||
|
||
|
||
func _row_by_id(rid: String) -> Dictionary:
|
||
for row in db.reward.rows:
|
||
if row.reward_id == rid:
|
||
return row
|
||
return {}
|
||
|
||
|
||
func _snapshot(row: Dictionary, rolled_power: int = -1) -> Dictionary:
|
||
var snap := {
|
||
"reward_id": String(row.reward_id),
|
||
"cell_type": String(row.cell_type),
|
||
"display_name": String(row.display_name),
|
||
"power": int(row.power) if rolled_power < 0 else rolled_power, # 区间行 = 身份批 roll 值(E14 授予时快照口径)
|
||
"tier": int(row.tier),
|
||
"stacks": int(row.get("stacks", 1)),
|
||
}
|
||
if String(row.get("effect_id", "")) != "":
|
||
snap["effect_id"] = String(row.effect_id)
|
||
return snap
|
||
|
||
|
||
func _diag(msg: String) -> void:
|
||
push_warning("[地图战斗资源] " + msg)
|
||
|
||
|
||
func _fatal(msg: String) -> void:
|
||
push_error("[地图战斗资源/fatal] " + msg)
|
||
assert(false, msg)
|