diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index eab43084..7fced528 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -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") diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 6b5428a0..cd845b0c 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -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)) # 攻击行显示名(掷点档位随行文本可读;重档折扣 = "被压制") diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index ebff39b7..d738577f 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -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)) diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index e0ea55bc..8a8fd10d 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -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 diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd index da546280..c1907485 100644 --- a/game/core/settlement/combat_segment.gd +++ b/game/core/settlement/combat_segment.gd @@ -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 diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd index 2eb25351..62e761f5 100644 --- a/game/core/settlement/grant_segment.gd +++ b/game/core/settlement/grant_segment.gd @@ -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) diff --git a/game/data/schemas/damage_roll_table.gd b/game/data/schemas/damage_roll_table.gd new file mode 100644 index 00000000..f339ea39 --- /dev/null +++ b/game/data/schemas/damage_roll_table.gd @@ -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 成员——不参与派生,仅作事件日志用途标记) diff --git a/game/data/schemas/damage_roll_table.gd.uid b/game/data/schemas/damage_roll_table.gd.uid new file mode 100644 index 00000000..11ae41fd --- /dev/null +++ b/game/data/schemas/damage_roll_table.gd.uid @@ -0,0 +1 @@ +uid://ddd6kga4tcivx diff --git a/game/data/schemas/settlement_core_table.gd b/game/data/schemas/settlement_core_table.gd new file mode 100644 index 00000000..5d7323a9 --- /dev/null +++ b/game/data/schemas/settlement_core_table.gd @@ -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 缓冲三形态),标空/挖格/清图不乘;雷安同池冻结不动 diff --git a/game/data/schemas/settlement_core_table.gd.uid b/game/data/schemas/settlement_core_table.gd.uid new file mode 100644 index 00000000..a86b0d80 --- /dev/null +++ b/game/data/schemas/settlement_core_table.gd.uid @@ -0,0 +1 @@ +uid://swk6u2opk6ix diff --git a/game/data/schemas/settlement_table.gd b/game/data/schemas/settlement_table.gd index 52191dea..efefa06b 100644 --- a/game/data/schemas/settlement_table.gd +++ b/game/data/schemas/settlement_table.gd @@ -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 diff --git a/game/data/tables/settlement.tres b/game/data/tables/settlement.tres index dd906c13..bb2cf0da 100644 --- a/game/data/tables/settlement.tres +++ b/game/data/tables/settlement.tres @@ -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,营地换型 = 改字段;局内无挖掘攒弹药的第二供给)。 diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index 91f66465..3fe1ca55 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -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 += "〕" diff --git a/game/sim/damage_probe.gd b/game/sim/damage_probe.gd index 55241b8e..ed146547 100644 --- a/game/sim/damage_probe.gd +++ b/game/sim/damage_probe.gd @@ -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" % [ diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index c4907dc4..cd1e9a4d 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -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(): diff --git a/game/tests/unit/battle_settlement/damage_math_test.gd b/game/tests/unit/battle_settlement/damage_math_test.gd index 68c9c037..24bac4be 100644 --- a/game/tests/unit/battle_settlement/damage_math_test.gd +++ b/game/tests/unit/battle_settlement/damage_math_test.gd @@ -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)) diff --git a/game/tests/unit/battle_settlement/extended_row_violation_test.gd.uid b/game/tests/unit/battle_settlement/extended_row_violation_test.gd.uid new file mode 100644 index 00000000..0341d945 --- /dev/null +++ b/game/tests/unit/battle_settlement/extended_row_violation_test.gd.uid @@ -0,0 +1 @@ +uid://ehnthaqk7h12 diff --git a/game/tests/unit/battle_settlement/roll_multiplier_test.gd b/game/tests/unit/battle_settlement/roll_multiplier_test.gd index f0c7f5f6..2edf9278 100644 --- a/game/tests/unit/battle_settlement/roll_multiplier_test.gd +++ b/game/tests/unit/battle_settlement/roll_multiplier_test.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index e854cc0d..5e188b3a 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -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: diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index 4dea2622..5ba420aa 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -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)——单板五型齐备不再必然: # 本断言多板扫描,取第一块五型齐备的板做行为断言。 diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index 55bf699e..5634ce3f 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -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 用途标签词表非空且互异(封闭语义由流改标签)", diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index c180a42c..a9f21456 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -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]) diff --git a/game/tests/unit/presentation/player_copy_sweep_test.gd b/game/tests/unit/presentation/player_copy_sweep_test.gd index 882d3d61..ce9511d9 100644 --- a/game/tests/unit/presentation/player_copy_sweep_test.gd +++ b/game/tests/unit/presentation/player_copy_sweep_test.gd @@ -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}))