卡①T4c(拆件批wr5/wr9/V12): settlement 拆两子 Resource 段——新建 settlement_core_table.gd(7列:source_label_map/effect_pool_map/pool_clamp_lo/pool_clamp_hi/round_direction/p_perfect/seal_hit_premium)+damage_roll_table.gd(10列:equivalence_ref_damage/armor_effectiveness/roll_faces/attack_bonus/roll_tier_bounds/roll_tier_discounts/crit_roll/crit_multiplier/damage_floor/armor_roll_stream)+.uid边车;主表三列形(segment_order留主表+core+damage_roll,类变量18→3,const四枚随主表),validate拆五私有(_validate_scalars/_validate_roll_scalars(9分支超阈续拆)/_validate_frozen/_validate_tier_table/_validate_armor_matrix)错误载荷逐字同构+段缺失fail-fast;settlement.tres单文件嵌套重建(两sub_resource,18列值与尾注批注史逐字—脚本比对18/18无diff+tail_equal);消费方改址97处(.settlement.<列>→.settlement.core./damage_roll. 链式95处+cross_validator局部2处,逐规则计数断言全中;segment_order直读1处零改址);测试改址9文件含G15清单(sim/probe/config_db交叉校验)。验收:gdstyle基线外0 NEW+16套件924 PASS+零SCRIPT ERROR〔拆件批 V12〕

This commit is contained in:
DKChan committed 2026-10-27 07:41:09 +08:00
1 parent bc7a171e3d
commit e2a2fbf3ee
23 files changed
+221 -160

No files matched your search

