Files
gplan-2/game/core/reward_economy.gd
T
DKChan a6a2bf885e 迭代11第①批:魔法道具同调实施批 T0-T10 全落(重闸 645/0 + MC off 逐位 PASS)
- 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 令)
2026-10-03 15:41:30 +08:00

275 lines
11 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: 每个非封锁格是否都携带可兑的格面资源身份,且同种子同操作逐位回放
# 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)