246 lines
11 KiB
GDScript
246 lines
11 KiB
GDScript
## 掷格件:装配期格面资源摇号——池行装配、区间行 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)
|