+2 -2
View File
@@ -43,13 +43,13 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
# 唯一来路=三选一共享池〔节点/关卡完成〕;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可
# 静态校验。
# ② SettlementTable.effect_pool_map 悬空
for eff in st_tab.effect_pool_map.keys():
for eff in st_tab.core.effect_pool_map.keys():
if not effect_ids.has(eff):
push_diag.call("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff)
# ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, seal_miss})
var required_sources := ["safe_cell", "seal_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "seal_miss"]
for s in required_sources:
if not st_tab.source_label_map.has(s):
if not st_tab.core.source_label_map.has(s):
push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s)
# ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验)
var en: EnemyTable = loaded.get("enemy")
+21 -21
View File
@@ -274,7 +274,7 @@ func _hero_attack_ctx(t: Dictionary) -> Dictionary:
var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array)
var ac_payload: Dictionary[Variant, Variant] = (t.context.get("fx_attack_payload", {}) as Dictionary)
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus)
var threshold: int = defender_ac(target) + int(db.settlement.damage_roll.attack_bonus)
# armor_roll 发射点(敌护甲掷骰判定;免掷恒折扣订阅面)
var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)})
# 按序改写族第一拍:矿工重锤命中交换(正负双修原生形态:命中判定减成 trade_hit 点〔更易命中〕
@@ -367,7 +367,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
var ac_hits: Array[Variant] = ctx["ac_hits"]
var roll_read: Dictionary[Variant, Variant] = ctx["roll_read"]
var atk_type: String = ctx["atk_type"]
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.crit_multiplier), int(ctx["cdelta"]))
var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.damage_roll.crit_multiplier), int(ctx["cdelta"]))
var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))}
payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型)
payload["roll"] = int(roll_read.roll)
@@ -376,7 +376,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
if bool(ctx["crit"]):
payload["crit"] = true
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense"))
var landed: int = maxi(int(db.settlement.damage_floor), pooled)
var landed: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled)
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
# (per dig_streak、cap 封顶)同通道合计;档一质变 rider+1 / 档二(crit 时)rider 乘暴击倍率。
@@ -390,7 +390,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
hero_rider += contrib
if hero_rider > 0:
if bool(ctx["crit"]) and rider_tier_crit:
hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier)))
hero_rider = int(round(float(hero_rider) * float(db.settlement.damage_roll.crit_multiplier)))
landed += hero_rider
payload["hero_rider"] = hero_rider
units.apply_damage(target, landed)
@@ -408,7 +408,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
if next_target != null:
# source_class 用中性名(fx_overflow 泛用通道,不挂件名)
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow",
"贯穿弹射", "board", maxi(int(db.settlement.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
"贯穿弹射", "board", maxi(int(db.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
DamageController.TARGET_FRONT_ALIVE, int(t.round_index), t.origin_cell, "pierce")
if policy == POLICY_SEGMENT:
_queue.append(of_t)
@@ -440,13 +440,13 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
func _resolve_enemy_skill(t: Dictionary) -> void:
var hero: Dictionary[Variant, Variant] = units.hero_unit()
var roll_read: Dictionary[Variant, Variant] = _armor_roll()
var threshold: int = defender_ac(hero) + int(db.settlement.attack_bonus)
var threshold: int = defender_ac(hero) + int(db.settlement.damage_roll.attack_bonus)
var crit: bool = bool(roll_read.crit)
var discount: int = _roll_discount(int(roll_read.roll), threshold)
var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light")))
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.crit_multiplier), cdelta)
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.settlement.damage_roll.crit_multiplier), cdelta)
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
var total: int = maxi(int(db.settlement.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
var total: int = maxi(int(db.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
# damage_taken 发射+施加(kind=enemy_skill;铁壁整备减伤订阅面:
# shield 加法族合计 value×per(charge_layers),稳健拓荒树规则同面(rebuild 合成订阅);
# 减后过下限保底。damage_reduce〔背水 hp_ratio when〕/counter_add〔保险丝/
@@ -466,7 +466,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void:
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)
if shield > 0:
total = maxi(int(db.settlement.damage_floor), total - shield)
total = maxi(int(db.settlement.damage_roll.damage_floor), total - shield)
units.apply_damage(hero, total)
var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)),
"roll": int(roll_read.roll), "ac": threshold, "tier": discount, "attack_type": String(t.attack_type)}
@@ -499,7 +499,7 @@ func _resolve_board_typed(t: Dictionary) -> void:
# 无模式 delta 才入棋盘窗(mode 路由:clamp_bonus=主角攻击面专享、discount_shift=折扣档面)
if String(h.get("kind", "")) == "modify_ticket" and h.has("delta") and String(h.get("mode", "")) == "":
cdelta += int(h.delta)
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_floor))
var dealt: int = DamageMath.counter_component(int(t.base_value), cdelta, int(db.settlement.damage_roll.damage_floor))
units.apply_damage(target, dealt)
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
"%s → %s" % [String(t.display), String(target.display_name)], dealt,
@@ -514,7 +514,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
_ineffective_row(t, "无存活敌人")
_outcomes[int(t.ticket_id)] = "zero"
return
var blast_dmg: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源
var blast_dmg: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
for u in alive:
units.apply_damage(u, blast_dmg) # 恒定表值(下限保底),逐个;受击即死——死亡即不动手
emit_row_cb.call({"type": "damage", "source_class": String(t.source_class), "origin_cell": t.origin_cell, "highlight": String(t.highlight)}, int(t.round_index),
@@ -525,7 +525,7 @@ func _resolve_board_blast(t: Dictionary) -> void:
# 棋盘主角直伤(反噬/治疗包负向/地形变异):无类型必中,下限保底;context 键并入行载荷
func _resolve_board_flat_hero(t: Dictionary) -> void:
var dealt: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源
var dealt: int = maxi(int(db.settlement.damage_roll.damage_floor), int(t.base_value)) # 伤害下限全来源
units.apply_damage(units.hero_unit(), dealt)
var extra: Dictionary[Variant, Variant] = {"target": "hero"}
for k in t.context.keys():
@@ -538,16 +538,16 @@ func _resolve_board_flat_hero(t: Dictionary) -> void:
# 护甲等级掷点(armor_roll 封闭流——逐次掷逐次取号、逐位可回放):返回 {roll, crit}
func _armor_roll() -> Dictionary:
var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.armor_roll_stream))
var roll: int = stream.randi_range(1, int(db.settlement.roll_faces))
return {"roll": roll, "crit": roll == int(db.settlement.crit_roll)}
var stream: RandomNumberGenerator = rng.open_stream(String(db.settlement.damage_roll.armor_roll_stream))
var roll: int = stream.randi_range(1, int(db.settlement.damage_roll.roll_faces))
return {"roll": roll, "crit": roll == int(db.settlement.damage_roll.crit_roll)}
# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点)
func _roll_discount(roll: int, threshold: int) -> int:
if roll == int(db.settlement.crit_roll):
if roll == int(db.settlement.damage_roll.crit_roll):
return 100
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.roll_tier_bounds, db.settlement.roll_tier_discounts)
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.damage_roll.roll_tier_bounds, db.settlement.damage_roll.roll_tier_discounts)
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
@@ -576,14 +576,14 @@ func defender_ac(unit: Dictionary) -> int:
func _counter_delta(attack_type: String, armor: String) -> int:
if attack_type == "":
return 0
var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {})
return int(row.get(armor, 0))
# 折扣档改善(猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径)
func _discount_shift_up(discount: int, shift: int) -> int:
var tbl: Array[Variant] = db.settlement.roll_tier_discounts
var tbl: Array[Variant] = db.settlement.damage_roll.roll_tier_discounts
var idx := tbl.find(discount)
if idx < 0:
return discount
@@ -597,9 +597,9 @@ func clamped_pool(pool: String) -> int:
var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id)
if row.is_empty():
continue
if String(db.settlement.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
if String(db.settlement.core.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent":
total += eff.effective_value # 池内加算(L×m 原样——越域不 clamp 由本处收口)
return clampi(total, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
return clampi(total, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_hi))
# 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制")
+1 -1
View File
@@ -329,7 +329,7 @@ func _counter_delta_of(matchup: String) -> int:
var parts := matchup.split("_vs_")
if parts.size() != 2:
return 0
var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness
var tbl: Dictionary[Variant, Variant] = db.settlement.damage_roll.armor_effectiveness
var row: Dictionary[Variant, Variant] = tbl.get(String(parts[0]), {})
return int(row.get(String(parts[1]), 0))
+2 -2
View File
@@ -196,8 +196,8 @@ func grant_routed(snap: Dictionary, polarity: String, att: Dictionary) -> bool:
# premium_pct 贯穿 _apply_percent_row 应用与呈现同乘)——标空/挖格/清图不乘,雷安同池冻结不动。
# 取整=四舍五入(与伤害管线 round_direction=floor 分域——registry clause3_inequality 登记;
# 1 点量纲行 ac_up/action 取整后不变=整数粒度地板)。
if source_class == "seal_hit" and int(db.settlement.seal_hit_premium) != 100:
var prem_pct: int = int(db.settlement.seal_hit_premium)
if source_class == "seal_hit" and int(db.settlement.core.seal_hit_premium) != 100:
var prem_pct: int = int(db.settlement.core.seal_hit_premium)
row = row.duplicate()
row["power"] = int(round(int(row.power) * prem_pct / 100.0))
row["premium_pct"] = prem_pct
+1 -1
View File
@@ -94,7 +94,7 @@ func _enemy_actions(ri: int) -> void:
var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0))))
var rider := 0
if srow.has("rider_mult"):
rider = maxi(int(oc.db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
rider = maxi(int(oc.db.settlement.damage_roll.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult))))
var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \
baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), String(u.get("attack_type", "normal")), rider)
et["source_unit"] = u
+1 -1
View File
@@ -146,7 +146,7 @@ func _reveal_mine_apply(mh_hits: Array, snap: Dictionary, cell: Vector2i, ri: in
if fx_host._fx_bonus_delta.has(axis):
fx_host._fx_bonus_delta[axis] += int(h.get("delta", 0))
"blast_scaled":
var blast_v := maxi(int(oc.db.settlement.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var blast_v := maxi(int(oc.db.settlement.damage_roll.damage_floor), int(float(int(snap.get("power", 0))) * float(h.get("mult", 1))))
var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board",
blast_v, DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "", {})
reward_grant.submit(bs_t, false)
+28
View File
@@ -0,0 +1,28 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 战斗结算表「伤害掷点与护甲」段 schema:当量参考敞口、攻击类型×护甲克制矩阵、护甲等级掷点
## 与伤害下限共 10 列纯数据载体(由 SettlementTable.damage_roll 持有;零函数零随机零 await)。
## 域校验由主表 validate 分面执行(fail-fast)。
class_name DamageRollTable
extends Resource
@export var equivalence_ref_damage: int = 12 # [1,999] knob——当量参考敞口 D_ref
# ---- 攻击类型 × 护甲克制 + 充能奖励定义(数值 provisional)----
# 主角攻击 = 普通(atk×(1+attack_up),不吃克制)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加。
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 无 damage_a_per_cell / damage_a_cap / damage_b_per_seal 与 charge_grants 字段(攻击模型=单分量)。
@export var armor_effectiveness: Dictionary[String, Variant] = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
# ---- 护甲等级掷点(数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 定稿)----
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何伤害落地全局下限 damage_floor(≥1)——
# 全来源(攻击双方/棋盘直伤/地形变异/反噬,取值域 [1,+∞) 无上限)。
# 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线),穿刺/魔法充能与棋盘来源伤害不掷不折。
# 微调旋钮 = 档位边界(roll_tier_bounds)与档位值(roll_tier_discounts),不动 AC 值(armor_class 归单位表)。
@export var roll_faces: int = 20 # [2,1000] knob——掷点骰面(d20)
@export var attack_bonus: int = 0 # [0,999] 结构位——攻击者掷点加成,切片恒 0(元进度/武器附魔未来投值)
@export var roll_tier_bounds: Array[int] = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array[int] = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25)
@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额)
@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量)
@export var damage_floor: int = 1 # [1,999] knob——任何伤害落地全局下限·全来源(无"被减伤归零 v=0"语义)
@export var armor_roll_stream: String = "armor_roll" # 掷点用途标签(rng_service PURPOSE_TAGS 成员——不参与派生,仅作事件日志用途标记)
@@ -0,0 +1 @@
uid://ddd6kga4tcivx
@@ -0,0 +1,14 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 战斗结算表「结算结构与封印溢价」段 schema:结构冻结位(段序外的取值域/池钳位/取整/效果池映射)
## 与封印溢价旋钮共 7 列纯数据载体(由 SettlementTable.core 持有;零函数零随机零 await)。
## 结构冻结位(pool clamp/效果池映射/取整)违例加载期致命(fail-fast)。
class_name SettlementCoreTable
extends Resource
@export var source_label_map: Dictionary[String, Variant] = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary[String, Variant] = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
@export var p_perfect: int = 200 # {100,200} percent——完美封印第二份兑付触发(载体域)
@export var seal_hit_premium: int = 100 # [100,999] percent knob——封印命中兑付溢价 ×(1+(n−100)/100):125=×1.25(条款3 条件二封印溢价形态 a,provisional);100=无溢价直通零扰动。只乘封印命中正向兑付(flat power/percent 幅度/gp 缓冲三形态),标空/挖格/清图不乘;雷安同池冻结不动
@@ -0,0 +1 @@
uid://swk6u2opk6ix
+71 -65
View File
@@ -1,38 +1,14 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 战斗结算表 schema:结算全局旋钮(段序/克制矩阵/掷点档位)单行全局类型化读取(垂直切片验证:完整战斗逐行归因)。
## 战斗结算表 schema:主表(段序骨架+两段子 Resource 嵌套)——结算全局旋钮(段序/克制矩阵/掷点档位)
## 单行全局类型化读取(垂直切片验证:完整战斗逐行归因)。主表只持段序骨架(3 列形):
## core=SettlementCoreTable(结算结构与封印溢价 7 列)/damage_roll=DamageRollTable(伤害掷点与护甲 10 列)。
## 结构冻结位(段序/取整/pool clamp/效果池映射)违例加载期致命(fail-fast)。
class_name SettlementTable
extends Resource
@export var equivalence_ref_damage: int = 12 # [1,999] knob——当量参考敞口 D_ref
@export var p_perfect: int = 200 # {100,200} percent——完美封印第二份兑付触发(载体域)
@export var seal_hit_premium: int = 100 # [100,999] percent knob——封印命中兑付溢价 ×(1+(n−100)/100):125=×1.25(条款3 条件二封印溢价形态 a,provisional);100=无溢价直通零扰动。只乘封印命中正向兑付(flat power/percent 幅度/gp 缓冲三形态),标空/挖格/清图不乘;雷安同池冻结不动
@export var segment_order: PackedStringArray = [] # 结构冻结——段序恒等校验违例致命
@export var source_label_map: Dictionary[String, Variant] = {} # 结构冻结——source_class→文案键
@export var effect_pool_map: Dictionary[String, Variant] = {} # 结构冻结——effect_id→pool(悬空加载致命)
@export var pool_clamp_lo: int = -100 # 结构冻结
@export var pool_clamp_hi: int = 100 # 结构冻结
@export var round_direction: String = "floor" # 结构冻结——单一取整规则
# ---- 攻击类型 × 护甲克制 + 充能奖励定义(数值 provisional)----
# 主角攻击 = 普通(atk×(1+attack_up),不吃克制)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加。
# 【两条抽象分离,勿焊死】攻击类型 = 伤害的属性轴,只决定查 armor_effectiveness 哪一行,定义里不含任何棋盘来源;
# 充能 = 按攻击类型分桶的附加伤害储备,"谁往桶里放东西"属**奖励的定义**(开格/标雷只是奖励触发源,奖励内容可改)。
# 无 damage_a_per_cell / damage_a_cap / damage_b_per_seal 与 charge_grants 字段(攻击模型=单分量)。
@export var armor_effectiveness: Dictionary[String, Variant] = {} # 克制修正矩阵(加法点数 delta):{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: 点数 [-3,+3]}}
# ---- 护甲等级掷点(数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 定稿)----
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何伤害落地全局下限 damage_floor(≥1)——
# 全来源(攻击双方/棋盘直伤/地形变异/反噬,取值域 [1,+∞) 无上限)。
# 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线),穿刺/魔法充能与棋盘来源伤害不掷不折。
# 微调旋钮 = 档位边界(roll_tier_bounds)与档位值(roll_tier_discounts),不动 AC 值(armor_class 归单位表)。
@export var roll_faces: int = 20 # [2,1000] knob——掷点骰面(d20)
@export var attack_bonus: int = 0 # [0,999] 结构位——攻击者掷点加成,切片恒 0(元进度/武器附魔未来投值)
@export var roll_tier_bounds: Array[int] = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
@export var roll_tier_discounts: Array[int] = [] # 档位值 percent 降序(与 bounds 同长;[75,50,25] = ×0.75/×0.5/×0.25)
@export var crit_roll: int = 20 # [1,roll_faces] knob——nat 最大面 = 暴击触发掷点(恒全额)
@export var crit_multiplier: int = 200 # [100,999] percent knob——暴击倍率(200 = ×2,只乘该次基础攻击分量)
@export var damage_floor: int = 1 # [1,999] knob——任何伤害落地全局下限·全来源(无"被减伤归零 v=0"语义)
@export var armor_roll_stream: String = "armor_roll" # 掷点用途标签(rng_service PURPOSE_TAGS 成员——不参与派生,仅作事件日志用途标记)
@export var segment_order: PackedStringArray = [] # 结构冻结——段序恒等校验违例致命(段序骨架留主表)
@export var core: SettlementCoreTable # 「结算结构与封印溢价」段(game/data/schemas/settlement_core_table.gd)
@export var damage_roll: DamageRollTable # 「伤害掷点与护甲」段(game/data/schemas/damage_roll_table.gd)
const ARMOR_KEYS := ["light", "ward", "heavy"]
const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"]
@@ -43,47 +19,77 @@ const P_PERFECT_DOMAIN := [100, 200]
func validate() -> Array:
var errs: Array[Dictionary] = []
if equivalence_ref_damage < 1 or equivalence_ref_damage > 999:
errs.append({"category": "domain", "message": "equivalence_ref_damage 越域 [1,999]: %d" % equivalence_ref_damage})
if not P_PERFECT_DOMAIN.has(p_perfect):
errs.append({"category": "pperfect_oob", "message": "p_perfect 载体域 {100,200}: %d" % p_perfect})
if seal_hit_premium < 100 or seal_hit_premium > 999:
errs.append({"category": "domain", "message": "seal_hit_premium 越域 [100,999](100=无溢价): %d" % seal_hit_premium})
if core == null or damage_roll == null:
errs.append({"category": "missing_segment", "message": "core/damage_roll 段缺失——settlement 表须两段嵌套(core=%s, damage_roll=%s)" % [core != null, damage_roll != null]})
return errs
_validate_scalars(errs)
_validate_roll_scalars(errs)
_validate_frozen(errs)
_validate_tier_table(errs)
_validate_armor_matrix(errs)
return errs
# 标量域(当量参考敞口/完美封印载体/封印溢价/掷点流标签)
func _validate_scalars(errs: Array[Dictionary]) -> void:
if damage_roll.equivalence_ref_damage < 1 or damage_roll.equivalence_ref_damage > 999:
errs.append({"category": "domain", "message": "equivalence_ref_damage 越域 [1,999]: %d" % damage_roll.equivalence_ref_damage})
if not P_PERFECT_DOMAIN.has(core.p_perfect):
errs.append({"category": "pperfect_oob", "message": "p_perfect 载体域 {100,200}: %d" % core.p_perfect})
if core.seal_hit_premium < 100 or core.seal_hit_premium > 999:
errs.append({"category": "domain", "message": "seal_hit_premium 越域 [100,999](100=无溢价): %d" % core.seal_hit_premium})
if String(damage_roll.armor_roll_stream) == "":
errs.append({"category": "domain", "message": "armor_roll_stream 空"})
# 掷点标量域(骰面/暴击触发与倍率/伤害下限/攻击加成)
func _validate_roll_scalars(errs: Array[Dictionary]) -> void:
var dr := damage_roll
if dr.roll_faces < 2 or dr.roll_faces > 1000:
errs.append({"category": "domain", "message": "roll_faces 越域 [2,1000]: %d" % dr.roll_faces})
if dr.crit_roll < 1 or dr.crit_roll > dr.roll_faces:
errs.append({"category": "domain", "message": "crit_roll 越域 [1,roll_faces(%d)]: %d" % [dr.roll_faces, dr.crit_roll]})
if dr.crit_multiplier < 100 or dr.crit_multiplier > 999:
errs.append({"category": "domain", "message": "crit_multiplier 越域 [100,999]: %d" % dr.crit_multiplier})
if dr.damage_floor < 1 or dr.damage_floor > 999:
errs.append({"category": "domain", "message": "damage_floor 越域 [1,999]: %d" % dr.damage_floor})
if dr.attack_bonus < 0 or dr.attack_bonus > 999:
errs.append({"category": "domain", "message": "attack_bonus 越域 [0,999]: %d" % dr.attack_bonus})
# 结构冻结(段序恒等〔主表〕/取整规则与池钳位〔core 段〕)
func _validate_frozen(errs: Array[Dictionary]) -> void:
var seg := Array(segment_order)
if seg != FROZEN_SEGMENT_ORDER:
errs.append({"category": "segorder_violation", "message": "segment_order 违例——恒等拍板段序校验失败: %s" % str(seg)})
if round_direction != "floor":
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % round_direction})
if pool_clamp_lo != -100 or pool_clamp_hi != 100:
if core.round_direction != "floor":
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % core.round_direction})
if core.pool_clamp_lo != -100 or core.pool_clamp_hi != 100:
errs.append({"category": "frozen", "message": "pool_clamp 域结构冻结 [-100,+100]"})
if roll_faces < 2 or roll_faces > 1000:
errs.append({"category": "domain", "message": "roll_faces 越域 [2,1000]: %d" % roll_faces})
if crit_roll < 1 or crit_roll > roll_faces:
errs.append({"category": "domain", "message": "crit_roll 越域 [1,roll_faces(%d)]: %d" % [roll_faces, crit_roll]})
if crit_multiplier < 100 or crit_multiplier > 999:
errs.append({"category": "domain", "message": "crit_multiplier 越域 [100,999]: %d" % crit_multiplier})
if damage_floor < 1 or damage_floor > 999:
errs.append({"category": "domain", "message": "damage_floor 越域 [1,999]: %d" % damage_floor})
if attack_bonus < 0 or attack_bonus > 999:
errs.append({"category": "domain", "message": "attack_bonus 越域 [0,999]: %d" % attack_bonus})
if String(armor_roll_stream) == "":
errs.append({"category": "domain", "message": "armor_roll_stream 空"})
# 档位表形状:bounds 升序且 ∈ [1, faces-1];discounts 降序且 ∈ [1,99](无零档——非归零);
# 档数 = 边界数 + 1(缺口 [1,b0] → discounts[0],(b_i,b_{i+1}] → discounts[i+1],> b 末值 → 末位档)
if roll_tier_bounds.is_empty() or roll_tier_discounts.size() != roll_tier_bounds.size() + 1:
errs.append({"category": "cross_field", "message": "roll_tier_bounds/discounts 形状违例(档数须 = 边界数 + 1): %d 边界 vs %d 档" % [roll_tier_bounds.size(), roll_tier_discounts.size()]})
else:
for i in roll_tier_bounds.size():
var b: int = int(roll_tier_bounds[i])
if b < 1 or b >= roll_faces or (i > 0 and b <= int(roll_tier_bounds[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_bounds[%d] 越域/非升序: %d" % [i, b]})
var d: int = int(roll_tier_discounts[i])
if d < 1 or d > 99 or (i > 0 and d >= int(roll_tier_discounts[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_discounts[%d] 越域/非降序(档位不设零档): %d" % [i, d]})
# 档位表形状:bounds 升序且 ∈ [1, faces-1];discounts 降序且 ∈ [1,99](无零档——非归零);
# 档数 = 边界数 + 1(缺口 [1,b0] → discounts[0],(b_i,b_{i+1}] → discounts[i+1],> b 末值 → 末位档)
func _validate_tier_table(errs: Array[Dictionary]) -> void:
var bounds := damage_roll.roll_tier_bounds
var discounts := damage_roll.roll_tier_discounts
if bounds.is_empty() or discounts.size() != bounds.size() + 1:
errs.append({"category": "cross_field", "message": "roll_tier_bounds/discounts 形状违例(档数须 = 边界数 + 1): %d 边界 vs %d 档" % [bounds.size(), discounts.size()]})
return
for i in bounds.size():
var b: int = int(bounds[i])
if b < 1 or b >= damage_roll.roll_faces or (i > 0 and b <= int(bounds[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_bounds[%d] 越域/非升序: %d" % [i, b]})
var d: int = int(discounts[i])
if d < 1 or d > 99 or (i > 0 and d >= int(discounts[i - 1])):
errs.append({"category": "cross_field", "message": "roll_tier_discounts[%d] 越域/非降序(档位不设零档): %d" % [i, d]})
# 克制矩阵 3×3 满格域 [-3,+3](缺项回退 -99 即越域报错——键集满格结构冻结)
func _validate_armor_matrix(errs: Array[Dictionary]) -> void:
for atk_type in ATTACK_TYPE_KEYS:
var row: Dictionary[Variant, Variant] = armor_effectiveness.get(atk_type, {})
var row: Dictionary[Variant, Variant] = damage_roll.armor_effectiveness.get(atk_type, {})
for armor in ARMOR_KEYS:
var pct: int = int(row.get(armor, -99))
if pct < -3 or pct > 3:
errs.append({"category": "domain", "message": "armor_effectiveness.%s.%s 越域 [-3,+3]: %d" % [atk_type, armor, pct]})
return errs
+17 -7
View File
@@ -1,13 +1,11 @@
[gd_resource type="Resource" script_class="SettlementTable" load_steps=2 format=3]
[gd_resource type="Resource" script_class="SettlementTable" load_steps=6 format=3]
[ext_resource type="Script" path="res://data/schemas/settlement_table.gd" id="1"]
[ext_resource type="Script" path="res://data/schemas/settlement_core_table.gd" id="2"]
[ext_resource type="Script" path="res://data/schemas/damage_roll_table.gd" id="3"]
[resource]
script = ExtResource("1")
equivalence_ref_damage = 4
p_perfect = 200
seal_hit_premium = 125
segment_order = PackedStringArray("grant", "buff_debuff", "combat", "decay", "verdict")
[sub_resource type="Resource" id="SettlementCoreTable_core"]
script = ExtResource("2")
source_label_map = {
"safe_cell": "安全格",
"seal_hit": "封印命中",
@@ -28,6 +26,12 @@ effect_pool_map = {
pool_clamp_lo = -100
pool_clamp_hi = 100
round_direction = "floor"
p_perfect = 200
seal_hit_premium = 125
[sub_resource type="Resource" id="DamageRollTable_roll"]
script = ExtResource("3")
equivalence_ref_damage = 4
armor_effectiveness = {
"normal": {
"light": 0,
@@ -54,6 +58,12 @@ crit_multiplier = 200
damage_floor = 1
armor_roll_stream = "armor_roll"
[resource]
script = ExtResource("1")
segment_order = PackedStringArray("grant", "buff_debuff", "combat", "decay", "verdict")
core = SubResource("SettlementCoreTable_core")
damage_roll = SubResource("DamageRollTable_roll")
; 攻击类型 × 护甲克制批(2026-09-12 用户拍板结构;2026-09-15 命名统一批 + 抽象分离批,数值 provisional 待用户/C3 调参):
; 主角攻击 = 单一基础分量(2026-09-19 充能退役批·用户拍板——原三分量模型退役,克制策略归冒险前配置:
; 类型 = hero 表 attack_type,营地换型 = 改字段;局内无挖掘攒弹药的第二供给)。
+2 -2
View File
@@ -133,7 +133,7 @@ func _flush_pending_agg() -> void:
func _source_label(src_key: String) -> String:
if src_key == "":
return ""
return String(_db.settlement.source_label_map.get(src_key, src_key))
return String(_db.settlement.core.source_label_map.get(src_key, src_key))
func _gold_frame(text: String) -> String:
@@ -171,7 +171,7 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
if row.get("roll", null) != null:
roll_txt = "〔掷 %d vs 护甲 %d" % [int(row.roll), int(row.ac)]
if bool(row.get("crit", false)):
roll_txt += " · 暴击×%d" % int(_db.settlement.crit_multiplier)
roll_txt += " · 暴击×%d" % int(_db.settlement.damage_roll.crit_multiplier)
elif int(row.tier) < 100:
roll_txt += " · ×%d%%" % int(row.tier)
roll_txt += "〕"
+10 -10
View File
@@ -53,17 +53,17 @@ func _initialize() -> void:
func _probe_boss(db, seed_val: int) -> void:
var math = load("res://core/damage_math.gd")
var clamp_lo: int = int(db.settlement.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.pool_clamp_hi)
var clamp_lo: int = int(db.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.core.pool_clamp_hi)
var hero_def_negative := 0
for eff_id in db.settlement.effect_pool_map.keys():
if String(db.settlement.effect_pool_map[eff_id]) != "hero_defense":
for eff_id in db.settlement.core.effect_pool_map.keys():
if String(db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
continue
var row: Dictionary = db.status_effect_row(eff_id)
if not row.is_empty() and String(row.get("unit", "")) == "percent":
hero_def_negative = mini(hero_def_negative, int(row.get("magnitude", 0)))
print("表值面:hero_defense 池最负 percent 行 %d · pool_clamp [%d, %d] · damage_floor %d · AC 基准 %d(+ac_up×%d 层)" % [
hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))])
hero_def_negative, clamp_lo, clamp_hi, int(db.settlement.damage_roll.damage_floor), int(db.hero.armor_class), int(db.status_effect_row("ac_up").get("max_stack", 0))])
print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---")
var run_a := _run_boss(db, seed_val, false)
@@ -175,11 +175,11 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
# 池归属表驱动(settlement.effect_pool_map),percent 池加算后 clamp(战斗结算 F2 同口径)
var pools := {"hero_defense": 0, "enemy_output": 0, "hero_offense": 0, "flat_attach": 0}
for eid in last_pool:
var pool_name: String = String(db.settlement.effect_pool_map.get(eid, ""))
var pool_name: String = String(db.settlement.core.effect_pool_map.get(eid, ""))
if pools.has(pool_name):
pools[pool_name] += int(last_pool[eid])
var clamp_lo: int = int(db.settlement.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.pool_clamp_hi)
var clamp_lo: int = int(db.settlement.core.pool_clamp_lo)
var clamp_hi: int = int(db.settlement.core.pool_clamp_hi)
var pd: int = clampi(int(pools["hero_defense"]), clamp_lo, clamp_hi)
var pe: int = clampi(int(pools["enemy_output"]), clamp_lo, clamp_hi)
var lines: Array = []
@@ -194,11 +194,11 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
# 掷点口径重建:基线按行留档档位打折(暴击 ×倍率),附加必中
var tier: int = int(row.get("tier", 100))
var crit: bool = bool(row.get("crit", false))
var base_factor: float = float(tier) * (float(db.settlement.crit_multiplier) / 100.0 if crit else 1.0)
var base_factor: float = float(tier) * (float(db.settlement.damage_roll.crit_multiplier) / 100.0 if crit else 1.0)
var rolled_base := int(floor(float(baseline) * base_factor / 100.0))
var raw: int = rolled_base + attach
var tick: int = int(row.get("value", 0))
var expected: int = maxi(int(db.settlement.damage_floor), math.pool_damage(raw, pd, pe))
var expected: int = maxi(int(db.settlement.damage_roll.damage_floor), math.pool_damage(raw, pd, pe))
var roll_txt := "掷%s" % str(int(row.get("roll", -1)))
var zeroed := "✓" if tick == expected else "✗ 算术不符!"
lines.append("拍=%s %s/档%d%s 基线%d→掷后%d 附加%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [
@@ -79,12 +79,12 @@ func _run_recipe_sequence(db) -> Array:
for e in units.all_enemies():
enemy_hp_before.append(int(e.hp))
var front = units.default_target()
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.settlement.attack_bonus)
var threshold_expected: int = ctrl.defender_ac(front) + int(h.db.settlement.damage_roll.attack_bonus)
var atk_type := ""
var eff_keys: Array = h.db.settlement.armor_effectiveness.keys()
var eff_keys: Array = h.db.settlement.damage_roll.armor_effectiveness.keys()
if eff_keys.size() > 0:
atk_type = String(eff_keys[0])
var floor_v: int = int(h.db.settlement.damage_floor)
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var t1: int = ctrl.submit(ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack",
"T1主角攻击", "战斗段", int(h.db.hero.atk), DamageControllerScript.TARGET_FRONT_ALIVE, 1))
@@ -123,9 +123,9 @@ func _run_recipe_sequence(db) -> Array:
var r1: Dictionary = _row_by_display_prefix("T1主角攻击")
var ex1: Dictionary = r1.get("extra", {})
Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected)
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces))
Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.damage_roll.roll_faces))
var tier_ok := false
for dv in h.db.settlement.roll_tier_discounts:
for dv in h.db.settlement.damage_roll.roll_tier_discounts:
if int(dv) == int(ex1.get("tier", -1)):
tier_ok = true
if int(ex1.get("tier", -1)) == 100:
@@ -158,7 +158,7 @@ func _run_recipe_sequence(db) -> Array:
continue
if String(r.source_class).is_empty() or String(r.display).is_empty():
protocol_ok = false
if int(r.value) < int(h.db.settlement.damage_floor):
if int(r.value) < int(h.db.settlement.damage_roll.damage_floor):
protocol_ok = false
if not (r.extra is Dictionary) or not r.extra.has("target"):
protocol_ok = false
@@ -186,7 +186,7 @@ func _test_policy_immediate_mode(db) -> void:
if not h.spawn_ok:
return
h.ctrl.policy = DamageControllerScript.POLICY_IMMEDIATE
var floor_v: int = int(h.db.settlement.damage_floor)
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit",
"A模式反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1))
Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed")
@@ -282,7 +282,7 @@ func _test_requires_gate_and_zero(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var floor_v: int = int(h.db.settlement.damage_floor)
var floor_v: int = int(h.db.settlement.damage_roll.damage_floor)
# 先清场(flush_submit 同步开单——清图批专用通道的契约面同时被覆盖)
var front_hp: int = int(h.units.query_alive("enemy")[0].max_hp)
var big: int = front_hp * 10
@@ -326,7 +326,7 @@ func _test_ac_merge(db) -> void:
return
var base_ac: int = int(h.db.hero.armor_class)
Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac)
var override_v: int = base_ac + int(h.db.settlement.damage_floor)
var override_v: int = base_ac + int(h.db.settlement.damage_roll.damage_floor)
h.ctrl.set_hero_ac_override(override_v)
Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v)
# ac 型状态效果叠其上(找 hero 侧 unit=ac 行)
@@ -351,15 +351,15 @@ func _test_pool_clamp(db) -> void:
var h = _harness(db)
if not h.spawn_ok:
return
var lo: int = int(h.db.settlement.pool_clamp_lo)
var hi: int = int(h.db.settlement.pool_clamp_hi)
var lo: int = int(h.db.settlement.core.pool_clamp_lo)
var hi: int = int(h.db.settlement.core.pool_clamp_hi)
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
# 找 hero_offense 池的 percent 型 hero 侧效果行
var pool_eff := {}
for row in h.db.status_effects.rows:
var eid := String(row.get("effect_id", ""))
if String(row.get("unit", "")) == "percent" and String(row.get("target_side", "")) == "hero" \
and String(h.db.settlement.effect_pool_map.get(eid, "")) == "hero_offense":
and String(h.db.settlement.core.effect_pool_map.get(eid, "")) == "hero_offense":
pool_eff = row
break
if pool_eff.is_empty():
@@ -24,7 +24,7 @@ func _init() -> void:
func _floor_val() -> int:
return int(db.settlement.damage_floor)
return int(db.settlement.damage_roll.damage_floor)
# ---- F2 乘区:池内加算 → 池间乘算 → floor(单一取整规则)----
@@ -32,11 +32,11 @@ func _floor_val() -> int:
func _test_pool_damage() -> void:
var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值)
Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d)
Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.core.pool_clamp_lo)) <= d)
Kit.check("正·主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_hi), 0) >= d)
Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0))
Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30))
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0)
Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.core.pool_clamp_lo), int(db.settlement.core.pool_clamp_lo)) >= 0)
# ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)----
@@ -54,8 +54,8 @@ func _test_counter_component() -> void:
func _test_roll_discount_percent() -> void:
var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表)
var bounds: Array = db.settlement.roll_tier_bounds
var discounts: Array = db.settlement.roll_tier_discounts
var bounds: Array = db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = db.settlement.damage_roll.roll_tier_discounts
Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0)
Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100
and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100)
@@ -78,7 +78,7 @@ func _test_roll_discount_percent() -> void:
func _test_rolled_component() -> void:
var base := float(_floor_val() + 7) # 表值推导探针基线
var crit_mult: int = int(db.settlement.crit_multiplier)
var crit_mult: int = int(db.settlement.damage_roll.crit_multiplier)
Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base)))
Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0)))
Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0))
@@ -0,0 +1 @@
uid://ehnthaqk7h12
@@ -109,7 +109,7 @@ func _smoke_armor_roll() -> void:
var dist := {100: 0, 75: 0, 50: 0, 25: 0}
var tier_ok := true
for roll in range(1, 21):
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts)
var d: int = DamageMath.roll_discount_percent(roll, cfg.ac, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
if not dist.has(d):
tier_ok = false
continue
@@ -117,7 +117,7 @@ func _smoke_armor_roll() -> void:
Kit.check("查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])], tier_ok and dist == cfg.expect)
var ev_sum := 0
for roll in range(1, 21):
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.roll_tier_bounds, _db.settlement.roll_tier_discounts)
ev_sum += DamageMath.roll_discount_percent(roll, 10, _db.settlement.damage_roll.roll_tier_bounds, _db.settlement.damage_roll.roll_tier_discounts)
Kit.check("AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum, ev_sum == 1575)
# ② nat20 暴击(行为面):探针按行为面扫描
# 「主角先手首掷 = 20」的种子,不假设 armor_roll scope0 位
@@ -125,12 +125,12 @@ func _smoke_armor_roll() -> void:
var crit_rows := {}
for s in range(1, 800):
crit_rows = _first_hero_attack_rows(s)
if int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll):
if int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll):
crit_seed = s
break
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)], crit_seed > 0)
Kit.check("nat20 种子可达(seed=%d——主角先手首掷命中 %d)" % [crit_seed, int(_db.settlement.damage_roll.crit_roll)], crit_seed > 0)
Kit.check("nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)",
int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false)))
int(crit_rows.get("roll", -1)) == int(_db.settlement.damage_roll.crit_roll) and bool(crit_rows.get("crit", false)))
Kit.check("nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)],
int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk))
# ③ 削弱续时(行为面):授予→衰减一轮→再授予 → 层数保持 1、剩余结算数重置
@@ -172,7 +172,7 @@ func _smoke_armor_roll() -> void:
Kit.check("棋盘来源伤害行零掷点字段(爆裂/直伤/清障炸药/奖励即时必中全额)", board_ok)
# ⑥ 全来源伤害下限(任何伤害落地取值域 [1,+∞) 无上限——棋盘克制直伤/反噬/地形变异/爆裂
# 全部收在 damage_floor 闸内)
var df: int = int(_db.settlement.damage_floor)
var df: int = int(_db.settlement.damage_roll.damage_floor)
Kit.check("棋盘克制直伤贴底 → 下限 1(点数 2 vs 不利修正 −1)", DamageMath.counter_component(2, -1, df) == 1)
Kit.check("克制修正触底穿透 → 下限 1(点数 1 vs 修正 −3)", DamageMath.counter_component(1, -3, df) == 1)
Kit.check("空桶不吃修正恒 0(零点数分量无伤害可落地——边界不变不受下限影响)", DamageMath.counter_component(0, 5, df) == 0)
@@ -351,7 +351,7 @@ func _smoke_damage_types() -> void:
Kit.check("ogre_chief 护甲 = light(Boss 行集——护甲类型重配批 2026-09-18:关卡 1 全轻甲)",
enemy_system_c.spawn(1, 4, int(_db.hero.max_hp), [], -1, -1, [], "", "boss") and String(enemy_system_c.all_enemies()[0].armor_type) == "light")
# ② 克制矩阵表值在位(三档点数 克制 +1 / 中性 0 / 被克 −1,加法核非 percent)
var am: Dictionary = _db.settlement.armor_effectiveness
var am: Dictionary = _db.settlement.damage_roll.armor_effectiveness
Kit.check("普通行:轻甲 0 / 魔法护甲 +1 / 重甲 −1(加法核)",
int(am.normal.light) == 0 and int(am.normal.ward) == 1 and int(am.normal.heavy) == -1)
Kit.check("穿刺行:轻甲 −1 / 魔法护甲 +1 / 重甲 0 中性(用户拍板)",
@@ -452,9 +452,9 @@ func _smoke_attack_chain_fresh_pool() -> void:
Kit.check("非施放攻击拍采样充足(%d 拍——增益施放行零兑付为设计语义不计)" % ticks.size(), ticks.size() >= 2)
var all_full := true
for t in ticks:
var base_factor: float = float(t.tier) * (float(_db.settlement.crit_multiplier) / 100.0 if t.crit else 1.0)
var base_factor: float = float(t.tier) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if t.crit else 1.0)
var rolled_base := int(floor(float(t.baseline) * base_factor / 100.0))
var expected: int = maxi(int(_db.settlement.damage_floor), rolled_base + int(t.get("attach", 0)))
var expected: int = maxi(int(_db.settlement.damage_roll.damage_floor), rolled_base + int(t.get("attach", 0)))
if int(t.tick) != expected or int(t.tick) <= 0:
all_full = false
Kit.check("fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线)(Boss 攻击链健康——Bug#2 回归断言)", all_full)
@@ -667,8 +667,8 @@ func _smoke_skill_pool_data() -> void:
Kit.check("蓄力状态行 empower(enemy +100 percent——敌方输出 ×2 走既有 enemy_output 池)", not empower.is_empty() and String(empower.target_side) == "enemy" and int(empower.magnitude) == 100)
Kit.check("硬化状态行 fortify(enemy −50 percent——敌方防御池负值 = 主角攻击折半;+100 符号 bug 2026-09-18 纠正,provisional 归 状态层重设计批)", not fortify.is_empty() and String(fortify.target_side) == "enemy" and int(fortify.magnitude) == -50)
Kit.check("effect_pool_map 收编 empower/fortify(敌方防御池 enemy_defense 入表)",
String(_db.settlement.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.settlement.effect_pool_map.get("fortify", "")) == "enemy_defense")
String(_db.settlement.core.effect_pool_map.get("empower", "")) == "enemy_output" \
and String(_db.settlement.core.effect_pool_map.get("fortify", "")) == "enemy_defense")
# ② 同种子回放逐位复现(D3 大地精,多实例同种)
@@ -900,7 +900,7 @@ func _smoke_debuff_rider() -> void:
var snare_seed := -1
var base_ac: int = int(_db.hero.armor_class)
var off_m: int = int(_db.status_effect_row("off_guard").magnitude)
var floor_v: int = int(_db.settlement.damage_floor)
var floor_v: int = int(_db.settlement.damage_roll.damage_floor)
for s in range(1, 401):
if venom_seed > 0 and snare_seed > 0:
break
@@ -932,7 +932,7 @@ func _smoke_debuff_rider() -> void:
if rider_v != maxi(floor_v, int(floor(float(boss_atk) * 0.75))):
venom_check_ok = false
# 终值精确复算(停手轮池恒空):主分量链结果 + 必中附加后置
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var base_factor: float = float(int(row.tier)) * (float(_db.settlement.damage_roll.crit_multiplier) / 100.0 if bool(row.get("crit", false)) else 1.0)
var rolled_main: int = int(floor(float(int(row.baseline)) * base_factor / 100.0))
var expected: int = maxi(floor_v, rolled_main + int(row.get("attach", 0))) + rider_v
if int(row.value) != expected:
@@ -494,7 +494,7 @@ func _smoke_polarity_routing() -> void:
# δ 读 settlement.seal_hit_premium(125 provisional);九型正向行值全固定(唯一区间行 clear_blast
# 系清图槽不入格池)⇒ 逐型期望全确定。整数量级四舍五入域注记:ac_up/action 1 点量纲取整后不变
# =整数粒度地板——等号压线判读在当量聚合面(audit 工具/registry clause3_inequality),非断言面。
var prem_pct: int = int(_db.settlement.seal_hit_premium)
var prem_pct: int = int(_db.settlement.core.seal_hit_premium)
Kit.check("封印溢价配置在位(seal_hit_premium=125 provisional——形态 a 兑付溢价)", prem_pct == 125)
var prem_rows := 0
var prem_ok := true
@@ -676,7 +676,7 @@ func _smoke_negative_dictionary() -> void:
not sapped.is_empty() and String(sapped.get("target_side", "")) == "hero" \
and int(sapped.get("magnitude", 0)) == -25 and int(sapped.get("duration_settlements", 0)) == 1 \
and int(sapped.get("max_stack", 0)) == 16 and String(sapped.get("stack_policy", "")) == "stack")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.effect_pool_map.get("sapped", "")) == "hero_offense")
Kit.check("effect_pool_map 收编 sapped → hero_offense(attack_up 镜像池)", String(_db.settlement.core.effect_pool_map.get("sapped", "")) == "hero_offense")
# 行为面:逐型负向兑付形态(真实板 + 真实路由路径)
# heal 权重低(无 heal 板概率 ≈1/3)——单板五型齐备不再必然:
# 本断言多板扫描,取第一块五型齐备的板做行为断言。
@@ -60,7 +60,7 @@ func _test_damage_controller_vocabularies() -> void:
# ---- D2↔B1:掷点用途标签 ∈ RngService 封闭词表(EC12——跨系统字面量契约的成员性断言) ----
func _test_rng_tag_closure(db) -> void:
var stream_label: String = String(db.settlement.armor_roll_stream)
var stream_label: String = String(db.settlement.damage_roll.armor_roll_stream)
Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)",
RngServiceScript.PURPOSE_TAGS.has(stream_label))
Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)",
@@ -282,25 +282,25 @@ func _smoke_ac_swap_guard() -> void:
Kit.check("奖励表 defense 正向行 effect_id = ac_up(显示名盾牌加护,权重/tier 不变)",
String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "盾牌加护")
Kit.check("effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)",
not _db.settlement.effect_pool_map.has("defense_up") and not _db.settlement.effect_pool_map.has("ac_up"))
not _db.settlement.core.effect_pool_map.has("defense_up") and not _db.settlement.core.effect_pool_map.has("ac_up"))
# ② 免伤墙归零护栏(泛化——BUG-0003 免疫)
var wall_ok := true
var wall_detail := ""
for eff_id in _db.settlement.effect_pool_map.keys():
if String(_db.settlement.effect_pool_map[eff_id]) != "hero_defense":
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) != "hero_defense":
continue
var row: Dictionary = _db.status_effect_row(eff_id)
if row.is_empty() or String(row.get("unit", "")) != "percent":
continue
var floor_v: int = int(row.get("magnitude", 0)) * int(row.get("max_stack", 1))
if floor_v <= int(_db.settlement.pool_clamp_lo):
if floor_v <= int(_db.settlement.core.pool_clamp_lo):
wall_ok = false
wall_detail += " %s(%d)" % [eff_id, floor_v]
Kit.check("hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)], wall_ok)
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.core.pool_clamp_lo)], wall_ok)
var hd_maps := 0
for eff_id in _db.settlement.effect_pool_map.keys():
if String(_db.settlement.effect_pool_map[eff_id]) == "hero_defense":
for eff_id in _db.settlement.core.effect_pool_map.keys():
if String(_db.settlement.core.effect_pool_map[eff_id]) == "hero_defense":
hd_maps += 1
Kit.check("hero_defense 池映射数==0(绊线钉——红=已加映射,去核上方护栏是否已同步激活)", hd_maps == 0)
# ③ 单位表 armor_class:域内校验(配置推导——正式投值归怪物/难度批,改表不断锚)
@@ -370,12 +370,12 @@ func _smoke_ac_swap_guard() -> void:
Kit.check("第一关相邻深度带总量比 ≤ 2.5(12/10/22/35/72——迭代 24 N1 曲线护栏,带键升序;越带%s)" % [
curve_detail if curve_detail != "" else "无"], curve_ok)
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
var bounds: Array = _db.settlement.roll_tier_bounds
var discounts: Array = _db.settlement.roll_tier_discounts
var bounds: Array = _db.settlement.damage_roll.roll_tier_bounds
var discounts: Array = _db.settlement.damage_roll.roll_tier_discounts
Kit.check("掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)",
int(_db.settlement.roll_faces) == 20 and int(_db.settlement.crit_roll) == 20 \
and int(_db.settlement.crit_multiplier) == 200 and int(_db.settlement.damage_floor) == 1 \
and int(_db.settlement.attack_bonus) == 0 and String(_db.settlement.armor_roll_stream) == "armor_roll")
int(_db.settlement.damage_roll.roll_faces) == 20 and int(_db.settlement.damage_roll.crit_roll) == 20 \
and int(_db.settlement.damage_roll.crit_multiplier) == 200 and int(_db.settlement.damage_roll.damage_floor) == 1 \
and int(_db.settlement.damage_roll.attack_bonus) == 0 and String(_db.settlement.damage_roll.armor_roll_stream) == "armor_roll")
Kit.check("档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)",
bounds == [3, 7] and discounts == [75, 50, 25])
@@ -96,7 +96,7 @@ func _sweep_source_names() -> void:
row_view._tip_label.text.contains("来源:危险格"))
row_view._hide_tip()
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
String(_db.settlement.source_label_map.get("mine_hit", "")) == "危险格")
String(_db.settlement.core.source_label_map.get("mine_hit", "")) == "危险格")
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db)
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))