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

272 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
# 行主序规范化 + 去重(排序责任不外推给 战斗结算)
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 a.cell.y * 1000 + a.cell.x < b.cell.y * 1000 + b.cell.x)
_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 = _rollable_rows() # cell 池正向行 weight>0(类型频次)
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 {}
# ---- F6 当量供给(行动经济 条款①对照实物输入——全活行投影 + tier_min_buff)----
func equivalence_supply() -> Dictionary:
var live: Array[Dictionary] = []
var tier_min_buff := 99
for row in db.reward.rows:
if int(row.weight) <= 0:
continue
live.append(row.duplicate()) # E6:表行值拷贝出口——消费方禁改配置源
# tier_min_buff = 我侧增益(hero_offense/hero_defense 池的 percent 正向行——attack_up/defense_up)最低档
if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" \
and String(row.get("effect_id", "")) != "":
var eff_row: Dictionary = db.status_effect_row(String(row.effect_id))
if not eff_row.is_empty() and String(eff_row.get("target_side", "")) == "hero":
tier_min_buff = mini(tier_min_buff, int(row.tier))
return {"live_rows": live, "tier_min_buff": tier_min_buff}
# ---- F1 选行算子纯函数 ----
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)