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

246 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.
## 掷格件:装配期格面资源摇号——池行装配、区间行 roll、权重合成(订阅加权/board_skew 两路投影)。
## 自 RewardEconomy 迁出掷格面(architecture v1.29 轴戊②);受理账面(grant/身份查询/快照/retire/
## polarity_row)留守 RewardEconomy——衔接面=格→奖励身份映射批束(本件产出、受理件接收)。
## 取号流声明:roll 每批恰开一 reward_roll 流(scope=每次 install 批不变),体序=normalize(行主序
## +去重,零 RNG)→ 开流 → luck_bias 查询 → 池行装配 → 身份批逐格取号 → 清图批尾 → 区间行追加取号
## ——取号序与取号次数与拆件前逐字同(同种子逐位复现红线)。
## 零回指声明:本件零回指 RewardEconomy(fatal 经构造注入 fatal_cb 链转宿主封印通道——BoardInstaller
## 协作件先例);随机只经注入 RngServiceCore(持 economy 传入的同一实例——流身份不变)。
class_name RewardRoller
extends RefCounted
var db: ConfigDb
var rng: RngServiceCore
var fatal_cb: Callable = Callable() # 协作件违约→宿主 fatal 封印(BoardInstaller 先例——economy fatal_handler 属性 setter 同拍转注)
var luck_bias_query: Callable = Callable() # 偏运合计查询口(reward_roll 发射归 BattleFxHost 效果层——缝拆形态随件迁移;缺省空回调=恒 0=原行集零扰动)
var board_skew: int = 0 # 图级偏斜旋钮(写点=BoardInstaller 装板决议,全局型行 effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入)
var _treasure_unlocked := {}
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type;
# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除)
const SKEW_DOWN_KINDS := ["heal", "defense"]
const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"]
func _init(bundle: Dictionary) -> void:
for k in ["db", "rng", "fatal_cb"]:
assert(bundle.has(k), "[掷格件] 引用束缺键: %s" % k)
db = bundle.db
rng = bundle.rng
fatal_cb = bundle.fatal_cb
# 资格清单摇号(economy.roll_board 装配序委托——守卫与受理复位留守 economy;本函数只承载摇号本体)。
# 批束键集封闭:{"mapping": Dictionary(cell→reward_id 行主序插入序), "power_roll": Dictionary(cell→int),
# "clear_identity": String, "clear_power": int}
func roll(eligible: Array) -> Dictionary[Variant, Variant]:
var list: Array[Dictionary] = _normalize_eligible(eligible)
var stream: RandomNumberGenerator = rng.open_stream("reward_roll") # 恰一 scope 批(格批 + 清图批尾 + 区间行追加取号)
# reward_roll 发射归 BattleFxHost(bias_rewards 订阅命中合计经注入查询口取回);
# 零命中/未接线=0=原行集零扰动
var luck_bias := 0
if luck_bias_query.is_valid():
luck_bias = luck_bias_query.call(list.size())
var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(订阅件装配时稀有正向 kinds 权重 +N——池结构零触碰)
var clear_rows: Array[Variant] = _pool_rows("clear_default")
# ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布)
var draft := {}
for e in list:
draft[e.cell] = _weighted_pick_row(cell_rows, stream.randi_range(0, _pool_weight(cell_rows) - 1)).reward_id
# ② 清图槽批尾:同流同批判尾摇定清图奖励对象身份(选行算子返表行——就地取 reward_id,
# randi_range 求值位置与拆件前逐位同)
var clear_rid := String(_weighted_pick_row(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)).reward_id)
# ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号;行查就地内联——
# 局部变量预算内,取号序与取号次数零变化)
var power_roll := {}
for cell in draft.keys():
if _row_by_id(draft[cell]).has("power_min"):
power_roll[cell] = stream.randi_range(int(_row_by_id(draft[cell]).power_min), int(_row_by_id(draft[cell]).power_max))
var clear_power := -1
if _row_by_id(clear_rid).has("power_min"):
clear_power = stream.randi_range(int(_row_by_id(clear_rid).power_min), int(_row_by_id(clear_rid).power_max))
return {"mapping": draft, "power_roll": power_roll, "clear_identity": clear_rid, "clear_power": clear_power}
func set_treasure_unlocked(ids: Dictionary) -> void:
_treasure_unlocked = ids
## 资格清单规范化域(开流前——零 RNG):行主序 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key)
func _normalize_eligible(eligible: Array) -> Array[Dictionary]:
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))
return list
# 订阅加权权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动)
# 合成序:先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后
#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化)
func _biased_cell_rows(bias: int) -> Array:
var base: Array[Variant] = _skewed_cell_rows()
if bias <= 0:
return base
var out: Array[Variant] = []
for row in base:
if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))):
var biased: Dictionary[Variant, Variant] = row.duplicate()
biased["weight"] = int(row.weight) + bias
out.append(biased)
else:
out.append(row)
return out
# 图级资源面偏斜投影(整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族
# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。
# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。
func _skewed_cell_rows() -> Array:
if board_skew <= 0:
return _rollable_rows()
var base: Array[Variant] = _rollable_rows()
var families := _skew_families(base)
if families["down"].is_empty() or families["up"].is_empty():
return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批)
# 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局)
var drops := _largest_remainder_split(families["down"], board_skew, 100, (2 * _pool_weight(families["down"]) * board_skew + 100) / 200)
# 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整
var gains := _largest_remainder_split(families["up"], drops_total(drops), _pool_weight(families["up"]), drops_total(drops))
var out := _apply_skew(base, families["down"], families["up"], drops, gains)
assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)")
return out
## 分族域:生存下调族与进攻上浮族行集拆分(池结构不变式零触碰)
func _skew_families(base: Array) -> Dictionary:
var down: Array[Variant] = []
var up: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
if SKEW_DOWN_KINDS.has(k):
down.append(row)
elif SKEW_UP_KINDS.has(k):
up.append(row)
return {"down": down, "up": up}
## 权重装配域:逐行精确份额增减(下调族扣 drops、上浮族加 gains——守恒 assert 随段迁移)
func _apply_skew(base: Array, down: Array, up: Array, drops: Array, gains: Array) -> Array:
var di := 0
var ui := 0
var out: Array[Variant] = []
for row in base:
var k := String(row.get("cell_type", ""))
var w := int(row.weight)
if SKEW_DOWN_KINDS.has(k):
w -= drops[di]
di += 1
elif SKEW_UP_KINDS.has(k):
w += gains[ui]
ui += 1
assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)")
var skewed: Dictionary[Variant, Variant] = row.duplicate()
skewed["weight"] = w
out.append(skewed)
return out
func drops_total(amounts: Array[int]) -> int:
var t := 0
for a in amounts:
t += a
return t
# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows
# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total
static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]:
var floors: Array[int] = []
var fracs: Array[int] = []
var used := 0
for row in rows:
var exact: int = int(row.weight) * numer
floors.append(int(exact / denom))
fracs.append(exact % denom)
used += int(exact / denom)
var order: Array[int] = []
for i in rows.size():
order.append(i)
order.sort_custom(func(a: int, b: int) -> bool:
if fracs[a] != fracs[b]:
return fracs[a] > fracs[b]
return a < b)
var out: Array[int] = floors
var rest: int = total - used
assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)")
for j in rest:
out[order[j]] += 1
return out
# 选行算子(weighted_pick 改造形态——算术逐字:acc 累加/断言/fatal 通道;返回表行,调用侧取
# row.reward_id;取号算术与次数零变化。单点=本件 _weighted_pick_row,economy 受理面行查留守 _row_by_id)
func _weighted_pick_row(rows: Array, r: int) -> Dictionary:
var total := _pool_weight(rows)
assert(r >= 0 and r < total, "选行算子取号越域(纯函数域守卫)")
var acc := 0
for row in rows:
acc += int(row.weight)
if r < acc:
return row
_fatal("选行算子未命中(不应可达)")
return {}
# aux 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
func _rollable_rows() -> Array:
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
var out: Array[Dictionary] = []
for row in db.run.reward.rows:
if String(row.get("cell_type", "")) == RewardTable.TREASURE_ITEM_TYPE:
var iid := String(row.get("effect_id", ""))
if iid == "" or not _treasure_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.run.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.run.reward.rows:
if row.reward_id == rid:
return row
return {}
func _fatal(msg: String) -> void:
push_error("[掷格件/fatal] " + msg)
if fatal_cb.is_valid():
fatal_cb.call(msg) # economy fatal_handler 同拍转注(封印传导,不绕第二中止通道)
assert(false, msg)