iter49-卡1:效果端口与掷格分件批(轴戊两件建件):EffectPort六Callable路由+RewardRoller掷格面迁出+两业务类零引擎直连+effect_port_test V1-V3
This commit is contained in:
1 parent
926b05a61c
commit
11beb4fc59
31 files changed
+697
-586
No files matched your search
+15
-15
@@ -37,15 +37,15 @@ var _pending_ap_delta := 0 # 下轮行动点修正排队(act
|
||||
var _charge_layers := 0
|
||||
var charge_layers: int:
|
||||
get: return _charge_layers
|
||||
# 效果引擎(dig_paid/charge_full 发射面;round_end 转存决策面)
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
# 效果端口件(dig_paid/dig_stop/charge_full 发射面经端口路由宿主;组装期单点注入——
|
||||
# host _init bind_port;缺省 null=未绑零扰动,与原 _fx==null 守卫同语义)
|
||||
var _port: EffectPort = null
|
||||
var _last_stop_ap := 0 # 停手侧剩余 AP 记账(转存决策在 round_end 发射点——grant_ap(transfer) 订阅定夺)
|
||||
|
||||
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
_fx = fx
|
||||
_ctx = ctx
|
||||
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
|
||||
func bind_port(port: EffectPort) -> void:
|
||||
_port = port
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
@@ -116,16 +116,16 @@ func _apply_dig_paid_hits(base_cost: int) -> Dictionary:
|
||||
var cost := base_cost
|
||||
var dig_refund := 0
|
||||
var rap_gain := 0
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch(_ctx, "dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
|
||||
if _port != null:
|
||||
for h in _port.emit("dig_paid", {"ap_cost": cost, "ap_surplus": _ap - cost}):
|
||||
if String(h.get("kind", "")) == "counter_spend":
|
||||
var extra := int(h.get("value", 1))
|
||||
if _ap >= cost + extra:
|
||||
cost += extra
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), 1)
|
||||
_port.counter_add(String(h.get("counter_id", "")), 1, false)
|
||||
elif String(h.get("kind", "")) == "counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
elif String(h.get("kind", "")) == "grant_ap":
|
||||
dig_refund += int(h.get("value", 0))
|
||||
elif String(h.get("kind", "")) == "grant_reserve_ap":
|
||||
@@ -174,13 +174,13 @@ func request_stop() -> bool:
|
||||
return false
|
||||
# dig_stop 发射+施加(停手挂点〔弃图兑换〕:剩余 AP 随载荷外发,counter_add
|
||||
# value_event="ap_left" 取值面〔值=停手时刻真实余量〕;发射先于记账清零)
|
||||
if _fx != null:
|
||||
for h in _fx.dispatch(_ctx, "dig_stop", {"ap_left": _ap}):
|
||||
if _port != null:
|
||||
for h in _port.emit("dig_stop", {"ap_left": _ap}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var val := int(h.get("value", 0))
|
||||
if String(h.get("value_event", "")) == "ap_left":
|
||||
val = _ap
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), val, h.has("set"))
|
||||
_port.counter_add(String(h.get("counter_id", "")), val, h.has("set"))
|
||||
_last_stop_ap = _ap
|
||||
_ap = 0
|
||||
_finish_round("stopped", "same_board")
|
||||
@@ -201,8 +201,8 @@ func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void:
|
||||
if transfer:
|
||||
var prev_layers := _charge_layers
|
||||
_charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap)
|
||||
if _fx != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
|
||||
_fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers})
|
||||
if _port != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP:
|
||||
_port.emit("charge_full", {"layers": _charge_layers})
|
||||
else:
|
||||
_rap += _last_stop_ap * maxi(1, transfer_pct) / 100
|
||||
_last_stop_ap = 0
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
## 留宿主,禁回调宿主私有族;揭示奖励兑付走 bind_grant 注入的兑付回调(RewardGrant 授予面——
|
||||
## snapshot 查询与 grant_routed 同面);对本件零反向强引用(禁 RefCounted 双向环——两件均随每战新建)。
|
||||
## 随机只经注入 rng 的 fx_reveal 封闭流(除该流外零随机;零 await——效果引擎全同步解释器)。
|
||||
## 类变量封闭面(14≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb,
|
||||
## 类变量封闭面(15≤15):bundle 7 项(db/rng/minesweeper/action_economy/damage/emit_cb/facts_cb,
|
||||
## 缺一键 assert——引用束注入防 _init max-parameters 新键)+grant_cb+engine+counters_ctx+
|
||||
## ac 束(键集封闭 base/round/battle)+_fx_bonus_delta+_fx_new_resource_types+reveal 束
|
||||
## (键集封闭 queue/busy)。禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/
|
||||
## (键集封闭 queue/busy)+facts_inject(键集封闭 dig_streak/rider_tier_bonus/rider_tier_crit——
|
||||
## 业务发射事实束运行态写面,combat_segment 每轮写 dig_streak/begin_battle 每战复位 rider 两键)。
|
||||
## 禁实现期增删键;禁为机检文本保留无效成员(原编排器 fx_counters_round/
|
||||
## battle 两零读者成员迁入即删——同物异名同字典,桶引用经 counters_ctx 承载)。
|
||||
## 效果端口件组建与转注(轴戊):_init 尾段建 EffectPort(六 Callable 绑本件对应方法)后对两业务类
|
||||
## bind_port——端口不落本件成员(单向接线,本件零回持);业务类发射面全经端口路由,
|
||||
## 计数器账面(counters_ctx)唯一宿主侧。
|
||||
class_name BattleFxHost
|
||||
extends RefCounted
|
||||
|
||||
@@ -29,6 +34,7 @@ var ac := {"base": -1, "round": 0, "battle": 0} # ac 束(base=主角 AC 基
|
||||
var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末宿主 bonus_delta_drain 读后清零)
|
||||
var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域)
|
||||
var reveal := {"queue": [], "busy": false} # reveal 束(queue=揭示效果待兑队列〔见血开窍/斩首学者/地质学家/跳格采掘共用——授予段末统一兑付,防轮 facts 迭代中追加重入〕;busy=兑付中标记——宿主 _on_cell_revealed 缓冲旁路判定,防二次缓冲二次兑付)
|
||||
var facts_inject := {"dig_streak": 0, "rider_tier_bonus": 0, "rider_tier_crit": false} # 业务发射事实束(键集封闭——原 damage.inject 运行态束迁宿主:dig_streak=combat_segment 每轮写;rider 两键=begin_battle 每战复位;charge_layers 不在束内=活读 action_economy)
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
@@ -43,6 +49,18 @@ func _init(bundle: Dictionary) -> void:
|
||||
facts_cb = bundle.facts_cb
|
||||
engine = EffectEngine.new(db)
|
||||
counters_ctx = EffectContext.new() # round/battle 两桶即 ctx 自带字典——编排器旧 fx_counters 两成员同物异名零读者,不再另立
|
||||
# 效果端口件组建与转注(轴戊——组装期单点:先于任何发射,六 Callable 绑本件方法后对两业务类转注;
|
||||
# 端口不落本件成员——单向接线,本件零回持;方法直引用形态同 bundle emit_cb/facts_cb 既有接线)
|
||||
var port := EffectPort.new({
|
||||
"emit_cb": fx_emit_biz,
|
||||
"counter_read_cb": counter_read,
|
||||
"counter_add_cb": counter_add,
|
||||
"fact_cb": biz_fact,
|
||||
"reveal_cb": enqueue_reveal,
|
||||
"ac_cb": apply_ac_bonus,
|
||||
})
|
||||
bundle.damage.bind_port(port)
|
||||
bundle.action_economy.bind_port(port)
|
||||
|
||||
|
||||
# 揭示兑付回调挂接(宿主 _init 建两件后接线——构造序禁环)
|
||||
@@ -55,6 +73,41 @@ func fx_emit(event: String, payload: Dictionary) -> Array:
|
||||
return engine.dispatch(counters_ctx, event, payload, _fx_fact)
|
||||
|
||||
|
||||
# 业务类发射路由(效果端口件 emit_cb——damage/action_economy 发射面统一出口;
|
||||
# 事实回调=biz_fact 闭面五键,battle_index/round_index/seals_count/nest_count 禁进业务发射面)
|
||||
func fx_emit_biz(event: String, payload: Dictionary) -> Array:
|
||||
return engine.dispatch(counters_ctx, event, payload, biz_fact)
|
||||
|
||||
|
||||
# 计数器账面路由(效果端口件 counter_read_cb——value_counter 层值读;账面唯一宿主侧)
|
||||
func counter_read(counter_id: String) -> int:
|
||||
return EffectEngine.counter(counters_ctx, counter_id)
|
||||
|
||||
|
||||
# 计数器账面路由(效果端口件 counter_add_cb——施加单点;账面唯一宿主侧)
|
||||
func counter_add(counter_id: String, value: int, set_form: bool) -> void:
|
||||
EffectEngine.counter_add(counters_ctx, counter_id, value, set_form)
|
||||
|
||||
|
||||
# 业务发射事实面(闭面五键——三 when 事实 {charge_layers 活读/dig_streak 轮内槽/hp_ratio 主角
|
||||
# 血量比例算式逐字承 damage_controller 原式}∪ 两 rider 运行态读数 {facts_inject 现值};
|
||||
# 其余事实一律 null——battle_index/round_index/seals_count/nest_count 禁进,扩面=行为变化)
|
||||
func biz_fact(name: String) -> Variant:
|
||||
match name:
|
||||
"charge_layers":
|
||||
return action_economy.charge_layers
|
||||
"dig_streak":
|
||||
return int(facts_inject["dig_streak"])
|
||||
"hp_ratio":
|
||||
var hu: Dictionary[Variant, Variant] = damage.units.hero_unit()
|
||||
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
|
||||
"rider_tier_bonus":
|
||||
return int(facts_inject["rider_tier_bonus"])
|
||||
"rider_tier_crit":
|
||||
return bool(facts_inject["rider_tier_crit"])
|
||||
return null
|
||||
|
||||
|
||||
# 偏运权重查询(reward_roll 发射面——bias_rewards 词汇在本层收口,掷格域只见整数)。
|
||||
# 三参形 dispatch(不带 facts:本事件订阅行 when 事实面不参与,命中集与缝拆前逐字同);
|
||||
# 发射时点由掷格件在开流之后、权重装配之前调用;零命中=0=原行集零扰动。
|
||||
|
||||
@@ -226,9 +226,8 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
"action_economy": action_economy, "status_effects": _status_effects, "units": _enemy_system, "emit_cb": _emit_row,
|
||||
"fatal_cb": _fatal, "attune_safe_seen_cb": _attune_safe_seen_read, "pool_effective_cb": _pool_effective})
|
||||
_fx_host.bind_grant(_reward_grant.reveal_redeem) # 构造序禁环:两件建齐后接线(fx 件对授予面走注入回调)
|
||||
_damage.set_fx(_fx_host.engine, _fx_host.counters_ctx) # 效果引擎注入(attack_commit/clamp_check/armor_roll/kill/damage_taken 发射面)
|
||||
action_economy.set_fx(_fx_host.engine, _fx_host.counters_ctx) # dig_paid/charge_full 发射面
|
||||
_reward_economy.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(reward_roll 发射归 fx 宿主)
|
||||
# 业务类发射面接线归 host._init 尾段组装期完成(效果端口件 bind_port 转注——先于 begin_battle 任何发射)
|
||||
_reward_economy.roller.luck_bias_query = _fx_host.luck_bias_for # 偏运查询口注入(掷格件缝拆形态随件迁移——reward_roll 发射归 fx 宿主)
|
||||
minesweeper.cell_revealed.connect(_on_cell_revealed)
|
||||
minesweeper.seal_judged.connect(_on_seal_judged)
|
||||
minesweeper.round_end_board.connect(_on_round_end)
|
||||
@@ -340,13 +339,11 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
|
||||
var adv_bucket: Variant = enc.get("adv_bucket", null)
|
||||
_fx_host.mount_buckets(adv_bucket if adv_bucket is Dictionary else {}, enc.get("level_counters")) # 冒险桶宿主字典(直构兜底自有空字典);关卡桶 LevelRunner 每关 new 下传、非 Dictionary 即不挂
|
||||
_reward_grant.bind_battle(_battle_hooks()) # 授予件战斗域回调束(充能消耗/真视计数/踩雷计数)
|
||||
_damage.fx_calls["reveal"] = _fx_host.enqueue_reveal
|
||||
_damage.fx_calls["ac"] = _fx_host.apply_ac_bonus
|
||||
var feat_ids: Array[Variant] = enc.get("feats", []) as Array
|
||||
# 流派档位质变不设(奇物.md v1「不设流派标签、
|
||||
# 不设档位机制」):rider_tier 两注入恒零,散件单体效果=唯一形态
|
||||
_damage.inject["rider_tier_bonus"] = 0
|
||||
_damage.inject["rider_tier_crit"] = false
|
||||
# 不设档位机制」):rider_tier 两运行态复位恒零,散件单体效果=唯一形态
|
||||
_fx_host.facts_inject["rider_tier_bonus"] = 0
|
||||
_fx_host.facts_inject["rider_tier_crit"] = false
|
||||
_facts.feat_first_dig_used = false
|
||||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
|
||||
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
|
||||
@@ -499,7 +496,7 @@ func _hits_of(hits: Array, kind: String) -> Array:
|
||||
|
||||
# aux 解锁门控转发(LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
|
||||
func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
_reward_economy.set_treasure_unlocked(ids)
|
||||
_reward_economy.roller.set_treasure_unlocked(ids)
|
||||
|
||||
|
||||
# ---- 开轮摇号(共识点 B:技能选择前置到轮次开始,数值计算留结算段)----
|
||||
|
||||
@@ -77,13 +77,13 @@ func install(during_round := false) -> bool:
|
||||
# region 布置 = 兜底回落合法态(防配置缺行静默裸奔——provisional)。
|
||||
func _resolve_global_variant(board: BoardData) -> void:
|
||||
board.board_variants.clear()
|
||||
reward_economy.board_skew = 0
|
||||
reward_economy.roller.board_skew = 0
|
||||
var row: Dictionary[Variant, Variant] = db.combat.variation.global_variant_row(int(encounter.level_index), String(board.board_type),
|
||||
String(encounter.get("node_type", "")))
|
||||
if row.is_empty():
|
||||
return
|
||||
board.board_variants = [String(row.variant_id)]
|
||||
reward_economy.board_skew = int(row.get("effect_value", 0))
|
||||
reward_economy.roller.board_skew = int(row.get("effect_value", 0))
|
||||
|
||||
|
||||
# ---- 地形变异 布置段(棋盘设置顺序第 4 步)----
|
||||
|
||||
@@ -41,19 +41,9 @@ var _outcomes := {} # ticket_id → "redeemed"/"zero"/"voided"(
|
||||
var _seq := 0 # 流水号
|
||||
var _halted := false # 主角倒下终止封印(死亡事件置位——flush 余单作废)
|
||||
var _hero_ac_override := -1 # 主角护甲等级合流入参(begin_battle 显式设定——-1 = 表值兜底,口径同战斗编排器)
|
||||
# 效果引擎(attack_commit/clamp_check/armor_roll/kill/damage_taken
|
||||
# 发射+施加面:dispatch 返回命中,本类按组合规则施加)
|
||||
var _fx: EffectEngine = null
|
||||
var _ctx: EffectContext = null
|
||||
# 战斗段运行态事实束(编排器每轮/每战注入——键集封闭,缺键读缺省即零扰动):
|
||||
# dig_streak/charge_layers——facts 取数面(attack_commit 载荷 dig_streak;猎巢人 per/整备
|
||||
# attack_bonus per/厚积薄发 when/铁壁 shield per 同读 charge_layers)——combat_segment 每轮写;
|
||||
# rider_tier_bonus/rider_tier_crit——流派档位两运行态(magic 档一 rider+1/档二 rider 可暴击)
|
||||
# ——begin_battle 每战写
|
||||
var inject := {"dig_streak": 0, "charge_layers": 0, "rider_tier_bonus": 0, "rider_tier_crit": false}
|
||||
# 效果引擎入站回调束(编排器 begin_battle 同点注入):reveal=揭示效果回调(kill 发射点 reveal_tiles
|
||||
# 订阅面,入队授予段末兑付)/ac=ac_bonus 回调(石肤 AC 累积单点同步)
|
||||
var fx_calls := {"reveal": Callable(), "ac": Callable()}
|
||||
# 效果端口件(组装期单点注入——host _init bind_port):发射/计数器/事实/揭示/AC 全经端口路由,
|
||||
# 缺省 null=未绑(与原 _fx==null 零扰动守卫同语义——单测直构不接端口的夹具零扰动照跑)
|
||||
var _port: EffectPort = null
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref, units_ref, effects_ref) -> void:
|
||||
@@ -69,44 +59,33 @@ func set_hero_ac_override(v: int) -> void:
|
||||
_hero_ac_override = v
|
||||
|
||||
|
||||
# 效果引擎注入(战斗编排器 _init——事件发射面接线)
|
||||
func set_fx(fx: EffectEngine, ctx: EffectContext) -> void:
|
||||
_fx = fx
|
||||
_ctx = ctx
|
||||
# 效果端口件注入(host _init 组装期单点——先于 begin_battle 任何发射)
|
||||
func bind_port(port: EffectPort) -> void:
|
||||
_port = port
|
||||
|
||||
|
||||
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加)
|
||||
# 事件发射薄面(发射+命中收集统一入口——订阅空=零循环零扰动;命中由各施加面按组合规则施加;
|
||||
# 路由经效果端口件,事实回调=biz_fact 闭面五键宿主侧)
|
||||
func _fx_hits(event: String, payload: Dictionary) -> Array:
|
||||
if _fx == null:
|
||||
if _port == null:
|
||||
return []
|
||||
return _fx.dispatch(_ctx, event, payload, _fx_fact)
|
||||
return _port.emit(event, payload)
|
||||
|
||||
|
||||
# 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面)
|
||||
# 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。
|
||||
# hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕;
|
||||
# fx_calls.reveal=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕)
|
||||
func _fx_fact(fact: String) -> Variant:
|
||||
match fact:
|
||||
"charge_layers":
|
||||
return int(inject["charge_layers"])
|
||||
"dig_streak":
|
||||
return int(inject["dig_streak"])
|
||||
"hp_ratio":
|
||||
var hu: Dictionary[Variant, Variant] = units.hero_unit()
|
||||
return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径)
|
||||
return null
|
||||
# 未绑事实回落(缺省 null——int()/bool() 包裹后与原 inject 缺省值逐位同:int(null)=0/bool(null)=false)
|
||||
func _fact_or(name: String) -> Variant:
|
||||
return null if _port == null else _port.fact(name)
|
||||
|
||||
|
||||
# per 取数(per 字段解析——载荷同名字段优先,回落补充事实面;add_ticket_rider/attack_bonus/
|
||||
# per 取数(per 字段解析——载荷同名字段优先,回落端口事实面;add_ticket_rider/attack_bonus/
|
||||
# shield/open_ticket 的倍数承载:贡献值 = value × per 读数,cap 对乘积封顶)
|
||||
func _per_value(name: String, payload: Dictionary) -> int:
|
||||
if name == "":
|
||||
return 1
|
||||
if payload.has(name):
|
||||
return int(payload[name])
|
||||
if _fx_fact(name) != null:
|
||||
return int(_fx_fact(name))
|
||||
if _fact_or(name) != null:
|
||||
return int(_fact_or(name))
|
||||
return 0
|
||||
|
||||
|
||||
@@ -169,7 +148,7 @@ func open_ticket(kind: String, source_class: String, display: String, spec: Dict
|
||||
var ac_payload := {"atk": int(t.base_value),
|
||||
"target": String(tgt.get("display_name", "")) if tgt != null else "",
|
||||
"target_id": String(tgt.get("enemy_id", "")) if tgt != null else "",
|
||||
"matchup": matchup_o, "dig_streak": int(inject["dig_streak"]), "nest_count": _nest_count()}
|
||||
"matchup": matchup_o, "dig_streak": int(_fact_or("dig_streak") or 0), "nest_count": _nest_count()}
|
||||
t.context["fx_attack_hits"] = _fx_hits("attack_commit", ac_payload)
|
||||
t.context["fx_attack_payload"] = ac_payload
|
||||
return t
|
||||
@@ -361,13 +340,13 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
|
||||
if String(h.get("kind", "")) == "attack_bonus":
|
||||
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload)
|
||||
if h.has("value_counter") and String(h.get("value_counter", "")) != "":
|
||||
contrib = EffectEngine.counter(_ctx, String(h.get("value_counter", "")))
|
||||
contrib = _port.counter_read(String(h.get("value_counter", "")))
|
||||
base_value += float(contrib)
|
||||
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序)
|
||||
# 计数器施加(attack_commit 命中行内 counter_add〔用后清零行〕,命中序=声明序;端口路由账面)
|
||||
for h in ac_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
# 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载)
|
||||
if ctx["spike_ready"]:
|
||||
for h in ac_hits:
|
||||
@@ -397,7 +376,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
|
||||
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
|
||||
# rider 不改变下限语义(landed 含 rider 后不再过 floor)。
|
||||
var hero_rider := int(inject["rider_tier_bonus"])
|
||||
var hero_rider := int(_fact_or("rider_tier_bonus") or 0) # or 0=未绑缺省与 int(null) 缺省同值(int() 构造拒 null——端口缺省 null 面的空值合并)
|
||||
for h in ac_hits:
|
||||
if String(h.get("kind", "")) == "add_ticket_rider":
|
||||
var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"])
|
||||
@@ -405,7 +384,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
contrib = mini(int(h.cap), contrib)
|
||||
hero_rider += contrib
|
||||
if hero_rider > 0:
|
||||
if bool(ctx["crit"]) and bool(inject["rider_tier_crit"]):
|
||||
if bool(ctx["crit"]) and bool(_fact_or("rider_tier_crit") or false):
|
||||
hero_rider = int(round(float(hero_rider) * float(db.combat.settlement.damage_roll.crit_multiplier)))
|
||||
landed += hero_rider
|
||||
payload["hero_rider"] = hero_rider
|
||||
@@ -421,9 +400,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void:
|
||||
for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}):
|
||||
if String(h.get("kind", "")) == "reveal_tiles":
|
||||
var reveal_cb: Callable = fx_calls["reveal"]
|
||||
if reveal_cb.is_valid():
|
||||
reveal_cb.call() # 斩首学者——击杀揭示 1 格(编排器入队,授予段末兑付)
|
||||
_port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付)
|
||||
continue
|
||||
if String(h.get("kind", "")) != "overflow":
|
||||
continue
|
||||
@@ -468,7 +445,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
|
||||
var total: int = int(roll["total"])
|
||||
# damage_taken 发射(kind=enemy_skill;铁壁整备减伤订阅面:shield 加法族合计 value×per(charge_layers),
|
||||
# 稳健拓荒树规则同面(rebuild 合成订阅);减后过下限保底。damage_reduce〔背水 hp_ratio when〕/
|
||||
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 _fx_fact 补充事实面回落提供)
|
||||
# counter_add〔保险丝/石肤〕/ac_bonus〔石肤〕同面施加;hp_ratio fact 由 damage_taken 载荷携带)
|
||||
var dt_res := _emit_hero_damage_taken(total, "enemy_skill", String(t.context.get("skill_id", "")), t.origin_cell, int(t.round_index))
|
||||
total = int(dt_res["total"])
|
||||
units.apply_damage(hero, total)
|
||||
@@ -525,12 +502,10 @@ func _apply_hero_damage_effects(hits: Array, total: int) -> Dictionary:
|
||||
"damage_reduce":
|
||||
total = maxi(0, total - int(h.get("value", 0)))
|
||||
"ac_bonus":
|
||||
var ac_cb: Callable = fx_calls["ac"]
|
||||
if ac_cb.is_valid():
|
||||
ac_cb.call(h)
|
||||
_port.apply_ac(h)
|
||||
"counter_add":
|
||||
var set_v = h.get("set", null)
|
||||
EffectEngine.counter_add(_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
_port.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null)
|
||||
if shield > 0:
|
||||
total = maxi(int(db.combat.settlement.damage_roll.damage_floor), total - shield)
|
||||
return {"total": total, "shield": shield}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
## 效果端口件:战斗业务类(DamageController/ActionEconomy)唯一的效果发射出口——收事件与载荷、
|
||||
## 交效果引擎结算、取回命中批。六 Callable 路由宿主(BattleFxHost.fx_emit_biz/counter_read/
|
||||
## counter_add/biz_fact/enqueue_reveal/apply_ac_bonus),经组装期 bind_port 单点注入。
|
||||
## 闭面红线:事实回调=biz_fact 闭面五键(charge_layers/dig_streak/hp_ratio/rider_tier_bonus/
|
||||
## rider_tier_crit),battle_index/round_index/seals_count/nest_count 禁进业务发射事实面。
|
||||
## 单向接线:本件只持宿主注入的 Callable,禁持 BattleFxHost 对象引用(宿主 bundle 已持两业务类
|
||||
## ——对象引用即成 host→damage→port→host 强引用环,两件均随每战新建=泄漏)。
|
||||
## 未绑缺省:emit 返回空批/计数器读 0/计数器施加与揭示与 AC 回调 no-op/事实答 null——
|
||||
## 与原业务类 _fx==null 零扰动守卫同语义(单测直构不接端口的既有夹具零扰动照跑)。
|
||||
class_name EffectPort
|
||||
extends RefCounted
|
||||
|
||||
var emit_cb: Callable # func(event: String, payload: Dictionary) -> Array——宿主 fx_emit_biz
|
||||
var counter_read_cb: Callable # func(counter_id: String) -> int——宿主 counter_read
|
||||
var counter_add_cb: Callable # func(counter_id: String, value: int, set_form: bool) -> void——宿主 counter_add
|
||||
var fact_cb: Callable # func(name: String) -> Variant——宿主 biz_fact(伤害域闭面)
|
||||
var reveal_cb: Callable # func() -> void——宿主 enqueue_reveal
|
||||
var ac_cb: Callable # func(h: Dictionary) -> void——宿主 apply_ac_bonus
|
||||
|
||||
|
||||
func _init(bundle: Dictionary) -> void:
|
||||
for k in ["emit_cb", "counter_read_cb", "counter_add_cb", "fact_cb", "reveal_cb", "ac_cb"]:
|
||||
assert(bundle.has(k), "[效果端口] 引用束缺键: %s" % k)
|
||||
emit_cb = bundle.emit_cb
|
||||
counter_read_cb = bundle.counter_read_cb
|
||||
counter_add_cb = bundle.counter_add_cb
|
||||
fact_cb = bundle.fact_cb
|
||||
reveal_cb = bundle.reveal_cb
|
||||
ac_cb = bundle.ac_cb
|
||||
|
||||
|
||||
# 事件发射(收事件与载荷、交宿主路由效果引擎结算、取回命中批;未绑→空批)
|
||||
func emit(event: String, payload: Dictionary) -> Array:
|
||||
if not emit_cb.is_valid():
|
||||
return []
|
||||
return emit_cb.call(event, payload)
|
||||
|
||||
|
||||
# 计数器层值读(value_counter 层值取数——宿主 counters_ctx 账面单点;未绑→0)
|
||||
func counter_read(counter_id: String) -> int:
|
||||
if not counter_read_cb.is_valid():
|
||||
return 0
|
||||
return int(counter_read_cb.call(counter_id))
|
||||
|
||||
|
||||
# 计数器施加(命中行 counter_add 用后清零/计账——未绑→no-op)
|
||||
func counter_add(counter_id: String, value: int, set_form: bool) -> void:
|
||||
if not counter_add_cb.is_valid():
|
||||
return
|
||||
counter_add_cb.call(counter_id, value, set_form)
|
||||
|
||||
|
||||
# 事实取值(闭面五键宿主侧单点——未绑→null)
|
||||
func fact(name: String) -> Variant:
|
||||
if not fact_cb.is_valid():
|
||||
return null
|
||||
return fact_cb.call(name)
|
||||
|
||||
|
||||
# 揭示效果入队(kill 发射点 reveal_tiles 订阅面——授予段末兑付;未绑→no-op)
|
||||
func reveal_effect() -> void:
|
||||
if not reveal_cb.is_valid():
|
||||
return
|
||||
reveal_cb.call()
|
||||
|
||||
|
||||
# AC 累积回调(damage_taken ac_bonus 石肤累积单点;未绑→no-op)
|
||||
func apply_ac(h: Dictionary) -> void:
|
||||
if not ac_cb.is_valid():
|
||||
return
|
||||
ac_cb.call(h)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b50wf7iako1uy
|
||||
+24
-220
@@ -1,17 +1,26 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 格面资源身份层:install 期一次性预分配每格资源身份,授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。
|
||||
## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁在本类引入授予期随机或重掷。
|
||||
## 格面资源身份层(受理账面):授予期零随机、Ready 后只读无重掷(垂直切片验证:格面资源同种子逐位回放)。
|
||||
## 摇号面已分家(architecture v1.29 轴戊):装配期格面资源摇号(池行装配/区间行 roll/权重合成)
|
||||
## 归 RewardRoller(本件 _init 自建自持有,外部经 roller 组件出口直达);本类保留受理账面
|
||||
## (grant/身份查询/快照/retire/polarity_row)与 roll_board 装配序(守卫→复位→委托→收纳→ready)。
|
||||
## 衔接面=格→奖励身份映射批束(键集封闭 mapping/power_roll/clear_identity/clear_power——掷格件产出、
|
||||
## 本件接收收纳)。
|
||||
## roll_board 每板恰一次(Ready 相位再调 = double-roll 拒绝;换图 = 合法新批);禁引入授予期随机或重掷。
|
||||
## 极性路由归战斗结算授予段——本类只提供 polarity_row 的(类型, 极性)→ 字典行查询面。
|
||||
class_name RewardEconomy
|
||||
extends RefCounted
|
||||
|
||||
var db: ConfigDb
|
||||
var rng: RngServiceCore
|
||||
# 违约中止通道——缺省空 Callable 退化 push_error+assert(现行为逐字保持);组装层注入
|
||||
# orchestrator._fatal,使 release 剥 assert 后仍经 battle_result 封印真中止。
|
||||
var fatal_handler: Callable = Callable()
|
||||
# 属性 setter 同拍转注 roller(构造序:economy._init 不触该成员、编排器注入晚于 _init——
|
||||
# roller 已在座时赋值即转注,掷格件违约封印与受理面同一通道)
|
||||
var fatal_handler: Callable = Callable():
|
||||
set(v):
|
||||
fatal_handler = v
|
||||
if roller != null:
|
||||
roller.fatal_cb = v
|
||||
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 = 定值行)
|
||||
@@ -19,19 +28,19 @@ var _granted := {} # Vector2i → 快照(重复受理幂等返
|
||||
var _clear_identity := ""
|
||||
var _clear_granted := false
|
||||
var _clear_snapshot := {}
|
||||
# 偏运权重查询口(reward_roll 发射归 BattleFxHost 效果层——掷格域不持效果引擎/上下文;
|
||||
# 参数=本次可掷格数、返回=偏运权重加算量、缺省空回调=恒 0=原行集零扰动)
|
||||
var luck_bias_query: Callable = Callable()
|
||||
var roller: RewardRoller # 掷格件(摇号面单点——自建自接线,BoardInstaller.generator 先例)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
db = db_ref
|
||||
rng = rng_ref
|
||||
roller = RewardRoller.new({"db": db_ref, "rng": rng_ref, "fatal_cb": fatal_handler})
|
||||
|
||||
|
||||
# ---- install 期 roll(恰一次;Ready 相位再调拒绝)----
|
||||
|
||||
# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单)
|
||||
# eligible = 全部非封锁格(安全格 + 危险格,战斗结算 install 编排供给;封锁格不入清单)。
|
||||
# 装配序:相位守卫(E5,先行——违约路径零取号)→ 空清单 fatal(E1)→ 受理面复位五拍 →
|
||||
# 委托掷格件摇号 → 收纳批束 → ready
|
||||
func roll_board(eligible: Array) -> bool:
|
||||
# 相位守卫:Ready 再调 = double-roll 拒绝(E5);Retired(换图后)= 合法新批重置(E6 roll 半)
|
||||
if _phase == "ready":
|
||||
@@ -40,56 +49,20 @@ func roll_board(eligible: Array) -> bool:
|
||||
if eligible.is_empty():
|
||||
_fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)")
|
||||
return false
|
||||
_eligible = _normalize_eligible(eligible)
|
||||
var list: Array[Dictionary] = _eligible
|
||||
_granted.clear() # 新批重置映射受理面(旧图快照消亡)
|
||||
_power_roll.clear()
|
||||
_clear_power = -1
|
||||
_clear_granted = false
|
||||
_clear_snapshot = {}
|
||||
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(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[Variant, Variant] = _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[Variant, Variant] = _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
|
||||
var batch := roller.roll(eligible)
|
||||
_mapping = batch.mapping
|
||||
_power_roll = batch.power_roll
|
||||
_clear_identity = batch.clear_identity
|
||||
_clear_power = batch.clear_power
|
||||
_phase = "ready"
|
||||
return true
|
||||
|
||||
|
||||
## 资格清单规范化域(开流前——零 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
|
||||
|
||||
|
||||
# ---- 查询面(Ready 只读;未开格恒可查——秘宝 桥接结构前提)----
|
||||
|
||||
# 相位只读出口(诊断观测面——unrolled/ready/retired;消费方禁直读 _phase 下划线字段)
|
||||
@@ -181,175 +154,6 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
|
||||
# ---- F1 选行算子纯函数 ----
|
||||
|
||||
# 偏运硬币 N(reward_roll bias_rewards 订阅命中合计经 _biased_cell_rows 入参;0 = 未装配零扰动)
|
||||
# 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰)
|
||||
const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"]
|
||||
|
||||
# 图级偏斜旋钮(battle_orchestrator 装板决议注入,全局型行
|
||||
# effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入)
|
||||
var board_skew: int = 0
|
||||
|
||||
# 偏斜族(需求卡片 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type;
|
||||
# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除)
|
||||
const SKEW_DOWN_KINDS := ["heal", "defense"]
|
||||
const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"]
|
||||
|
||||
|
||||
# 偏运硬币权重投影(底稿 §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
|
||||
|
||||
|
||||
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 解锁门控集(「解锁→进投放池」:未解锁 aux 行不入摇号池。
|
||||
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
|
||||
var _treasure_unlocked := {}
|
||||
|
||||
|
||||
func set_treasure_unlocked(ids: Dictionary) -> void:
|
||||
_treasure_unlocked = ids
|
||||
|
||||
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
## 掷格件:装配期格面资源摇号——池行装配、区间行 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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ns1oyct41fhk
|
||||
@@ -31,10 +31,9 @@ func run(ri: int) -> void:
|
||||
round_safe += 1
|
||||
elif evt.kind == "mine":
|
||||
round_mine = true
|
||||
oc._damage.inject["dig_streak"] = 0 if round_mine else round_safe
|
||||
# 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备
|
||||
# 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新)
|
||||
oc._damage.inject["charge_layers"] = oc.action_economy.charge_layers
|
||||
oc.fx_host().facts_inject["dig_streak"] = 0 if round_mine else round_safe
|
||||
# 蓄力层不再逐轮注入(效果端口件闭面——fact charge_layers 由宿主 biz_fact 活读 action_economy
|
||||
# 现值:唯一写点=finish_round_stop 于上轮授予段尾,本轮授予段发射前无第二写点,活读等价)
|
||||
_hero_strike(ri)
|
||||
_enemy_actions(ri)
|
||||
_board_deferred_settle(ri)
|
||||
@@ -61,7 +60,7 @@ func _hero_strike(ri: int) -> void:
|
||||
# ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位
|
||||
# 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致)
|
||||
for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"):
|
||||
var segments := mini(int(h.get("cap", 2)), int(oc._damage.inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
|
||||
var segments := mini(int(h.get("cap", 2)), int(oc.fx_host().facts_inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
|
||||
for s in segments:
|
||||
# source_class 用中性名;attack_segments 兑付通道保留泛用面
|
||||
var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段",
|
||||
|
||||
@@ -14,11 +14,21 @@ const AUTOLOAD_PATH_RNG := "/root/RngService"
|
||||
const AUTOLOAD_PATH_SAVE := "/root/SaveManager"
|
||||
const AUTOLOAD_PATH_META := "/root/MetaProgression"
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd")
|
||||
const Narrator := preload("res://presentation/battle_narrator.gd")
|
||||
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
|
||||
const DebugSessionScript := preload("res://presentation/debug_session.gd")
|
||||
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
|
||||
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
|
||||
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
|
||||
const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族
|
||||
const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件
|
||||
const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件
|
||||
const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件
|
||||
const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件
|
||||
const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景
|
||||
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
|
||||
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
|
||||
|
||||
var db
|
||||
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
|
||||
|
||||
@@ -230,7 +230,7 @@ static func motion(dur: float) -> float:
|
||||
# 的字形/无演出回退路径;禁 preload(素材缺位树上直接断言炸脚本,冒烟安全红线)
|
||||
const TEX_HERO_IDLE := "res://assets/art/character/character_hero_idle.png"
|
||||
const TEX_HERO_WALK := "res://assets/art/character/character_hero_walk.png"
|
||||
const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(素材=原稿分辨率,显示档缩放读 standee_motion PORTRAIT_DISPLAY_H)
|
||||
const TEX_HERO_PORTRAIT := "res://assets/art/character/character_hero_portrait.png" # B 区站桩立绘·normal 态(48px 档)
|
||||
const TEX_HERO_ATTACK := "res://assets/art/character/character_hero_attack.png" # B 区站桩立绘·attack 态(三态切换)
|
||||
const TEX_HERO_HIT := "res://assets/art/character/character_hero_hit.png" # B 区站桩立绘·hit 态(三态切换)
|
||||
# 敌种立绘(B 区站桩)——文件键 = 数据表主键 enemy_id(圣经 §8.3 命名规范),
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 337 B |
@@ -196,6 +196,7 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/presentation/stage_background_test.gd", "expect": 0}, # B 区舞台背景三关变体与连续地面层(V1–V5/V8——ground_h 表单源/board_type 选图/缺图回退/地面几何/光柱退役)
|
||||
{"path": "res://tests/unit/battle_settlement/damage_math_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/damage_controller_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/battle_settlement/effect_port_test.gd", "expect": 0}, # 效果端口件(效果端口与掷格分件批 V1–V3——端口单实例/业务类零引擎直连源码锚/事实面闭集两对照)
|
||||
{"path": "res://tests/unit/battle_settlement/roll_multiplier_test.gd", "expect": 0}, # 掷点与乘区宿主(敌攻击类型轴/护甲掷点行为面/乘区接线/伤害类型克制与标错/fresh 池攻击链——批 B 自 smoke_all 迁入)
|
||||
{"path": "res://tests/unit/battle_settlement/seal_polarity_test.gd", "expect": 0}, # 封印与极性宿主(末点 AP 封印/封印不可逆四情况/清图奖励双份与弱形式/刷新 provenance 守卫/极性路由与封印溢价/负向字典/格面资源资格——批 B 自 smoke_all 迁入)
|
||||
{"path": "res://tests/unit/battle_settlement/round_flow_skill_pool_test.gd", "expect": 0}, # 轮内流与技能池宿主(全战斗回放/轮内刷新与 V5 快照单源/遭遇区间骰/结算五段全序/技能池数据面·回放独立·EV 锚·死亡打断·校验拒绝/妨害附加/四桶计数——批 B 自 smoke_all 迁入)
|
||||
|
||||
@@ -314,7 +314,7 @@ fi
|
||||
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
|
||||
# 口径仿 S1/S2 的 grep 排除手法。
|
||||
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd"
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||||
S8_MISS=0
|
||||
for f in $S8_FILES; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -358,7 +358,7 @@ fi
|
||||
if require_scan_face "S7" "$DIR_CORE"; then
|
||||
# B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件,
|
||||
# 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文)
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd"
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||||
S7_MISS=0
|
||||
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -390,7 +390,7 @@ else
|
||||
s7_scan B3 game/core/status_container.gd \
|
||||
'notify_battle_start|apply_effect|query_active|decay_round|notify_battle_end'
|
||||
s7_scan B4 game/core/reward_economy.gd \
|
||||
'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase|weighted_pick'
|
||||
'roll_board|query_identity|query_full_snapshot|grant|grant_clear|retire_mapping|polarity_row|phase'
|
||||
if [ -n "$S7_HITS" ]; then
|
||||
echo "[static_scan] S7 FAIL: B2/B3/B4 API 符号越出 B1 文件集命中 $(printf '%s' "$S7_HITS" | grep -c .) 处(编排唯一性,B1 AC-43)"
|
||||
printf '%s' "$S7_HITS"
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# tests/unit/battle_settlement/effect_port_test.gd — 效果端口件验收宿主(效果端口与掷格分件批)
|
||||
# 断言面 = 三套(需求卡片 §9 对照表行 1/2/3 载体):
|
||||
# V1 端口单实例与真实发射路由(装配链同一实例+attack_commit 经真实战斗流命中可观测)
|
||||
# V2 业务类零引擎直连(源码契约锚——两业务文件全文零 EffectEngine/EffectContext token)
|
||||
# V3 伤害域事实面闭集(合成行阳性/阴性对照——载荷携带事实可达、宿主链事实不可达)
|
||||
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
|
||||
# ConfigDB 显式实例化构造注入(各套自建 RngService——源套掷点隔离形态)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/effect_port_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const EffectEngineScript := preload("res://core/effect_engine.gd")
|
||||
|
||||
const TEST_SEED := 42 # 夹具种子(基建值非玩法数值——同种子两跑对比的隔离前提)
|
||||
const LAYER_COUNTER := "swap_charge_layers" # 整备军备蓄力层计数器(feats.tres charge_rotation 行)
|
||||
|
||||
var _db
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_db = ConfigDBScript.new()
|
||||
var load_ok: bool = _db.load_tables("res://data/tables", true)
|
||||
Kit.check("前置:表加载零 error", load_ok and _db.diagnostics_count("error") == 0)
|
||||
if load_ok:
|
||||
_smoke_effect_port_wiring()
|
||||
_test_business_classes_engine_free()
|
||||
_test_biz_fact_face_closed()
|
||||
quit(Kit.finish("effect_port_test"))
|
||||
|
||||
|
||||
# ---- 夹具 ----
|
||||
|
||||
func _enc(feats: Array) -> Dictionary:
|
||||
return {"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp),
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [],
|
||||
"battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}}
|
||||
|
||||
|
||||
func _orch(seed_v: int):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_v)
|
||||
return load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
|
||||
|
||||
func _hero_attack_value(rows: Array) -> int:
|
||||
for r in rows:
|
||||
if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "hero_attack":
|
||||
return int(r.get("value", 0))
|
||||
return -1
|
||||
|
||||
|
||||
# ---- V1 端口单实例与真实发射路由 ----
|
||||
|
||||
func _smoke_effect_port_wiring() -> void:
|
||||
print("--- 效果端口:装配链单实例+真实发射路由 ---")
|
||||
# 充能跑(整备军备在册——attack_commit 订阅行 attack_bonus per 蓄力层计数器)
|
||||
var orch_c = _orch(TEST_SEED)
|
||||
var rows_c: Array = []
|
||||
orch_c.step_row.connect(func(row): rows_c.append(row))
|
||||
Kit.check("V1 begin_battle 受理", orch_c.begin_battle(_enc(["charge_rotation"])))
|
||||
# 端口同一性(内部字段直查=受控例外:端口同一性无公开查询面,对照表行 1 注明理由)
|
||||
var port_d = orch_c.fx_host().damage._port
|
||||
var port_a = orch_c.action_economy._port
|
||||
Kit.check("V1 端口在座(damage/action_economy 双绑)", port_d != null and port_a != null)
|
||||
Kit.check("V1 端口单实例(两业务类持同一 EffectPort)", port_d == port_a)
|
||||
# 蓄力层预置 3(调试注入两层·数据面粒度——计数器账面直写,与 refresh_feats_test V8 同法)
|
||||
EffectEngineScript.counter_add(orch_c.fx_host().counters_ctx, LAYER_COUNTER, 3)
|
||||
# 最小回合:停手即轮末结算(主角先手攻击在战斗段同步兑付——零 await 同步链)
|
||||
orch_c.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var v_c := _hero_attack_value(rows_c)
|
||||
Kit.check("V1 attack_commit 命中批可观测:主角攻击行在流", v_c >= 0)
|
||||
Kit.check("V1 命中批经端口路由全处理(attack_bonus 消费行+counter_add set-0 行——层账 3→0)",
|
||||
EffectEngineScript.counter(orch_c.fx_host().counters_ctx, LAYER_COUNTER) == 0)
|
||||
# 对照跑(同种子同 enc 零预置——attack_commit 订阅行 when 不通过=零命中零扰动)
|
||||
var orch_0 = _orch(TEST_SEED)
|
||||
var rows_0: Array = []
|
||||
orch_0.step_row.connect(func(row): rows_0.append(row))
|
||||
orch_0.begin_battle(_enc(["charge_rotation"]))
|
||||
orch_0.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var v_0 := _hero_attack_value(rows_0)
|
||||
Kit.check("V1 对照跑攻击行在流", v_0 >= 0)
|
||||
Kit.check("V1 delta 读数非零(蓄力层加算进入主角攻击基值)", v_c - v_0 >= 1)
|
||||
|
||||
|
||||
# ---- V2 业务类零引擎直连(源码契约锚——theme_projection_test 源码锚惯例)----
|
||||
|
||||
func _test_business_classes_engine_free() -> void:
|
||||
print("--- 效果端口:业务类零引擎直连源码锚 ---")
|
||||
for path in ["res://core/damage_controller.gd", "res://core/action_economy.gd"]:
|
||||
var src := FileAccess.get_file_as_string(path)
|
||||
Kit.check("V2 扫描面非空前置:%s" % path, src.length() > 0)
|
||||
Kit.check("V2 全文零引擎直连 token(EffectEngine/EffectContext):%s" % path,
|
||||
not src.contains("EffectEngine") and not src.contains("EffectContext"))
|
||||
|
||||
|
||||
# ---- V3 伤害域事实面闭集(合成行两对照——damage_taken 真实单据流)----
|
||||
|
||||
func _test_biz_fact_face_closed() -> void:
|
||||
print("--- 效果端口:业务发射事实面闭集 ---")
|
||||
var orch = _orch(TEST_SEED)
|
||||
Kit.check("V3 begin_battle 受理", orch.begin_battle(_enc([])))
|
||||
# 合成行直改数据面(调试注入两层·数据面粒度):effect 段逐字抄现役铁壁 shield 表行(kind/value 零推表);
|
||||
# rebuild 走持有 id 过滤真实表行——合成行经直改表行进入装载域(订阅面换代为合成行独占,确定性)
|
||||
var pos_id := "probe_shield_pos"
|
||||
var neg_id := "probe_shield_neg"
|
||||
_db.crossrun.feats.rows.append({"feat_id": pos_id, "display_name": "探针盾甲甲", "rarity": "common",
|
||||
"desc": "V3 阳性对照(hp_ratio 载荷携带)", "triggers": [{"on": "damage_taken",
|
||||
"when": [{"fact": "hp_ratio", "op": "lte", "value": 100}], "do": {"kind": "shield", "value": 1}}]})
|
||||
_db.crossrun.feats.rows.append({"feat_id": neg_id, "display_name": "探针盾甲乙", "rarity": "common",
|
||||
"desc": "V3 阴性对照(battle_index 闭面外)", "triggers": [{"on": "damage_taken",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1}], "do": {"kind": "shield", "value": 1}}]})
|
||||
var ctrl = orch.fx_host().damage
|
||||
# 阳性对照(载荷携带事实——harness 能见命中):合成行独占订阅面(rebuild 持有 id 过滤——
|
||||
# 只挂阳性行),board_flat_hero 真实单据流,base_value 足量避开伤害下限保底
|
||||
orch.fx_host().rebuild_subscriptions([pos_id], [], [])
|
||||
var hp0 := int(ctrl.units.hero_unit().get("hp", 0))
|
||||
var t_a: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测甲",
|
||||
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
|
||||
ctrl.flush_submit(t_a)
|
||||
var hp1 := int(ctrl.units.hero_unit().get("hp", 0))
|
||||
Kit.check("V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9)", hp0 - hp1 == 9)
|
||||
# 阴性对照(闭面外事实——宿主链答而业务面不答):订阅面换代为阴性合成行独占,同路径零减量=F10 现状形态守卫
|
||||
orch.fx_host().rebuild_subscriptions([neg_id], [], [])
|
||||
var t_b: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测乙",
|
||||
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
|
||||
ctrl.flush_submit(t_b)
|
||||
var hp2 := int(ctrl.units.hero_unit().get("hp", 0))
|
||||
Kit.check("V3 阴性对照:battle_index 不答——零减量(10→10,事实面未扩)", hp1 - hp2 == 10)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b11nytp21w58b
|
||||
@@ -221,27 +221,27 @@ func _smoke_global_variant() -> void:
|
||||
Kit.check("确定性:同种子 Boss 图两遍 board_variants/变异格/取号逐位相同(region 布置路径)", _arr_str_eq(bb2.board_variant_ids(), boss1_ids) and bb2.variants == boss1_variants and crng2.variant_calls == boss1_calls)
|
||||
# ④ 资源面偏斜投影(RewardEconomy 纯函数面——守恒/白名单/构造例/合成序)
|
||||
var eco = load("res://core/reward_economy.gd").new(_db, load("res://core/rng_service.gd").new())
|
||||
var base_rows: Array = eco._rollable_rows()
|
||||
var base_sum: int = eco._pool_weight(base_rows)
|
||||
var base_rows: Array = eco.roller._rollable_rows()
|
||||
var base_sum: int = eco.roller._pool_weight(base_rows)
|
||||
Kit.check("偏斜基线:正向池现行合计 77(挖掘风险批口径)", base_sum == 77)
|
||||
eco.board_skew = 0
|
||||
var off_rows: Array = eco._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
|
||||
eco.roller.board_skew = 0
|
||||
var off_rows: Array = eco.roller._biased_cell_rows(0) # 内容数据驱动扩展批 T4:偏运权重改 reward_roll 订阅命中入参(luck_bias 注入位退役)
|
||||
Kit.check("off 态零扰动:board_skew=0 行集与基线逐位相同(不进投影域)", off_rows.size() == base_rows.size() and _rows_weights_equal(off_rows, base_rows))
|
||||
eco.board_skew = 30
|
||||
var on_rows: Array = eco._biased_cell_rows(0)
|
||||
Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco._pool_weight(on_rows) == 77)
|
||||
eco.roller.board_skew = 30
|
||||
var on_rows: Array = eco.roller._biased_cell_rows(0)
|
||||
Kit.check("合计守恒:偏斜后正向池合计 = 77(整数最大余数重归一)", eco.roller._pool_weight(on_rows) == 77)
|
||||
var wmap := _rows_weight_map(on_rows)
|
||||
Kit.check("白名单豁免:weaken/gp/action 三行偏斜后逐位不动", wmap.get("weaken", -1) == 8 and wmap.get("ap_up", -1) == 6 and wmap.get("gp_coin", -1) == 2)
|
||||
Kit.check("生存族下调:heal 4→3 / defense 11→7(v=30 构造例)", wmap.get("heal_minor", -1) == 3 and wmap.get("ac_up", -1) == 7)
|
||||
Kit.check("进攻族上浮:attack 14→15 / blast 16→18 / pierce 8→9 / magic 8→9(v=30 构造例,等比上浮守恒)", wmap.get("attack_up", -1) == 15 and wmap.get("spark_aoe", -1) == 18 and wmap.get("pierce_strike", -1) == 9 and wmap.get("magic_bolt", -1) == 9)
|
||||
eco.board_skew = 30
|
||||
var combo_rows: Array = eco._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
|
||||
eco.roller.board_skew = 30
|
||||
var combo_rows: Array = eco.roller._biased_cell_rows(2) # 件级加算 2=偏运硬币行 value(reward_roll bias_rewards 命中面同值)
|
||||
var cw := _rows_weight_map(combo_rows)
|
||||
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco._pool_weight(combo_rows) == 77 + 4 * 2)
|
||||
Kit.check("合成序:图级偏斜先(乘法面)件级 luck_bias 后(加法 +2×四稀有类)——合计 77+8", eco.roller._pool_weight(combo_rows) == 77 + 4 * 2)
|
||||
Kit.check("合成序构造例:skew(heal 3)+2=5 / gp 2+2=4 / pierce 9+2=11 / magic 9+2=11(图级定型后玩家构筑叠加)", cw.get("heal_minor", -1) == 5 and cw.get("gp_coin", -1) == 4 and cw.get("pierce_strike", -1) == 11 and cw.get("magic_bolt", -1) == 11)
|
||||
eco.board_skew = 50
|
||||
var v50: Dictionary = _rows_weight_map(eco._biased_cell_rows(0))
|
||||
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco._pool_weight(eco._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
|
||||
eco.roller.board_skew = 50
|
||||
var v50: Dictionary = _rows_weight_map(eco.roller._biased_cell_rows(0))
|
||||
Kit.check("域上界 v=50:守恒成立且逐行权重恒正(域 [0,50] 安全)", eco.roller._pool_weight(eco.roller._biased_cell_rows(0)) == 77 and int(v50.get("heal_minor", -1)) > 0)
|
||||
# wr10 偏运判定收口+注入接线(p5 缝拆)——判定面夹具白盒装载(现网 game/data/tables/ 无产出
|
||||
# bias_rewards 的数据行,先例=tree_rules_v11_test 读 _triggers);命中数前置写在消息里防空扫描面假绿。
|
||||
var wrng = load("res://core/rng_service.gd").new()
|
||||
@@ -256,13 +256,13 @@ func _smoke_global_variant() -> void:
|
||||
Kit.check("wr10①判定面:空订阅=%d / 装载后命中 %d 行 → luck_bias_for(4)=%d(期望 0/1/2;命中 0 即判据空转)" % [empty_bias, hit_rows, loaded_bias],
|
||||
empty_bias == 0 and hit_rows == 1 and loaded_bias == 2)
|
||||
Kit.check("wr10②接线面:组装点注入的查询口有效且与宿主查询口同源",
|
||||
woc._reward_economy.luck_bias_query.is_valid() and woc._reward_economy.luck_bias_query == wfx.luck_bias_for)
|
||||
woc._reward_economy.board_skew = 0
|
||||
woc._reward_economy.roller.luck_bias_query.is_valid() and woc._reward_economy.roller.luck_bias_query == wfx.luck_bias_for)
|
||||
woc._reward_economy.roller.board_skew = 0
|
||||
# 取值面防御:未接线时按「取不到偏运」计红而非崩在空回调上(撤销证明⑤态下本套件其余断言仍须跑完)
|
||||
var qb: Callable = woc._reward_economy.luck_bias_query
|
||||
var qb: Callable = woc._reward_economy.roller.luck_bias_query
|
||||
var wbias: int = int(qb.call(4)) if qb.is_valid() else -1
|
||||
Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias))],
|
||||
wbias == 2 and woc._reward_economy._pool_weight(woc._reward_economy._biased_cell_rows(wbias)) == 77 + 4 * 2)
|
||||
Kit.check("wr10③权重生效面:注入回调取回 %d → 偏运池合计 77+4*2(实测 %d)" % [wbias, woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias))],
|
||||
wbias == 2 and woc._reward_economy.roller._pool_weight(woc._reward_economy.roller._biased_cell_rows(wbias)) == 77 + 4 * 2)
|
||||
# 普通图摇号全链路确定性(off 路径)——同种子普通图资源映射逐位相同(golden 对拍另有批前快照)
|
||||
var nrng = load("res://core/rng_service.gd").new()
|
||||
nrng.set_master_seed(42)
|
||||
|
||||
Reference in new issue
Block a user