feat(slice): 护甲等级掷点批收编——d20 判定式+defense_up→ac_up 换装+armor_roll 流(冒烟 233 项全绿)
- 判定式:1d20 ≥ AC+attack_bonus 全额,低掷三档折扣,nat20 暴击×2,下限 max(1,·) - 换装:defense_up 退役 → ac_up(+1 AC/层×2);hero AC10 / 敌方 0 暂定 - rng_service 增 armor_roll 封闭流;settlement.tres 七掷点参数 - 文档:战斗结算 v0.10/敌人系统 v0.4/难度曲线与Boss v0.5/增益状态 v0.4/地图战斗资源 v0.6 + registry + 06-待办清单 - EV 口径注记:AC12 按判定式 28.75%(提案 32.5% 不符,按规范登记) - 顺手清偿 supply_scan.gd 遗留 .uid
This commit is contained in:
1 parent
1c63830317
commit
131d1047e1
39 files changed
+1461
-182
No files matched your search
@@ -4,7 +4,8 @@
|
||||
# 战斗结算编排中枢:轮次生命周期唯一 owner · 五段结算(授予→增益/减益→战斗→衰减→判定——2026-09-15
|
||||
# 格面资源批,道具窗口/道具段随可选类删除下线)· 授予段极性路由(「动作 × 格型」矩阵:开安全格/标中旗
|
||||
# = 正向,开雷格/标错旗 = 负向;结算队列 + 抵消过滤——标旗后被撤旗以轮末在场旗为基线不判)·
|
||||
# 10 类行协议可回放步骤流(零随机零 await)· 高光双落(完美排雷 ×2 / 标中旗 highlight)·
|
||||
# 10 类行协议可回放步骤流(零 await;随机消费 = 开战遭遇骰初始化随机 + 护甲等级掷点回合内随机——
|
||||
# 均走封闭命名流,2026-09-16 护甲等级掷点批)· 高光双落(完美排雷 ×2 / 标中旗 highlight)·
|
||||
# Waiting 相位结算触发(不依赖事件到达序)。
|
||||
# 切片登记:① 事件总线载体 = 直连信号(实现期 ADR 项,provisional)② Waiting 相位刷新通道 2026-09-15 用户拍板整体
|
||||
# 拆除(仅 Active 挖掘期受理)③ 效果行协议 = 授予段逐 grant 行 + 增益/减益段续存实例组合行。
|
||||
@@ -402,32 +403,40 @@ func _combat_segment(ri: int) -> void:
|
||||
# ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动;授予段负向直伤致死 → 致电短路不先手)。
|
||||
# 三段伤害构成(2026-09-12 用户拍板;2026-09-15 命名统一批 + 攻击类型/奖励抽象分离批):普通(atk×(1+attack_up),不吃克制)
|
||||
# + 穿刺/魔法(由充能供给——充能按攻击类型分桶,谁能充/充哪个桶归奖励定义 charge_grants)。攻击类型只管查甲效矩阵哪一行,与来源无关。
|
||||
# 护甲等级掷点批(2026-09-16):普通分量吃掷点判定(含暴击);穿刺/魔法充能分量与棋盘来源伤害不掷不折;
|
||||
# 任何攻击落地伤害 ≥ damage_floor(全局 max(1,·))——"被减伤归零 v=0"零值伤害行分支随批退役。
|
||||
var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0
|
||||
var hero_target = _enemy_system.default_target()
|
||||
if hero_alive and hero_target != null:
|
||||
var armor: String = String(hero_target.get("armor_type", "light"))
|
||||
var up := clampi(_pool_effective("attack_up"), int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_hi))
|
||||
var points := _charge_points() # {攻击类型: 充能点数}——奖励定义投递的结果(本段不认识"开格/标雷")
|
||||
var roll_read: Dictionary = _armor_roll() # 普通分量掷点(每吃掷点分量逐次掷)
|
||||
var threshold: int = _defender_ac(hero_target) + int(db.settlement.attack_bonus)
|
||||
var crit: bool = bool(roll_read.crit)
|
||||
var discount: int = _roll_discount(int(roll_read.roll), threshold)
|
||||
var base_val: float = float(_hero_atk) * (100.0 + float(up)) / 100.0
|
||||
var comp: Dictionary = {}
|
||||
var d_hero := 0
|
||||
for atk_type in SettlementTable.ATTACK_TYPE_KEYS:
|
||||
var c: int = 0
|
||||
if atk_type == "normal":
|
||||
c = maxi(0, int(floor(float(_hero_atk) * (100.0 + float(up)) / 100.0))) # 普通分量走攻击力基线:吃增益、不吃克制
|
||||
c = DamageMath.rolled_component(base_val, discount, crit, int(db.settlement.crit_multiplier), _armor_effectiveness("normal", armor))
|
||||
else:
|
||||
c = _charge_component(int(points.get(atk_type, 0)), atk_type, armor)
|
||||
comp[atk_type] = c
|
||||
d_hero += c
|
||||
var payload: Dictionary = {"target": hero_target.display_name}
|
||||
payload.merge(comp)
|
||||
if d_hero > 0:
|
||||
_enemy_system.apply_damage(hero_target, d_hero)
|
||||
payload["killed"] = not hero_target.alive # 击杀标记在扣血后取(原语义不变)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击", d_hero, payload)
|
||||
else:
|
||||
# 零值伤害行(战斗结算 边界表:"合成后为 0 时不调扣血接口,改写零值伤害行——免伤与零输出必须可见";
|
||||
# 敌方侧同款口径已就绪——照抄。分量字段齐备保 P2 归因不断链)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, "主角攻击(被减伤归零)", 0, payload)
|
||||
payload["roll"] = int(roll_read.roll)
|
||||
payload["ac"] = threshold
|
||||
payload["tier"] = discount
|
||||
if crit:
|
||||
payload["crit"] = true
|
||||
var d_landed: int = maxi(int(db.settlement.damage_floor), d_hero)
|
||||
_enemy_system.apply_damage(hero_target, d_landed)
|
||||
payload["killed"] = not hero_target.alive # 击杀标记在扣血后取(原语义不变)
|
||||
_emit_row("damage", "hero_attack", Vector2i(-1, -1), ri, _attack_label("主角攻击", crit, discount), d_landed, payload)
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
if int(_enemy_system.hero_unit().hp) <= 0:
|
||||
break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板)
|
||||
@@ -436,15 +445,59 @@ func _combat_segment(ri: int) -> void:
|
||||
if beat == "charge":
|
||||
_emit_row("charge", "enemy_action", Vector2i(-1, -1), ri, "%s 蓄力(本轮踩雷白喂)" % u.display_name, 0, {"target": "hero"})
|
||||
else:
|
||||
var d_tick := _resolve_damage(int(atk.total))
|
||||
if d_tick <= 0:
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击(被减伤归零)" % u.display_name, 0, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
else:
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, "%s 攻击" % u.display_name, d_tick, {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach)})
|
||||
# 基线部分吃掷点判定(含暴击)+ 伏击附加(必中全额)→ 合计 → 增益池乘区 → max(1, ·)(伤害下限)
|
||||
var roll_read_e: Dictionary = _armor_roll()
|
||||
var threshold_e: int = _defender_ac(_enemy_system.hero_unit()) + int(db.settlement.attack_bonus)
|
||||
var crit_e: bool = bool(roll_read_e.crit)
|
||||
var discount_e: int = _roll_discount(int(roll_read_e.roll), threshold_e)
|
||||
var rolled_base: int = DamageMath.rolled_component(float(int(atk.baseline)), discount_e, crit_e, int(db.settlement.crit_multiplier), 100)
|
||||
var d_tick := _resolve_damage(rolled_base + int(atk.attach))
|
||||
d_tick = maxi(int(db.settlement.damage_floor), d_tick)
|
||||
_enemy_system.apply_damage(_enemy_system.hero_unit(), d_tick) # 调用序②受击上报
|
||||
var payload_e: Dictionary = {"target": "hero", "baseline": int(atk.baseline), "attach": int(atk.attach),
|
||||
"roll": int(roll_read_e.roll), "ac": threshold_e, "tier": discount_e}
|
||||
if crit_e:
|
||||
payload_e["crit"] = true
|
||||
_emit_row("damage", "enemy_action", Vector2i(-1, -1), ri, _attack_label("%s 攻击" % u.display_name, crit_e, discount_e), d_tick, payload_e)
|
||||
_enemy_system.advance(u) # 调用序③推进——每结算每存活敌恰一次(hero 不在推进域)
|
||||
|
||||
|
||||
# 攻击行显示名(掷点档位随行文本可读——2026-09-16 护甲等级掷点批;零值行语义已退役,重档折扣 = "被压制")
|
||||
func _attack_label(base: String, crit: bool, discount: int) -> String:
|
||||
if crit:
|
||||
return base + "(暴击)"
|
||||
if discount < 100:
|
||||
return base + "(被压制)"
|
||||
return base
|
||||
|
||||
|
||||
# 护甲等级掷点(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)}
|
||||
|
||||
|
||||
# 掷点折扣读数:nat 最大面恒全额(暴击分支前置),其余查档(缺口 = 阈值 − 掷点)
|
||||
func _roll_discount(roll: int, threshold: int) -> int:
|
||||
if roll == int(db.settlement.crit_roll):
|
||||
return 100
|
||||
return DamageMath.roll_discount_percent(roll, threshold, db.settlement.roll_tier_bounds, db.settlement.roll_tier_discounts)
|
||||
|
||||
|
||||
# 防守方护甲等级读数(数值轴——与 armor_type 克制属性轴严格分离):敌人 = spawn 期行值快照;
|
||||
# 主角 = 表值基准 + ac 型效果加算(ac_up 层数×幅度——不入任何池,无 clamp)
|
||||
func _defender_ac(unit: Dictionary) -> int:
|
||||
if bool(unit.get("is_hero", false)):
|
||||
var total: int = int(db.hero.armor_class)
|
||||
for eff in _status_effects.query_active("hero"):
|
||||
var row: Dictionary = db.status_effect_row(eff.effect_id)
|
||||
if not row.is_empty() and String(row.unit) == "ac":
|
||||
total += int(eff.effective_value)
|
||||
return total
|
||||
return int(unit.get("armor_class", 0))
|
||||
|
||||
|
||||
# F2 乘区:池内加算 → clamp [-100,+100] → 池间乘算 → floor(单一取整规则)——算术归 DamageMath(O3)
|
||||
func _resolve_damage(d_raw: int) -> int:
|
||||
var p_hero := _clamped_pool("hero_defense")
|
||||
@@ -516,7 +569,7 @@ func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
var enemies: Array = []
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "light"))})
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "beat": query_beat_of(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))})
|
||||
var buffs: Array = []
|
||||
for eff in _status_effects.query_active("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
@@ -543,7 +596,7 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
"verdict": verdict,
|
||||
"unit_count": enemies.size(),
|
||||
"spawn_count": _enemy_system.all_enemies().size(), # 本战斗实例化总数(含死亡残位)——unit_count=0 时区分「首轮全灭」vs「空场」(playtest-2026-09-12 报告 Bug#1 误读教训)
|
||||
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0))},
|
||||
"hero": {"hp": int(_enemy_system.hero_unit().get("hp", 0)), "max_hp": int(_enemy_system.hero_unit().get("max_hp", 0)), "atk": int(_enemy_system.hero_unit().get("atk", 0)), "armor_class": _defender_ac(_enemy_system.hero_unit())},
|
||||
"economy": {"phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "refresh_used": _refresh_used_this_round},
|
||||
"board": board_stats,
|
||||
"enemies": enemies,
|
||||
@@ -615,8 +668,8 @@ func _battle_over(result: String) -> void:
|
||||
# ---- 呈现状态面(E1 封装收口 2026-09-13):呈现层 / headless 自动玩家的唯一读取出口——
|
||||
# 协作对象(_enemy_system/_status_effects/_reward_economy)私有化后,跨层读取一律经本快照或下列命名面(diagnostic_snapshot 同款模式,
|
||||
# 禁再链式穿透 settlement.enemy_system.hero 一类路径)。载荷 schema 成文(code-conventions 2.1):
|
||||
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive} /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,beat,ring_size,ring_index,ambush_layers,alive}] /
|
||||
# round / battle_result / phase / ap / rap / can_request_refresh / hero{hp,max_hp,atk,alive,armor_class} /
|
||||
# enemies[{order,display_name,hp,max_hp,enemy_id,armor_type,armor_class,beat,ring_size,ring_index,ambush_layers,alive}] /
|
||||
# ambush_layers / hero_effects[](增益状态 query_active 投影)
|
||||
func presentation_snapshot() -> Dictionary:
|
||||
var enemies: Array = []
|
||||
@@ -632,6 +685,7 @@ func presentation_snapshot() -> Dictionary:
|
||||
"hp": int(u.hp), "max_hp": int(u.max_hp),
|
||||
"enemy_id": String(u.enemy_id),
|
||||
"armor_type": String(u.get("armor_type", "light")),
|
||||
"armor_class": int(u.get("armor_class", 0)),
|
||||
"beat": String(_enemy_system.query_beat(u)) if alive else "",
|
||||
"ring_size": ring.size(),
|
||||
"ring_index": int(u.get("steps", 0)) % maxi(ring.size(), 1),
|
||||
@@ -641,7 +695,7 @@ func presentation_snapshot() -> Dictionary:
|
||||
var hero := {}
|
||||
var h: Dictionary = _enemy_system.hero_unit()
|
||||
if not h.is_empty():
|
||||
hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false))}
|
||||
hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false)), "armor_class": _defender_ac(h)}
|
||||
return {
|
||||
"round": round_index,
|
||||
"battle_result": battle_result,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# 实现 GDD:design/archive/gdd-2026-09-14/b1-settlement.md(系统 ID B1·战斗结算——registry 追溯锚,命名规范见 CLAUDE.md)
|
||||
# 战斗结算·战斗段伤害公式族(零状态纯算术宿主——规范 1.2「工具性纯函数挂明确宿主类」;
|
||||
# 自 battle_orchestrator 拆出 2026-09-13,封装审查 O3)。池读取与表值读取仍归 战斗结算,本类只做算术。
|
||||
# 护甲等级掷点批(2026-09-16):掷点查档与吃掷点分量算术收编本类(判定式参数化——表值零字面量纪律不变)。
|
||||
class_name DamageMath
|
||||
extends RefCounted
|
||||
|
||||
@@ -16,3 +17,27 @@ static func pool_damage(d_raw: int, p_hero: int, p_enemy: int) -> int:
|
||||
# 充能分量:充能点数(调用方已按奖励定义换算点数、并按源套单轮计数上限)× 克制 percent → floor
|
||||
static func charge_component(points: int, effectiveness_percent: int) -> int:
|
||||
return int(floor(float(points) * float(effectiveness_percent) / 100.0))
|
||||
|
||||
|
||||
# ---- 护甲等级掷点批(2026-09-16 用户拍板——判定式与档位参数全在 settlement.tres,本类只做算术)----
|
||||
|
||||
# 掷点折扣查档(纯函数):掷点 ≥ 阈值 → 100 全额;低掷按缺口(阈值 − 掷点)查升序边界落档,
|
||||
# 返回档位值 percent(档数 = 边界数 + 1:缺口 ∈ [1, bounds[0]] → discounts[0],(bounds[i], bounds[i+1]] →
|
||||
# discounts[i+1],> bounds 末值 → discounts 末位——档位不设零档,非归零)。nat 最大面的"恒全额"由调用方先行分支。
|
||||
static func roll_discount_percent(roll: int, threshold: int, bounds: Array, discounts: Array) -> int:
|
||||
if roll >= threshold:
|
||||
return 100
|
||||
var tier := 0
|
||||
for i in bounds.size():
|
||||
if threshold - roll > int(bounds[i]):
|
||||
tier = i + 1
|
||||
return int(discounts[tier])
|
||||
|
||||
|
||||
# 吃掷点分量算术(纯函数):基线值 × 掷点折扣 ×(暴击时)暴击倍率 × 克制 percent → floor(单一取整;
|
||||
# 乘法交换——折扣/暴击/克制次序无关,取整链保持现登记:每分量恰一次 floor)。暴击恒全额由 discount=100 承载。
|
||||
static func rolled_component(base_value: float, discount_percent: int, crit: bool, crit_multiplier: int, effectiveness_percent: int) -> int:
|
||||
var v: float = base_value * float(discount_percent) / 100.0
|
||||
if crit:
|
||||
v *= float(crit_multiplier) / 100.0
|
||||
return int(floor(v * float(effectiveness_percent) / 100.0))
|
||||
@@ -7,13 +7,14 @@
|
||||
# 取号语义:全新实例首个 open_stream = scope 0(地图战斗资源 预推进术语的登记口径)。
|
||||
extends Node
|
||||
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter"] # 2026-09-12 增 encounter(遭遇随机化批,用户拍板——registry rng_service 条目同步)
|
||||
const STREAM_NAMES := ["board_gen", "reward_roll", "encounter", "armor_roll"] # 2026-09-12 增 encounter(遭遇随机化批);2026-09-16 增 armor_roll(护甲等级掷点批,用户拍板——回合内随机、逐次掷逐次取号,registry rng_service 条目同步)
|
||||
|
||||
# 分段表达式书写(数据配置 R2 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
const SALT_BOARD_GEN := (0xA0761D64 << 32) | 0x78BD642F # 0xA0761D6478BD642F
|
||||
const SALT_REWARD_ROLL := (0xE7037ED1 << 32) | 0xA0B428DB # 0xE7037ED1A0B428DB
|
||||
const SALT_ENCOUNTER := (0x8EBC6AF0 << 32) | 0x9C88C6E3 # 0x8EBC6AF09C88C6E3(遭遇骰——registry stream_seed_derivation 权威值;旧值 0x5DEECE66D<<32 超 int64 被截断,黄金向量漂移)
|
||||
const SALT_ARMOR_ROLL := (0xC2B2AE3D << 32) | 0x27D4EB2F # 0xC2B2AE3D27D4EB2F(护甲掷点流——互异奇常数 ≠ 乘数;黄金向量见冒烟/registry 同批登记)
|
||||
const OUT_MASK := 0x7FFFFFFFFFFFFFFF
|
||||
const INT64_MIN := -9223372036854775808
|
||||
|
||||
@@ -22,7 +23,7 @@ const INT64_MIN := -9223372036854775808
|
||||
var _master_seed: int = -1 # -1 = 未播种(取数即致命,EC7)
|
||||
var master_seed: int:
|
||||
get: return _master_seed
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0}
|
||||
var _counters := {"board_gen": 0, "reward_roll": 0, "encounter": 0, "armor_roll": 0}
|
||||
|
||||
|
||||
func _salt_for(stream: String) -> int:
|
||||
@@ -33,6 +34,8 @@ func _salt_for(stream: String) -> int:
|
||||
return SALT_REWARD_ROLL
|
||||
"encounter":
|
||||
return SALT_ENCOUNTER
|
||||
"armor_roll":
|
||||
return SALT_ARMOR_ROLL
|
||||
push_error("[RngService] 未知名流: %s" % stream)
|
||||
assert(false, "未知名流(流集合封闭,EC12)")
|
||||
return 0
|
||||
@@ -54,7 +57,7 @@ func set_master_seed(seed_val: int) -> void:
|
||||
if seed_val < 0:
|
||||
seed_val = -seed_val
|
||||
push_warning("[RngService] master_seed 负输入 abs 归一化: %d" % seed_val)
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
assert(_counters["board_gen"] == 0 and _counters["reward_roll"] == 0 and _counters["encounter"] == 0 and _counters["armor_roll"] == 0, "冒险中途重播种会击穿同冒险无碰撞前提(仅许全部流计数器全零时调用)")
|
||||
_master_seed = seed_val
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
@@ -65,6 +68,7 @@ func new_adventure(seed_val: int) -> void:
|
||||
_counters["board_gen"] = 0
|
||||
_counters["reward_roll"] = 0
|
||||
_counters["encounter"] = 0
|
||||
_counters["armor_roll"] = 0
|
||||
set_master_seed(seed_val)
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override:
|
||||
"pattern": Array(row.pattern), # 行值快照(拷贝)
|
||||
"advantage_per_round_cap": int(row.advantage_per_round_cap),
|
||||
"armor_type": String(row.get("armor_type", "light")), # 护甲类型快照(克制矩阵消费面——战斗结算·战斗段;2026-09-15 命名统一批:light/ward/heavy)
|
||||
"armor_class": int(row.get("armor_class", 0)), # 护甲等级快照(掷点判定阈值基数——战斗结算·战斗段;2026-09-16 护甲等级掷点批)
|
||||
"steps": 0,
|
||||
"alive": true,
|
||||
"is_hero": false,
|
||||
|
||||
@@ -82,6 +82,10 @@ func validate() -> Array:
|
||||
var at: String = String(row.get("armor_type", "light"))
|
||||
if not ARMOR_TYPES.has(at):
|
||||
errs.append({"category": "enum", "message": "%s armor_type 未知枚举: %s(域 {light, ward, heavy})" % [eid, at]})
|
||||
# armor_class(可省略——缺省 0 向后兼容;2026-09-16 护甲等级掷点批:防守方掷点判定阈值基数)
|
||||
var ac: int = int(row.get("armor_class", 0))
|
||||
if ac < 0 or ac > 999:
|
||||
errs.append({"category": "domain", "message": "%s armor_class 越域 [0,999]: %d" % [eid, ac]})
|
||||
# 每行集恰一敌种(行集内 enemy_id 唯一)
|
||||
var rk := "%d_%d" % [L, D]
|
||||
if rowset_species.has(rk):
|
||||
|
||||
@@ -8,6 +8,7 @@ extends Resource
|
||||
|
||||
@export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚)
|
||||
@export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击
|
||||
@export var armor_class: int = 10 # [0,999] knob——防守方护甲等级基准(2026-09-16 护甲等级掷点批,暂定 10 归 难度曲线与Boss);消费面 = 战斗结算掷点判定阈值基数(ac_up 效果加算其上)
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
@@ -16,4 +17,6 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "max_hp 越域 [1,999]: %d" % max_hp})
|
||||
if atk < 1 or atk > 999:
|
||||
errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk})
|
||||
if armor_class < 0 or armor_class > 999:
|
||||
errs.append({"category": "domain", "message": "armor_class 越域 [0,999]: %d" % armor_class})
|
||||
return errs
|
||||
@@ -1,7 +1,7 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 玩家能否无指导在一场完整战斗中经历「开战→挖掘规划→轮末结算逐行兑现→胜/负」并逐行归因(P2)
|
||||
# Date: 2026-09-10
|
||||
# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径。
|
||||
# 数据配置 SettlementTable schema(战斗结算 回填值;单行全局,Rule 4 豁免)——八字段口径 + 克制/充能批 + 护甲等级掷点批。
|
||||
# 三行致命 manifest:segorder_violation / poolmap_dangling / pperfect_oob。
|
||||
class_name SettlementTable
|
||||
extends Resource
|
||||
@@ -21,6 +21,19 @@ extends Resource
|
||||
# 原 damage_a_per_cell / damage_a_cap / damage_b_per_flag 三字段即"类型×来源"焊死的产物,已由 charge_grants 取代。
|
||||
@export var charge_grants: Dictionary = {} # 奖励侧定义:触发源 → {attack_type(投哪个桶), per_unit(每单位点数), cap(单轮计数上限,0 = 不封顶)}
|
||||
@export var armor_effectiveness: Dictionary = {} # 克制矩阵 percent:{攻击类型 {normal|pierce|magic}: {护甲 {light|ward|heavy}: percent}}
|
||||
# ---- 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——registry settlement_table.ac_roll/ac_discount_tiers 同批定稿)----
|
||||
# 判定式:掷 1d20,掷点 ≥ 防守方 AC + 攻击加成 → 全额;低掷按缺口(AC−掷点)查档打折,无 miss、无零收益档;
|
||||
# nat 最大面(crit_roll)= 暴击:该次基础攻击分量 ×2 且恒全额;任何攻击落地伤害全局下限 damage_floor(≥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 = [] # 缺口档边界升序:缺口 ≥ bounds[i] 落入第 i 档([3,7] = 缺口1-3/4-7/8+ 三档)
|
||||
@export var roll_tier_discounts: Array = [] # 档位值 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 STREAM_NAMES 成员;逐次掷逐次取号,可逐位回放)
|
||||
|
||||
const ARMOR_KEYS := ["light", "ward", "heavy"]
|
||||
const ATTACK_TYPE_KEYS := ["normal", "pierce", "magic"]
|
||||
@@ -42,6 +55,30 @@ func validate() -> Array:
|
||||
errs.append({"category": "frozen", "message": "round_direction 结构冻结 floor: %s" % round_direction})
|
||||
if pool_clamp_lo != -100 or 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]})
|
||||
# 充能奖励定义域校验(触发源 → {attack_type, per_unit, cap})
|
||||
for src in charge_grants.keys():
|
||||
var g: Dictionary = charge_grants[src]
|
||||
|
||||
@@ -9,7 +9,7 @@ extends Resource
|
||||
@export var rows: Array = [] # Array[Dictionary]
|
||||
|
||||
const TARGET_SIDES := ["hero", "enemy"]
|
||||
const UNITS := ["percent", "flat"]
|
||||
const UNITS := ["percent", "flat", "ac"] # 2026-09-16 护甲等级掷点批增 ac——AC 阈值轴效果(ac_up),非池百分比非定点值
|
||||
const STACK_POLICIES := ["stack", "refresh", "independent"]
|
||||
|
||||
|
||||
@@ -37,11 +37,13 @@ func validate() -> Array:
|
||||
if not STACK_POLICIES.has(row.get("stack_policy", "")):
|
||||
errs.append({"category": "enum", "message": "%s stack_policy 出域" % eid})
|
||||
var m: int = int(row.get("magnitude", 0))
|
||||
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1
|
||||
# 跨字段:percent ∈ [-100,100] 且 ≠0 / flat ≥ 1 / ac ∈ [-20,20] 且 ≠0(AC 阈值轴——幅度域独立,防呆上限 20)
|
||||
if unit == "percent" and (m == 0 or m < -100 or m > 100):
|
||||
errs.append({"category": "cross_field", "message": "%s percent magnitude 域错: %d" % [eid, m]})
|
||||
if unit == "flat" and m < 1:
|
||||
errs.append({"category": "cross_field", "message": "%s flat magnitude 须 ≥1: %d" % [eid, m]})
|
||||
if unit == "ac" and (m == 0 or m < -20 or m > 20):
|
||||
errs.append({"category": "cross_field", "message": "%s ac magnitude 域错([-20,20] 且 ≠0): %d" % [eid, m]})
|
||||
var dur: int = int(row.get("duration_settlements", 0))
|
||||
if dur < 1 or dur > 99:
|
||||
errs.append({"category": "domain", "message": "%s duration_settlements 越域 [1,99]: %d" % [eid, dur]})
|
||||
|
||||
@@ -18,7 +18,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "light"
|
||||
"armor_type": "light",
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 1,
|
||||
@@ -33,7 +34,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "light"
|
||||
"armor_type": "light",
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 2,
|
||||
@@ -47,7 +49,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward"
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 3,
|
||||
@@ -62,7 +65,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward"
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 1,
|
||||
"node_depth": 4,
|
||||
@@ -76,7 +80,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "heavy"
|
||||
"armor_type": "heavy",
|
||||
"armor_class": 0
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"node_depth": 0,
|
||||
@@ -90,7 +95,8 @@ rows = [{
|
||||
"heavy_multiplier": 2,
|
||||
"pattern": ["attack", "charge", "heavy"],
|
||||
"advantage_per_round_cap": 4,
|
||||
"armor_type": "ward"
|
||||
"armor_type": "ward",
|
||||
"armor_class": 0
|
||||
}]
|
||||
|
||||
; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照):
|
||||
@@ -112,3 +118,7 @@ rows = [{
|
||||
; 敌方攻击区间批(2026-09-14 用户拍板②第二步——"上限掺运气"敌方侧:atk 上阶/下阶,中点恒等行值;
|
||||
; roll 时机 = 遭遇骰同流〔encounter〕spawn 期整战持久,每拍抖动否决〔数字可读性锚〕;B2 零随机纪律保持
|
||||
; ——收 override 即确定值。宽度 ±3 provisional 归 C3 测量迭代;验收 = 完美策略档 0 加成通关 ≤40%。
|
||||
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板):armor_class 字段入列(缺省 0 向后兼容)——防守方掷点判定阈值基数,
|
||||
; 与 armor_type(克制属性轴)两概念严格分离、不做映射。首关六行暂定 0(正式行值归怪物/敌人系统设定批——
|
||||
; 登记待办:第一关怪物 AC 相对较低,值待怪物设定投值;投值所有权归 难度曲线与Boss)。
|
||||
@@ -6,3 +6,7 @@
|
||||
script = ExtResource("1")
|
||||
max_hp = 50
|
||||
atk = 6
|
||||
armor_class = 10
|
||||
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板):armor_class = 10(暂定基准,归 难度曲线与Boss;
|
||||
; EV 参考数按判定式复算 = 期望减免 21.25%);ac_up 增益 +1 AC/层(×2 层 = AC+2)在其上加算。
|
||||
@@ -15,11 +15,11 @@ rows = [{
|
||||
"pool_tag": "cell",
|
||||
"tier": 2
|
||||
}, {
|
||||
"reward_id": "defense_up",
|
||||
"reward_id": "ac_up",
|
||||
"cell_type": "defense",
|
||||
"polarity": "positive",
|
||||
"effect_id": "defense_up",
|
||||
"display_name": "减伤",
|
||||
"effect_id": "ac_up",
|
||||
"display_name": "护甲强化",
|
||||
"power": 0,
|
||||
"weight": 16,
|
||||
"pool_tag": "cell",
|
||||
@@ -130,3 +130,6 @@ rows = [{
|
||||
; 区间行(伤害量值区间批 2026-09-14):heal_minor [2,6] · clear_blast [25,55]——中点锚 power 恒等期望当量,
|
||||
; roll 时机 = 身份预分配批内追加取号(既有取号序零扰动);spark_aoe 定值行无区间字段。
|
||||
; 减伤墙调参批(2026-09-13):defense_up 权重 24→16——攻击成为池内最高行(历史注记)。
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板):defense 型正向行换装——reward_id/effect_id defense_up → ac_up
|
||||
; (显示名暂定"护甲强化",AC +1/层 ×2 层);cell_type defense / 权重 16 / tier 2 零改动(类型频次合计 79 不变);
|
||||
; 百分比减伤行无双轨退役——status_effects.tres defense_up 行同批删除。
|
||||
@@ -18,7 +18,6 @@ source_label_map = {
|
||||
}
|
||||
effect_pool_map = {
|
||||
"attack_up": "hero_offense",
|
||||
"defense_up": "hero_defense",
|
||||
"weaken": "enemy_output",
|
||||
"ambush": "flat_attach",
|
||||
"enrage": "enemy_output",
|
||||
@@ -57,6 +56,14 @@ armor_effectiveness = {
|
||||
"heavy": 150
|
||||
}
|
||||
}
|
||||
roll_faces = 20
|
||||
attack_bonus = 0
|
||||
roll_tier_bounds = [3, 7]
|
||||
roll_tier_discounts = [75, 50, 25]
|
||||
crit_roll = 20
|
||||
crit_multiplier = 200
|
||||
damage_floor = 1
|
||||
armor_roll_stream = "armor_roll"
|
||||
|
||||
; 攻击类型 × 护甲克制批(2026-09-12 用户拍板结构;2026-09-15 命名统一批 + 抽象分离批,数值 provisional 待用户/C3 调参):
|
||||
; 主角攻击 = 普通(atk×(1+attack_up),对全部护甲 100%)+ 穿刺 + 魔法(后两者由充能供给),三分量各自 floor 后相加;
|
||||
@@ -72,3 +79,12 @@ armor_effectiveness = {
|
||||
; sapped(我方乏力 -25%,hero_offense 池)= attack/defense/weaken 三型负向字典行
|
||||
; (极性路由兑付:挖雷格 / 标错安全格按该格 cell_type 取负向行;heal 负向 = 主角直伤不经状态、blast 负向 = ambush+2 层)。
|
||||
; 减伤墙调参批(2026-09-13):pool_clamp 域 [-100,+100] 结构冻结不动;减伤杠杆走 status_effects.tres(批注在该表)。
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板,数字即规范——战斗结算 GDD 护甲等级掷点节定稿同批):
|
||||
; roll_faces=20 / roll_tier_bounds=[3,7] / roll_tier_discounts=[75,50,25] / crit_roll=20 / crit_multiplier=200 /
|
||||
; damage_floor=1 / armor_roll_stream=armor_roll;attack_bonus=0 结构位(元进度/武器附魔未来投值)。
|
||||
; 判定式:掷点 ≥ 防守方 AC + attack_bonus → 全额;缺口 1-3 → ×0.75、4-7 → ×0.5、8+ → ×0.25;无 miss 无零收益档;
|
||||
; nat20 = 暴击 ×2 恒全额(双方同规);任何攻击落地伤害 ≥ damage_floor(全局 max(1,·))。
|
||||
; 掷点只吃基础攻击分量(主角普通分量 / 敌方攻击拍与重击拍基线);穿刺/魔法充能与棋盘来源伤害必中全额。
|
||||
; EV 参考数(按判定式复算):AC10 期望减免 21.25% / AC11 25% / AC12 28.75%;微调旋钮 = bounds/discounts,不动 AC 值。
|
||||
; effect_pool_map 同批删 defense_up 键(减伤百分比行整体退役、无双轨)——ac_up 是 AC 阈值轴效果,不入任何池;
|
||||
; off_guard(hero_defense 池)保留不动,池结构与乘区公式零改动。
|
||||
@@ -14,11 +14,11 @@ rows = [{
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 2
|
||||
}, {
|
||||
"effect_id": "defense_up",
|
||||
"display_name": "减伤",
|
||||
"effect_id": "ac_up",
|
||||
"display_name": "护甲强化",
|
||||
"target_side": "hero",
|
||||
"magnitude": -30,
|
||||
"unit": "percent",
|
||||
"magnitude": 1,
|
||||
"unit": "ac",
|
||||
"duration_settlements": 1,
|
||||
"stack_policy": "stack",
|
||||
"max_stack": 2
|
||||
@@ -76,3 +76,9 @@ rows = [{
|
||||
; 格面资源批(2026-09-15):新增 sapped(乏力——weaken 型负向,hero_offense 池,attack_up 镜像;
|
||||
; 进 effect_pool_map 同批登记)——状态表六行→七行;enrage/off_guard 语义 = attack/defense 型负向
|
||||
; (标错安全格 / 踩到雷格经极性路由兑付,不再绑"标错反噬"单一来源)。
|
||||
; 护甲等级掷点批(2026-09-16 用户拍板——百分比减伤增益整体换装为 AC,无双轨):
|
||||
; defense_up(减伤 −30 percent ×2 层)整行退役(上方 2026-09-13 减伤墙批注随之留档)——
|
||||
; "叠满池触底 ×0"免伤墙的结构面随 AC 掷点从根杜绝;新增 ac_up(护甲强化 +1 ac ×2 层)——
|
||||
; unit=ac 新量纲(AC 阈值轴效果,不入 effect_pool_map 任何池),消费面 = 战斗结算掷点判定
|
||||
; (防守方 AC = 主角表值 armor_class + Σac 型效果层数×幅度);off_guard(失衡 +25 percent,hero_defense 池)
|
||||
; 保留不动,其余六行零改动——状态表仍七行(一行换装)。
|
||||
@@ -48,7 +48,7 @@ static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Di
|
||||
var mod_text := ""
|
||||
if mod_total != 0:
|
||||
mod_text = "(%+d%% %s)" % [mod_total, "·".join(sources)]
|
||||
stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects))
|
||||
stage.hero_card.set_data(hp, max_hp, atk, mod_text, _hero_status_summary(s.hero_effects), int(hero.get("armor_class", -1)))
|
||||
info.set_hero(hp, max_hp)
|
||||
info.set_attack(atk, mod_text)
|
||||
|
||||
@@ -80,7 +80,7 @@ static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelVi
|
||||
if runner != null:
|
||||
info.set_hero(runner.hero_hp, runner.effective_max_hp())
|
||||
info.set_attack(runner.effective_atk(), "(关卡推进中)")
|
||||
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "")
|
||||
stage.hero_card.set_data(runner.hero_hp, runner.effective_max_hp(), runner.effective_atk(), "(关卡推进中)", "", int(db.hero.armor_class))
|
||||
stage.clear_enemies() # 节点间隙清残位敌卡(死亡瞬间的灰化呈现已在结算行驱动完成)
|
||||
info.set_rap(0, int(db.action_economy.rap_refresh_cost)) # battle_over RAP 清零反馈(行动经济 义务)
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ func _build_ui() -> void:
|
||||
_register_modal(m)
|
||||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||||
|
||||
var start: Dictionary = UiAssembler.build_start_overlay(self)
|
||||
var start: Dictionary = UiAssembler.build_start_overlay(self, db)
|
||||
_start_overlay = start.overlay
|
||||
_seed_edit = start.seed_edit
|
||||
start.lvbtn.pressed.connect(_start_full_level)
|
||||
|
||||
@@ -83,7 +83,7 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di
|
||||
|
||||
# 开始覆盖层(返回 {overlay, box, seed_edit, lvbtn, tunebtn, dbgbtn}——按钮接线归组合根;
|
||||
# 单场遭遇用户入口已删(2026-09-13 用户裁决——单场原语仅探针经 debug_session.start_single 使用))
|
||||
static func build_start_overlay(host: Control) -> Dictionary:
|
||||
static func build_start_overlay(host: Control, db = null) -> Dictionary:
|
||||
var overlay := overlay_bg(host, Color8(28, 22, 18))
|
||||
var box := overlay_box(overlay)
|
||||
var title := UiKit.label("Dungeon Sweeper", 34, true)
|
||||
@@ -109,7 +109,10 @@ static func build_start_overlay(host: Control) -> Dictionary:
|
||||
var dbgbtn := UiKit.button("调试 · 连续关卡两层(HP 关内延续)")
|
||||
box.add_child(dbgbtn)
|
||||
|
||||
var help := UiKit.label("玩法:每轮 8 AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/减伤/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−8 当场换图继续挖(每轮剩 2 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。", 13, false)
|
||||
# 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化")
|
||||
var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [
|
||||
int(db.action_economy.ap_per_round), int(db.action_economy.rap_refresh_cost), int(db.action_economy.refresh_max_per_round)]
|
||||
var help := UiKit.label(help_text, 13, false)
|
||||
help.add_theme_color_override("font_color", Color8(178, 166, 146))
|
||||
box.add_child(help)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# highlight 金框〔flag_hit/clear_reward_h2/perfect_broken〕、击杀标记)。
|
||||
# Date: 2026-09-12(基础 = 2026-09-11 13 类行协议格式化版;2026-09-15 格面资源批 15→10 类:
|
||||
# 删 item / consumed_ineffective / use_rejected / window_open / window_skip 五类——可选类道具与窗口下线,
|
||||
# 战斗结算 行协议清单同步)
|
||||
# 战斗结算 行协议清单同步;2026-09-16 护甲等级掷点批:伤害行补掷点读数/档位随行标注 + nat20 金框高亮)
|
||||
# 高内聚边界:战斗结算 步骤流 → 玩家文案的全部格式化规则内聚于此;来源文案映射来自 SettlementTable。
|
||||
class_name BattleLogView
|
||||
extends PanelContainer
|
||||
@@ -176,9 +176,20 @@ func _format_row(row: Dictionary, agg_count := -1) -> String:
|
||||
elif row.get("pierce", null) != null:
|
||||
# 主角攻击三分量拆行(攻击类型×护甲克制批——P2 归因:这刀里哪段是穿刺/魔法挣的)
|
||||
extra = "(普通 %d + 穿刺 %d + 魔法 %d)" % [int(row.normal), int(row.pierce), int(row.magic)]
|
||||
# 护甲等级掷点留档(2026-09-16 掷点批——掷点读数与档位随行可读;吃掷点分量恒带,棋盘来源行不带)
|
||||
var roll_txt := ""
|
||||
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)
|
||||
elif int(row.tier) < 100:
|
||||
roll_txt += " · ×%d%%" % int(row.tier)
|
||||
roll_txt += "〕"
|
||||
var kill := " ⚔击杀!" if row.get("killed", false) else ""
|
||||
var body := "%s -%d%s%s%s%s" % [name_, v, extra, tail, kill, suffix]
|
||||
var body := "%s -%d%s%s%s%s%s" % [name_, v, extra, roll_txt, tail, kill, suffix]
|
||||
var body_col := "#e07050" if row.get("killed", false) else ("#9a8a72" if v == 0 else "#e0a050")
|
||||
if bool(row.get("crit", false)):
|
||||
return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行
|
||||
return prefix + "[color=%s]%s[/color]" % [body_col, body]
|
||||
"heal":
|
||||
return prefix + "[color=#7cc47c]%s +%d%s%s[/color]" % [name_, v, tail, suffix]
|
||||
|
||||
@@ -22,6 +22,7 @@ var _intent_badge: Label
|
||||
var _ring: Label
|
||||
var _status_label: Label
|
||||
var _armor := "light" # 护甲类型快照(set_data 写入、set_intent 渲染——环点行常驻标签)
|
||||
var _armor_class := 0 # 护甲等级快照(2026-09-16 护甲等级掷点批——环点行常驻;与护甲类型两轴分立)
|
||||
var _dead := false
|
||||
|
||||
|
||||
@@ -72,6 +73,7 @@ func set_data(d: Dictionary) -> void:
|
||||
_hp_bar.value = int(d.get("hp", 0))
|
||||
_set_order(int(d.get("order", 1)))
|
||||
_armor = String(d.get("armor_type", "light"))
|
||||
_armor_class = int(d.get("armor_class", 0))
|
||||
set_intent(String(d.get("beat", "")), int(d.get("ring_size", 0)), int(d.get("ring_index", 0)))
|
||||
var ambush: int = int(d.get("ambush_layers", 0))
|
||||
_status_label.text = "伏击池 ×%d(全体共享)" % ambush if ambush > 0 else ""
|
||||
@@ -101,13 +103,14 @@ func set_intent(beat: String, ring_size: int, ring_index: int) -> void:
|
||||
else:
|
||||
_intent_badge.text = "意图:" + label + ("▼" if beat == "heavy" else "")
|
||||
# 环点 = 拍数直觉编码(● 已到拍 / ○ 未到)+ 护甲类型标签(克制对策的可读前提——恒常驻非悬停)
|
||||
# + 护甲等级 AC(掷点折扣阈值——2026-09-16 护甲等级掷点批;与护甲类型两轴分立可读)
|
||||
if ring_size > 0:
|
||||
var dots := ""
|
||||
for i in ring_size:
|
||||
dots += "●" if i == ring_index else "○"
|
||||
_ring.text = "%s · 环 %s(%d 拍)" % [_armor_tag(_armor), dots, ring_size]
|
||||
_ring.text = "%s · AC %d · 环 %s(%d 拍)" % [_armor_tag(_armor), _armor_class, dots, ring_size]
|
||||
else:
|
||||
_ring.text = _armor_tag(_armor)
|
||||
_ring.text = "%s · AC %d" % [_armor_tag(_armor), _armor_class]
|
||||
|
||||
|
||||
func _armor_tag(armor: String) -> String:
|
||||
|
||||
@@ -12,6 +12,7 @@ var _portrait: Label
|
||||
var _hp_bar: ProgressBar
|
||||
var _hp_label: Label
|
||||
var _atk_label: Label
|
||||
var _ac_label: Label
|
||||
var _status_label: Label
|
||||
|
||||
|
||||
@@ -49,15 +50,18 @@ func _init() -> void:
|
||||
col.add_child(_hp_label)
|
||||
_atk_label = UiKit.label("攻击 —", 13, false)
|
||||
col.add_child(_atk_label)
|
||||
_ac_label = UiKit.label("护甲 —", 13, false)
|
||||
col.add_child(_ac_label)
|
||||
_status_label = _fixed_label(46) # 缺陷①约束:固定槽高 + clip——状态文案切换零回流
|
||||
col.add_child(_status_label)
|
||||
|
||||
|
||||
func set_data(hp: int, max_hp: int, atk: int, atk_mod: String, status_summary: String) -> void:
|
||||
func set_data(hp: int, max_hp: int, atk: int, atk_mod: String, status_summary: String, ac: int = -1) -> void:
|
||||
_hp_bar.max_value = maxi(1, max_hp)
|
||||
_hp_bar.value = hp
|
||||
_hp_label.text = "HP %d / %d" % [hp, max_hp]
|
||||
_atk_label.text = "攻 %d %s" % [atk, atk_mod]
|
||||
_ac_label.text = "护甲 %d" % ac if ac >= 0 else "护甲 —"
|
||||
_status_label.text = status_summary
|
||||
|
||||
|
||||
@@ -65,6 +69,7 @@ func set_idle() -> void:
|
||||
_hp_bar.value = 0
|
||||
_hp_label.text = "HP —"
|
||||
_atk_label.text = "攻击 —"
|
||||
_ac_label.text = "护甲 —"
|
||||
_status_label.text = ""
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,12 @@ extends HBoxContainer
|
||||
const MAX_SLOTS := 4
|
||||
# 极性/语义映射(数据源 = SettlementTable.effect_pool_map 效果语义 + StatusEffectTable display_name):
|
||||
# 益 = ▲/◆/▼(金/蓝族);害 = ⚡/⚔/✕/☾(旗朱族)——形状+色相双通道,文字名常驻第三通道。
|
||||
# 状态表七行全映射(attack_up/defense_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约
|
||||
# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键)。
|
||||
# 状态表七行全映射(attack_up/ac_up/weaken/ambush/enrage/off_guard/sapped)——HUD框架 图标键契约
|
||||
# "一键一枚专属纹样,查不到的键立即断言 + ? 占位 + 诊断"(2026-09-15 格面资源批补 sapped 键;
|
||||
# 2026-09-16 护甲等级掷点批:defense_up → ac_up 换装)。
|
||||
const EFFECT_ICON := {
|
||||
"attack_up": ["▲", Color8(176, 122, 20), "益"],
|
||||
"defense_up": ["◆", Color8(46, 110, 142), "益"],
|
||||
"ac_up": ["◇", Color8(46, 110, 142), "益"],
|
||||
"weaken": ["▼", Color8(46, 110, 142), "益"],
|
||||
"ambush": ["⚡", Color8(198, 61, 34), "害"],
|
||||
"enrage": ["⚔", Color8(198, 61, 34), "害"],
|
||||
@@ -24,7 +25,7 @@ const EFFECT_ICON := {
|
||||
}
|
||||
const EFFECT_SEMANTIC := {
|
||||
"attack_up": "主角攻击加成(hero_offense 池加算)",
|
||||
"defense_up": "受到伤害减免(hero_defense 池加算)",
|
||||
"ac_up": "护甲等级 +1/层(掷点折扣判定阈值——AC 数值轴,受击低掷按档位打折)",
|
||||
"weaken": "敌方输出削弱(enemy_output 池乘算)",
|
||||
"ambush": "敌方全体攻击附加(flat_attach——blast 型负向,战斗段共享不消耗)",
|
||||
"enrage": "敌方输出加成(enemy_output 池加算——attack 型负向)",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-13(playtest-2026-09-12 报告 Top1——零承伤根因分割探针)
|
||||
# 逐结算行打印 beat / atk.total / hero_defense 池值 / d_tick,分割报告悬置的两个嫌疑:
|
||||
# H1 defense_up 叠满墙(设计语义——hero_defense 池触 clamp 下限 = 全额免伤)
|
||||
# H2 Boss/敌攻击链缺陷(bug——fresh 池下攻击拍仍零兑现)
|
||||
# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 defense_up 当轮授予当轮衰减——
|
||||
# 真人局 JSONL buffs[] 恒空 ≠ 池为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。
|
||||
# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/墙深/池界全读表(数据配置 Rule 1——零数值字面量)。
|
||||
# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘墙行为)
|
||||
# 2026-09-16 改掷点口径(护甲等级掷点批):defense_up 墙已随 AC 换装退役——
|
||||
# H1 旧 defense_up 叠满墙失义(hero_defense 池现仅 off_guard 正向行,×0 墙结构面消失)
|
||||
# H2 Boss/敌攻击链健康:fresh 池下非蓄力拍 d_tick = max(下限, 池乘区(掷点档位基线 + 伏击附加))
|
||||
# 快照盲区说明:settlement_snapshot 取在衰减段之后,时效 1 的 ac_up 当轮授予当轮衰减——
|
||||
# 真人局 JSONL buffs[] 恒空 ≠ 状态为空,本探针改从结算行(授予/续存 effect 行的 pool_value)重建战斗段时点池值。
|
||||
# 观测面:全部读战斗结算步骤行 / 表值 / 呈现快照(零核心穿透);池归属/档位/下限全读表(数据配置 Rule 1——零数值字面量)。
|
||||
# 模式(-- 后参数):boss [seed](默认 42:Run A 零挖掘 fresh 池 + Run B 自动挖掘授予行为)
|
||||
# level [seed](全关遭遇回放 + 多种子 spawn 扫描——报告 Bug#1 证据面)
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- boss 42
|
||||
# Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42
|
||||
@@ -53,31 +53,35 @@ func _initialize() -> void:
|
||||
|
||||
func _probe_boss(db, seed_val: int) -> void:
|
||||
var math = load("res://core/damage_math.gd")
|
||||
var def_row: Dictionary = db.status_effect_row("defense_up")
|
||||
var wall_floor: int = int(def_row.get("magnitude", 0)) * int(def_row.get("max_stack", 1))
|
||||
var clamp_lo: int = int(db.settlement.pool_clamp_lo)
|
||||
var clamp_hi: int = int(db.settlement.pool_clamp_hi)
|
||||
print("表值面:defense_up magnitude=%d × max_stack=%d = 墙最深池 %d · pool_clamp [%d, %d]" % [
|
||||
int(def_row.get("magnitude", 0)), int(def_row.get("max_stack", 1)), wall_floor, clamp_lo, 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":
|
||||
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))])
|
||||
|
||||
print("--- Run A:fresh 池(零挖掘停手轮——无授予、无墙、攻击链裸验)---")
|
||||
print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---")
|
||||
var run_a := _run_boss(db, seed_val, false)
|
||||
_print_rounds(db, math, run_a, true)
|
||||
var a_ok := _assert_rounds(db, math, run_a, true)
|
||||
check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池):非蓄力拍 d_tick = atk.total 全额兑现(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack)
|
||||
check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == DamageMath.pool_damage(×%d 拍)" % a_ok.n_attack)
|
||||
check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack)
|
||||
check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack)
|
||||
check(String(run_a.result) == "defeat", "A3 攻击链致死可达:fresh 池 Boss 在 %d 轮内击倒英雄(result=%s——伤害确实落地)" % [run_a.snaps.size(), str(run_a.result)])
|
||||
var run_a2 := _run_boss(db, seed_val, false)
|
||||
check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同")
|
||||
check(JSON.stringify(run_a.rows) == JSON.stringify(run_a2.rows), "A4 同种子回放:Run A 两遍结算行逐行相同(含 armor_roll 掷点流)")
|
||||
|
||||
print("--- Run B:自动挖掘(授予照常发生——当前数据墙行为实测)---")
|
||||
print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---")
|
||||
var run_b := _run_boss(db, seed_val, true)
|
||||
_print_rounds(db, math, run_b, true)
|
||||
var b_ok := _assert_rounds(db, math, run_b, false)
|
||||
check(b_ok.arith_ok, "B1 池算术完整(含墙轮):全部非蓄力拍 d_tick == pool_damage(raw, hero_def, enemy_out)(×%d 拍)" % b_ok.n_attack)
|
||||
check(b_ok.wall_tick_positive, "B2 墙轮不归零(当前数据重证):hero_def<0 的轮 d_tick 恒 > 0(墙轮 ×%d,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest])
|
||||
var binding_floor: int = maxi(wall_floor, clamp_lo) # 墙深与池界取更浅者 = 实际可达的最深免伤池
|
||||
check(wall_floor > clamp_lo, "B3 墙归零不可达(表值判定):墙最深池 %d > pool_clamp_lo %d → 最低残余 ×%.2f——旧数据墙(旧 magnitude×2 = clamp_lo 触底 → ×0)为真人局零承伤根因,09-13 调参批已拆(H1 成立=设计语义,非攻击链 bug)" % [wall_floor, clamp_lo, float(100 + binding_floor) / 100.0])
|
||||
check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack)
|
||||
check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest])
|
||||
check(hero_def_negative * 16 > clamp_lo or hero_def_negative >= 0, "B3 墙归零不可达(表值判定):hero_defense 池最负行 %d——叠满不可触池底 %d(×0 免伤墙结构面已随 AC 换装消失)" % [hero_def_negative, clamp_lo])
|
||||
|
||||
|
||||
func _run_boss(db, seed_val: int, dig: bool) -> Dictionary:
|
||||
@@ -200,12 +204,19 @@ func _analyze_round(db, math, rows: Array, beats: Dictionary) -> Dictionary:
|
||||
elif t == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
var baseline: int = int(row.get("baseline", 0))
|
||||
var attach: int = int(row.get("attach", 0))
|
||||
var raw: int = baseline + attach
|
||||
# 掷点口径重建(2026-09-16 护甲等级掷点批):基线按行留档档位打折(暴击 ×倍率),附加必中
|
||||
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 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 = math.pool_damage(raw, pd, pe)
|
||||
var expected: int = maxi(int(db.settlement.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 原始=%d+%d=%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [
|
||||
String(beats.get(int(row.get("round_index", 0)), "?")), baseline, attach, raw, pd, pe, tick, expected, zeroed])
|
||||
lines.append("拍=%s %s/档%d%s 基线%d→掷后%d +伏击%d hero_def=%d enemy_out=%d d_tick=%d 期望=%d %s" % [
|
||||
String(beats.get(int(row.get("round_index", 0)), "?")), roll_txt, tier, "暴击" if crit else "",
|
||||
baseline, rolled_base, attach, pd, pe, tick, expected, zeroed])
|
||||
ticks.append({"raw": raw, "tick": tick, "expected": expected, "ok": tick == expected})
|
||||
elif t == "damage" and String(row.get("source_class", "")) == "hero_attack":
|
||||
lines.append("主角攻击 %d(普通 %d + 穿刺 %d + 魔法 %d)→ %s" % [
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-09-13(playtest-2026-09-12 Q1 复核——"安全格减伤是否过量导致满血打完 Boss")
|
||||
# 逐结算轮统计 defense_up 授予层数(effect 行 value=-50)与该轮敌方实伤,分割"免伤墙轮"
|
||||
# (≥2 层 = 池 clamp -100 = 100% 免伤)与非墙轮的实际承伤。
|
||||
# 2026-09-16 改 AC 口径(护甲等级掷点批):defense_up 百分比减伤行已整体退役换装 ac_up
|
||||
# (+1 护甲等级/层 ×2 层),本探针改逐结算轮统计 ac_up 授予层数(AC10→11→12)与该轮敌方实伤,
|
||||
# 分割"高 AC 轮"(叠满 = AC12,期望减免 28.75%)与基线轮(AC10,期望减免 21.25%)的承伤读数。
|
||||
# 运行:Godot_console.exe --headless --path . --script res://smoke/defense_scan.gd -- [N] [seed0]
|
||||
extends SceneTree
|
||||
|
||||
@@ -20,19 +21,19 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
var autoplay = load("res://smoke/level_autoplay.gd")
|
||||
print("=== 减伤叠层扫描:第一关 · N=%d · 种子 %d+%d ===" % [n, seed0, n])
|
||||
print("%-6s %-6s %-8s %-10s %-12s %-12s" % ["seed", "轮数", "战斗轮", "墙轮(≥2层)", "墙轮承伤", "非墙轮承伤"])
|
||||
var wall_rounds := 0
|
||||
print("=== 护甲强化叠层扫描(AC 口径):第一关 · N=%d · 种子 %d+%d ===" % [n, seed0, n])
|
||||
print("%-6s %-6s %-8s %-12s %-14s %-14s" % ["seed", "轮数", "战斗轮", "满层轮(AC12)", "满层轮承伤", "非满层轮承伤"])
|
||||
var full_rounds := 0
|
||||
var open_rounds := 0
|
||||
var wall_dmg := 0
|
||||
var full_dmg := 0
|
||||
var open_dmg := 0
|
||||
var stack_total := 0
|
||||
var combat_rounds := 0
|
||||
for i in range(n):
|
||||
var st = autoplay.run_level(db, seed0 + i, 1, 0, 0, true)
|
||||
# 逐轮聚合(按战斗分段——round_index 跨战斗重置,直接用轮号会串桶):
|
||||
# effect 行 value=-50 计减伤层;enemy_action damage 计承伤
|
||||
var def_by_round := {}
|
||||
# effect 行 effect_id=ac_up 计护甲强化层;enemy_action damage 计承伤
|
||||
var ac_by_round := {}
|
||||
var dmg_by_round := {}
|
||||
var battle_seq := 0
|
||||
var last_round := 0
|
||||
@@ -42,43 +43,43 @@ func _initialize() -> void:
|
||||
battle_seq += 1 # 轮号回卷 = 新战斗
|
||||
last_round = r
|
||||
var key := battle_seq * 1000 + r
|
||||
if String(row.get("type", "")) == "effect" and String(row.get("display_name", "")) == "减伤" \
|
||||
if String(row.get("type", "")) == "effect" and String(row.get("effect_id", "")) == "ac_up" \
|
||||
and String(row.get("source_class", "")) == "safe_cell":
|
||||
def_by_round[key] = int(def_by_round.get(key, 0)) + 1
|
||||
ac_by_round[key] = int(ac_by_round.get(key, 0)) + 1
|
||||
elif String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
dmg_by_round[key] = int(dmg_by_round.get(key, 0)) + int(row.get("value", 0))
|
||||
for k in def_by_round:
|
||||
for k in ac_by_round:
|
||||
combat_rounds += 1
|
||||
stack_total += int(def_by_round[k])
|
||||
stack_total += int(ac_by_round[k])
|
||||
var d: int = int(dmg_by_round.get(k, 0))
|
||||
if int(def_by_round[k]) >= 2:
|
||||
wall_rounds += 1
|
||||
wall_dmg += d
|
||||
if int(ac_by_round[k]) >= 2:
|
||||
full_rounds += 1
|
||||
full_dmg += d
|
||||
else:
|
||||
open_rounds += 1
|
||||
open_dmg += d
|
||||
print("%-6d %-6d %-8d %-10d %-12s %-12s" % [seed0 + i, st.rounds, def_by_round.size(),
|
||||
_wall_count(def_by_round), str(dmg_in(dmg_by_round, def_by_round, 2)), str(dmg_in(dmg_by_round, def_by_round, 1))])
|
||||
print("%-6d %-6d %-8d %-12d %-14s %-14s" % [seed0 + i, st.rounds, ac_by_round.size(),
|
||||
_full_count(ac_by_round), str(dmg_in(dmg_by_round, ac_by_round, 2)), str(dmg_in(dmg_by_round, ac_by_round, 1))])
|
||||
if combat_rounds > 0:
|
||||
print("---- 战斗轮 %d:免伤墙轮 %d(%.0f%%,均值 %.2f 层/轮)· 墙轮承伤合计 %d(合法——-30 批墙帽 ×0.40)· 非墙轮承伤合计 %d(均 %.1f/轮)" % [
|
||||
combat_rounds, wall_rounds, 100.0 * float(wall_rounds) / float(combat_rounds),
|
||||
float(stack_total) / float(combat_rounds), wall_dmg, open_dmg, float(open_dmg) / float(maxi(1, open_rounds))])
|
||||
print("---- 战斗轮 %d:满层轮 %d(%.0f%%,均值 %.2f 层/轮)· 满层轮承伤合计 %d(AC12——期望减免 28.75%%)· 非满层轮承伤合计 %d(均 %.1f/轮,AC10/11)" % [
|
||||
combat_rounds, full_rounds, 100.0 * float(full_rounds) / float(combat_rounds),
|
||||
float(stack_total) / float(combat_rounds), full_dmg, open_dmg, float(open_dmg) / float(maxi(1, open_rounds))])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _wall_count(def_by_round: Dictionary) -> int:
|
||||
func _full_count(ac_by_round: Dictionary) -> int:
|
||||
var c := 0
|
||||
for r in def_by_round:
|
||||
if int(def_by_round[r]) >= 2:
|
||||
for r in ac_by_round:
|
||||
if int(ac_by_round[r]) >= 2:
|
||||
c += 1
|
||||
return c
|
||||
|
||||
|
||||
func dmg_in(dmg_by_round: Dictionary, def_by_round: Dictionary, mode: int) -> int:
|
||||
# mode=2 墙轮承伤 / mode=1 非墙轮承伤
|
||||
func dmg_in(dmg_by_round: Dictionary, ac_by_round: Dictionary, mode: int) -> int:
|
||||
# mode=2 满层轮承伤 / mode=1 非满层轮承伤
|
||||
var t := 0
|
||||
for r in def_by_round:
|
||||
var walled := int(def_by_round[r]) >= 2
|
||||
if (mode == 2 and walled) or (mode == 1 and not walled):
|
||||
for r in ac_by_round:
|
||||
var full: bool = int(ac_by_round[r]) >= 2
|
||||
if (mode == 2 and full) or (mode == 1 and not full):
|
||||
t += int(dmg_by_round.get(r, 0))
|
||||
return t
|
||||
@@ -25,7 +25,7 @@ func _run_encounter(db, L: int) -> void:
|
||||
var dmg_total: Array = []
|
||||
var safe_opened: Array = []
|
||||
var clears: Array = [] # 每战斗清图(换新板)次数
|
||||
var buff_grants: Array = [] # attack_up+defense_up 授予数
|
||||
var buff_grants: Array = [] # attack_up+ac_up 授予数
|
||||
var losses := 0
|
||||
var timeouts := 0
|
||||
for i in range(N):
|
||||
@@ -61,7 +61,7 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary:
|
||||
settlement.step_row.connect(func(row):
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
out.hero_dmg += int(row.value)
|
||||
elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "defense_up"):
|
||||
elif row.type == "effect" and row.source_class == "safe_cell" and (row.effect_id == "attack_up" or row.effect_id == "ac_up"):
|
||||
out.buffs += 1)
|
||||
settlement.minesweeper.cell_revealed.connect(func(evt):
|
||||
if evt.kind == "safe" and evt.channel != "initial":
|
||||
|
||||
@@ -38,6 +38,7 @@ func _initialize() -> void:
|
||||
check(rng.derive_at("board_gen", 3) == 8849696722413293638, "R2 向量 board_gen m=42 scope=3")
|
||||
check(rng.derive_at("reward_roll", 0) == 7422916049646135537, "R2 向量 reward_roll m=42 scope=0")
|
||||
check(rng.derive_at("encounter", 0) == 1061841193803499209, "R2 向量 encounter m=42 scope=0(registry salt_encounter=0x8EBC6AF09C88C6E3 权威值)")
|
||||
check(rng.derive_at("armor_roll", 0) == 4806095330042243845, "R2 向量 armor_roll m=42 scope=0(护甲等级掷点批 2026-09-16——registry salt_armor_roll=0xC2B2AE3D27D4EB2F 权威值)")
|
||||
check(rng.derive_at("board_gen", 0) == rng.derive_at("board_gen", 0), "derive_at 纯函数恒等")
|
||||
# scope 取号语义:首个 open_stream = scope 0;计数器单调
|
||||
var s0 = rng.open_stream("board_gen")
|
||||
@@ -60,7 +61,8 @@ var _rng
|
||||
func _run_core_smoke(db, rng) -> void:
|
||||
_db = db
|
||||
_rng = rng
|
||||
_smoke_wall_zero_guard()
|
||||
_smoke_ac_swap_guard()
|
||||
_smoke_armor_roll()
|
||||
_smoke_board_generator()
|
||||
_smoke_variation_framework()
|
||||
_smoke_full_battle(1, 0, "L1 · grub")
|
||||
@@ -196,18 +198,194 @@ func _smoke_level() -> void:
|
||||
check(int(_db.hero.atk) == 6 and int(_db.hero.max_hp) == 50, "模拟后 hero 表恢复基线(atk=%d hp=%d)" % [int(_db.hero.atk), int(_db.hero.max_hp)])
|
||||
|
||||
|
||||
# ---- 减伤墙归零护栏(2026-09-13 用户拍板:墙语义维持现状 + 护栏——BUG-0003 事故类永久免疫)----
|
||||
# 表级不变式:2×defense_up.magnitude > pool_clamp_lo。违反 = 叠满两层触池底 = F2 乘区 ×0 全额免伤
|
||||
# (playtest-2026-09-12 真人局 12/12 轮零承伤根因)。任何未来 magnitude/clamp 调参重新造出 ×0 墙在此红灯,
|
||||
# 强制显式重审(含 clamp 收紧等结构变更——本护栏随之需要有意修订)。
|
||||
# ---- 护甲等级掷点批·换装护栏(2026-09-16 用户拍板:百分比减伤行整体退役换装 AC,无双轨)----
|
||||
# ① 换装在位:defense_up 状态行/池映射/奖励行三处全无残留,ac_up 三处就位(unit=ac ×2 层);
|
||||
# ② 免伤墙归零护栏泛化保留(BUG-0003 事故类永久免疫):hero_defense 池任何 percent 行
|
||||
# magnitude × max_stack > pool_clamp_lo——单效果叠满不可触池底(现行池内仅 off_guard 正向行,
|
||||
# 墙结构面已随 AC 换装消失,本断言防未来负向行回归时静默造墙);
|
||||
# ③ 单位表 armor_class 字段:主角 10(暂定)/ 敌方行 0(暂定——正式值归怪物设定批);
|
||||
# ④ 掷点参数域:档位边界升序/档位值降序无零档/骰面与暴击参数在域(加载校验双保险)。
|
||||
|
||||
func _smoke_wall_zero_guard() -> void:
|
||||
print("--- 增益状态·减伤墙归零护栏 ---")
|
||||
var def_row: Dictionary = _db.status_effect_row("defense_up")
|
||||
var wall_floor: int = int(def_row.get("magnitude", 0)) * int(def_row.get("max_stack", 1))
|
||||
var clamp_lo: int = int(_db.settlement.pool_clamp_lo)
|
||||
check(wall_floor > clamp_lo, "墙最深池 %d > pool_clamp_lo %d → 叠满不可归零(最低残余 ×%.2f)" % [
|
||||
wall_floor, clamp_lo, float(100 + wall_floor) / 100.0])
|
||||
func _smoke_ac_swap_guard() -> void:
|
||||
print("--- 护甲等级掷点批·换装护栏 ---")
|
||||
# ① 换装在位(三处无双轨)
|
||||
check(_db.status_effect_row("defense_up").is_empty(), "状态表 defense_up 行已退役(无双轨)")
|
||||
var ac_row: Dictionary = _db.status_effect_row("ac_up")
|
||||
check(not ac_row.is_empty() and String(ac_row.get("target_side", "")) == "hero" \
|
||||
and String(ac_row.get("unit", "")) == "ac" and int(ac_row.get("magnitude", 0)) == 1 \
|
||||
and int(ac_row.get("max_stack", 0)) == 2 and String(ac_row.get("stack_policy", "")) == "stack",
|
||||
"ac_up 行在位(hero · +1 ac · ×2 层 · stack——AC 阈值轴新量纲)")
|
||||
var ac_reward := {}
|
||||
for row in _db.reward.rows:
|
||||
if String(row.get("cell_type", "")) == "defense" and String(row.get("polarity", "")) == "positive":
|
||||
ac_reward = row
|
||||
check(String(ac_reward.get("effect_id", "")) == "ac_up" and String(ac_reward.get("display_name", "")) == "护甲强化",
|
||||
"奖励表 defense 正向行 effect_id = ac_up(显示名暂定护甲强化,权重/tier 不变)")
|
||||
check(not _db.settlement.effect_pool_map.has("defense_up") and not _db.settlement.effect_pool_map.has("ac_up"),
|
||||
"effect_pool_map 无 defense_up/ac_up 键(减伤不入池——AC 不经乘区)")
|
||||
# ② 免伤墙归零护栏(泛化——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":
|
||||
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):
|
||||
wall_ok = false
|
||||
wall_detail += " %s(%d)" % [eff_id, floor_v]
|
||||
check(wall_ok, "hero_defense 池 percent 行叠满不可触池底(墙最深%s > clamp_lo %d——单效果归零结构性不可达)" % [
|
||||
wall_detail if wall_detail != "" else "(池内现无负向行)", int(_db.settlement.pool_clamp_lo)])
|
||||
# ③ 单位表 armor_class
|
||||
check(int(_db.hero.armor_class) == 10, "主角 armor_class = 10(暂定基准——EV 期望减免 21.25%%)")
|
||||
var ac_all := true
|
||||
for row in _db.enemy.rows:
|
||||
if not row.has("armor_class") or int(row.armor_class) != 0:
|
||||
ac_all = false
|
||||
check(ac_all, "敌方全行 armor_class = 0(首关暂定——正式行值归怪物设定批待办)")
|
||||
# ④ 掷点参数域(加载校验已过——此处钉形状语义)
|
||||
var bounds: Array = _db.settlement.roll_tier_bounds
|
||||
var discounts: Array = _db.settlement.roll_tier_discounts
|
||||
check(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",
|
||||
"掷点参数拍板值在位(d20 / nat20 暴击 ×200%% / 下限 1 / attack_bonus 0 / armor_roll 流)")
|
||||
check(bounds == [3, 7] and discounts == [75, 50, 25], "档位表拍板值在位(缺口 1-3 → ×0.75 · 4-7 → ×0.5 · 8+ → ×0.25——无零档)")
|
||||
|
||||
|
||||
# ---- 护甲等级掷点批·行为面(2026-09-16 拍板验收:折扣档分布 / nat20 暴击 / 伤害下限 / 棋盘来源必中)----
|
||||
|
||||
func _smoke_armor_roll() -> void:
|
||||
print("--- 护甲等级掷点(查档纯函数 + 暴击 + 伤害下限 + AC12 分布 + 棋盘必中)---")
|
||||
# ① 查档纯函数:全掷点域逐位对照(AC10 / AC12)+ EV 锚(AC10 期望减免 21.25% —— registry 参考数)
|
||||
var expect_ac10 := {100: 11, 75: 3, 50: 4, 25: 2} # 掷 10-20 全额 / 7-9 / 3-6 / 1-2
|
||||
var expect_ac12 := {100: 9, 75: 3, 50: 4, 25: 4} # 掷 12-20 全额 / 9-11 / 5-8 / 1-4
|
||||
for cfg in [{"ac": 10, "expect": expect_ac10}, {"ac": 12, "expect": expect_ac12}]:
|
||||
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)
|
||||
if not dist.has(d):
|
||||
tier_ok = false
|
||||
continue
|
||||
dist[d] += 1
|
||||
check(tier_ok and dist == cfg.expect, "查档 AC%d 全掷点域分布(100/75/50/25 = %s)" % [cfg.ac, str([dist[100], dist[75], dist[50], dist[25]])])
|
||||
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)
|
||||
check(ev_sum == 1575, "AC10 EV 锚:期望系数合计 %d/2000 = 期望减免 21.25%%(nat20 恒全额含内)" % ev_sum)
|
||||
# ② nat20 暴击(行为面):种子扫描定位 armor_roll scope0 首掷 = 20 的种子 → 停手轮主角先手必暴击
|
||||
var crit_seed := -1
|
||||
for s in range(1, 800):
|
||||
var rg_probe = load("res://core/rng_service.gd").new()
|
||||
rg_probe.set_master_seed(s)
|
||||
if rg_probe.open_stream("armor_roll").randi_range(1, int(_db.settlement.roll_faces)) == int(_db.settlement.crit_roll):
|
||||
crit_seed = s
|
||||
break
|
||||
check(crit_seed > 0, "nat20 种子可达(seed=%d——armor_roll scope0 首掷命中 %d)" % [crit_seed, int(_db.settlement.crit_roll)])
|
||||
var crit_rows := _first_hero_attack_rows(crit_seed)
|
||||
check(int(crit_rows.get("roll", -1)) == int(_db.settlement.crit_roll) and bool(crit_rows.get("crit", false)),
|
||||
"nat20 → 行留档 roll=20 + crit 标记(主角先手普通分量)")
|
||||
check(int(crit_rows.get("normal", -1)) == 2 * int(_db.hero.atk) and int(crit_rows.get("value", -1)) == 2 * int(_db.hero.atk),
|
||||
"nat20 → 普通分量 ×2(%d = 2×%d,暴击倍率只乘该分量,穿刺/魔法不吃)" % [int(crit_rows.get("normal", -1)), int(_db.hero.atk)])
|
||||
# ③ 伤害下限(行为面):削弱 ×2 = 池触底 ×0 → 落地伤害 = damage_floor(零值行已退役)
|
||||
var floor_rows := _weaken_zero_round_rows()
|
||||
check(not floor_rows.is_empty() and int(floor_rows[0].get("value", -1)) == int(_db.settlement.damage_floor),
|
||||
"池触底 ×0 → 落地伤害 = %d(全局 max(1,·)——×%d 拍采样)" % [int(_db.settlement.damage_floor), floor_rows.size()])
|
||||
var no_zero_label := true
|
||||
for r in floor_rows:
|
||||
if String(r.get("display_name", "")).contains("归零") or int(r.get("value", 0)) <= 0:
|
||||
no_zero_label = false
|
||||
check(no_zero_label, "零值伤害行语义退役:无\"被减伤归零\"行、无 v=0 伤害行(被压制/保底下限可见)")
|
||||
# ④ AC12 折扣档分布(行为面):ac_up ×2 逐轮授给(时效 1)→ Boss 行攻击拍逐拍判定
|
||||
var tiers := _ac12_tier_samples()
|
||||
var tier_distinct := {}
|
||||
var tiers_valid := true
|
||||
for t in tiers:
|
||||
tier_distinct[int(t)] = true
|
||||
if not [100, 75, 50, 25].has(int(t)):
|
||||
tiers_valid = false
|
||||
check(tiers_valid and tiers.size() >= 3, "AC12 攻击拍逐拍判定(×%d 拍全部落档域 {100,75,50,25})" % tiers.size())
|
||||
check(tier_distinct.size() >= 2, "折扣档实际流动(AC12 实测档位 %s——≥2 档可见)" % str(tier_distinct.keys()))
|
||||
# ⑤ 棋盘来源必中 + 吃掷点行留档(全战斗流扫描)
|
||||
var run = _run_battle(1, 0)
|
||||
var board_ok := true
|
||||
var attack_ok := true
|
||||
var all_positive := true
|
||||
for row in run.stream:
|
||||
if String(row.get("type", "")) != "damage":
|
||||
continue
|
||||
if int(row.get("value", 0)) <= 0:
|
||||
all_positive = false
|
||||
var src := String(row.get("source_class", ""))
|
||||
if src == "hero_attack" or src == "enemy_action":
|
||||
if not (row.has("roll") and row.has("ac") and row.has("tier")):
|
||||
attack_ok = false
|
||||
elif row.has("roll"):
|
||||
board_ok = false # 棋盘来源(爆裂/直伤/清障炸药)必中全额——不掷不留档
|
||||
check(all_positive, "全战斗流零 v=0 伤害行(伤害下限全局生效)")
|
||||
check(attack_ok, "hero_attack/enemy_action 伤害行掷点读数+档位随行留档(P2 归因)")
|
||||
check(board_ok, "棋盘来源伤害行零掷点字段(伏击附加/爆裂/直伤/清障炸药必中全额)")
|
||||
|
||||
|
||||
# 定位种子下首场战斗第一行主角攻击伤害行(停手轮——零授予零充能,普通分量 = 攻击基线裸值)
|
||||
func _first_hero_attack_rows(seed_val: int) -> Dictionary:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}):
|
||||
return {}
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack":
|
||||
return row
|
||||
return {}
|
||||
|
||||
|
||||
# 削弱 ×2(池 enemy_output 触底 ×0)下的敌方攻击伤害行采样(D2 熊地精——首拍攻击)
|
||||
func _weaken_zero_round_rows() -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260916)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
return rows
|
||||
# 测试通道:直授削弱 ×2(来源格取真实坐标——行来源断言要求;时效 1 当轮结算内有效)
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("weaken", {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": 1})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var out: Array = []
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
|
||||
# AC12 折扣档采样:每停手轮前直授 ac_up ×2(时效 1 逐轮衰减)——Boss 行攻击/重击拍逐拍判定留档
|
||||
func _ac12_tier_samples() -> Array:
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260917)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.hero.max_hp)}):
|
||||
return []
|
||||
var guard := 12
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
guard -= 1
|
||||
for i in range(2):
|
||||
settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var tiers: Array = []
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "enemy_action":
|
||||
tiers.append(int(row.get("tier", -1)))
|
||||
return tiers
|
||||
|
||||
|
||||
func _smoke_board_generator() -> void:
|
||||
@@ -768,17 +946,19 @@ func _level_battle_nodes(level_index: int) -> Array:
|
||||
|
||||
# ---- 战斗段 fresh 池攻击链回归(playtest-2026-09-12 报告 Bug#2 沉淀——
|
||||
# 「Boss 攻击拍零兑现」实为旧 defense_up 数据 ×2 叠满 = 池触底 ×0(设计语义);本断言钉死:
|
||||
# 池为空时攻击链必须全额兑现——若未来出现 fresh 池零兑现(真攻击链缺陷)在此被抓)----
|
||||
# 池为空时攻击链必须按掷点档位足额兑现——2026-09-16 护甲等级掷点批改口径:
|
||||
# d_tick = max(damage_floor, pool_damage(floor(基线×档位%), 0, 0)),档位读数随行重建可归因)----
|
||||
|
||||
func _smoke_attack_chain_fresh_pool() -> void:
|
||||
print("--- 战斗结算·fresh 池攻击链回归(Bug#2)---")
|
||||
print("--- 战斗结算·fresh 池攻击链回归(Bug#2·掷点口径)---")
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20260913)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var ticks: Array = [] # {raw, tick}——敌行动伤害行重建
|
||||
var ticks: Array = [] # {baseline, attach, roll, tier, crit, tick}——敌行动伤害行重建
|
||||
settlement.step_row.connect(func(row):
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
ticks.append({"raw": int(row.baseline) + int(row.attach), "tick": int(row.value)}))
|
||||
ticks.append({"baseline": int(row.baseline), "attach": int(row.attach), "roll": int(row.roll),
|
||||
"tier": int(row.tier), "crit": bool(row.get("crit", false)), "tick": int(row.value)}))
|
||||
check(settlement.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(_db.hero.max_hp)}), "fresh 池回归战斗启动(Boss 行)")
|
||||
# 停手轮:零挖掘零旗 → 零授予 → 池恒空;Boss pattern 前 4 拍 = attack/attack/charge/heavy
|
||||
for i in range(4):
|
||||
@@ -788,9 +968,12 @@ func _smoke_attack_chain_fresh_pool() -> void:
|
||||
check(ticks.size() >= 2, "非蓄力攻击拍采样充足(%d 拍——charge 拍零兑付为设计语义不计)" % ticks.size())
|
||||
var all_full := true
|
||||
for t in ticks:
|
||||
if int(t.tick) != int(t.raw) or int(t.raw) <= 0:
|
||||
var base_factor: float = float(t.tier) * (float(_db.settlement.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.attach))
|
||||
if int(t.tick) != expected or int(t.tick) <= 0:
|
||||
all_full = false
|
||||
check(all_full, "fresh 池非蓄力拍 d_tick == atk.total 全额兑现(Boss 攻击链健康——Bug#2 回归断言)")
|
||||
check(all_full, "fresh 池非蓄力拍 d_tick = max(1, 掷点档位基线 + 伏击附加)(Boss 攻击链健康——Bug#2 回归断言)")
|
||||
|
||||
func _encounter_replay(seed_val: int) -> Array:
|
||||
var out: Array = []
|
||||
@@ -914,9 +1097,10 @@ func _smoke_refresh_provenance_guard() -> void:
|
||||
|
||||
# ==================== 格面资源批新增覆盖(极性路由 / 负向字典 / 资源面 / 五段全序) ====================
|
||||
# 极性路由断言(战斗结算 验收):挖安全格/标中旗只出正向、挖雷格/标错旗只出负向、兑付行指回来源格。
|
||||
# 正向字典效果集 = {attack_up, defense_up, weaken}(percent)+ spark_aoe/heal_minor(flat);
|
||||
# 正向字典效果集 = {attack_up, ac_up, weaken}(ac_up = 2026-09-16 护甲等级掷点批换装,原 defense_up)
|
||||
# + spark_aoe/heal_minor(flat);
|
||||
# 负向字典效果集 = {enrage, off_guard, sapped}(percent)+ 直伤(heal flat)/ ambush(blast flat)。
|
||||
const POSITIVE_EFFECTS := ["attack_up", "defense_up", "weaken"]
|
||||
const POSITIVE_EFFECTS := ["attack_up", "ac_up", "weaken"]
|
||||
const NEGATIVE_EFFECTS := ["enrage", "off_guard", "sapped", "ambush"]
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://dw3rd4l55w70r
|
||||
@@ -0,0 +1,612 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""试玩日志分析器(垂直切片 tools/ 专用,纯标准库,零第三方依赖)。
|
||||
|
||||
消费 presentation/playtest_logger.gd 产生的 playtest_*.jsonl,
|
||||
产出 关卡 / 战斗 / 轮次 三级统计,并为护甲等级(AC)掷点批预留验收统计面。
|
||||
|
||||
已知测量盲区(本工具的核心补偿,勿绕过):
|
||||
settlement 快照在衰减段之后发出、且现有状态寿命全部为 1 结算
|
||||
→ 快照 buffs 字段恒为空。战斗期真实增益只能从 step 行(type=effect,
|
||||
文本 增伤/削弱/减伤/…)按结算轮窗口复原。本工具从不把快照 buffs
|
||||
当战斗期状态使用,只将其并列输出以持续暴露盲区。
|
||||
|
||||
AC 掷点批预留位:
|
||||
落批后伤害 step 行会新增掷点字段(读数/档位/暴击,字段名以落批实际为准)。
|
||||
本工具按 FIELD_CANDIDATES 探测:字段存在 → 激活掷点统计面;
|
||||
不存在(AC 批前旧日志)→ 降级输出"不适用(AC 批前日志)"。
|
||||
同时所有 step 行的未知键都会进"数据质量"观测面——落批后先看那里
|
||||
拿到真实字段名,再在 FIELD_CANDIDATES 里补一行适配即可。
|
||||
|
||||
用法:
|
||||
python log_analysis.py <jsonl 文件 | 目录 | 通配> [--per-run] [--rounds]
|
||||
目录/通配 = 批量汇总;--per-run 附加逐局明细;--rounds 附加逐轮流水。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
# ---------------------------------------------------------------- 常量与映射
|
||||
|
||||
# step 行已知键(schema 事实源 = playtest_logger.gd _on_step_row,2026-09-16 版)
|
||||
STEP_KNOWN_KEYS = {"kind", "type", "source", "cell", "round", "text", "value", "highlight"}
|
||||
|
||||
# AC 掷点批候选字段名(落批后按实际字段名在此登记——预留的一行适配点)
|
||||
ROLL_FIELD_CANDIDATES = ("roll", "d20", "dice", "nat", "nat_roll", "roll_value")
|
||||
TIER_FIELD_CANDIDATES = ("tier", "discount", "discount_tier", "damage_tier", "bracket")
|
||||
CRIT_FIELD_CANDIDATES = ("crit", "critical", "is_crit", "nat20")
|
||||
AC_FIELD_CANDIDATES = ("ac", "target_ac", "defense_ac")
|
||||
|
||||
# effect 行显示文本 → 增益语义键(未登记文本原样作键,新增益自动浮现)
|
||||
TEXT_TO_BUFF = {"增伤": "attack_up", "削弱": "weaken", "减伤": "defend_up"}
|
||||
|
||||
# 档位语义:缺口 = AC − 掷点读数(读数达标 = 全额)
|
||||
TIER_MULTIPLIER = {"全额": 1.0, "×0.75": 0.75, "×0.5": 0.5, "×0.25": 0.25}
|
||||
THEORY_EV_REDUCTION_AC10 = 0.2125 # AC10 理论 EV 减免(任务口径)
|
||||
THEORY_CRIT_RATE = 0.05 # nat20 暴击率理论值
|
||||
CRIT_TOLERANCE = 0.02 # 健康检查宽容度 ±2 个百分点
|
||||
|
||||
BAND_NORMAL = 0.20 # 承伤带:普通场 ≤20% max_hp
|
||||
BAND_BOSS = 0.50 # 承伤带:Boss 场 ≤50% max_hp
|
||||
|
||||
BATTLE_TYPES = ("battle", "boss")
|
||||
|
||||
|
||||
def _first(row, candidates):
|
||||
"""返回 row 中第一个命中的候选字段值。"""
|
||||
for c in candidates:
|
||||
if c in row:
|
||||
return row[c]
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 逐局状态机
|
||||
|
||||
class BattleAcc:
|
||||
"""一场战斗(battle/boss 节点)的累计器。"""
|
||||
|
||||
def __init__(self, node_type, depth):
|
||||
self.node_type = node_type
|
||||
self.depth = depth
|
||||
self.rounds = [] # 每结算一个 dict(见 RunAcc._finalize_round)
|
||||
self.damage = 0 # Σ 敌方攻击落地伤害(enemy_action × type=damage 的 value)
|
||||
self.heals = 0 # Σ 主角治疗(type=heal 的 value)
|
||||
self.attack_rounds = 0 # 有敌方攻击落地的轮数
|
||||
self.max_hp = None
|
||||
self.start_hp = None # 首结算快照 HP + 首轮承伤 − 首轮治疗 反推的进场 HP
|
||||
self.last_hp = None
|
||||
|
||||
|
||||
class RunAcc:
|
||||
"""一局(run 行切分;无 run 行的旧日志 = 整文件一局,种子取 session 行)。"""
|
||||
|
||||
def __init__(self, src_file, session_seed):
|
||||
self.src_file = src_file
|
||||
self.session_seed = session_seed
|
||||
self.seed = session_seed
|
||||
self.policy = ""
|
||||
self.level_index = None
|
||||
self.result = None
|
||||
self.end_hp = None
|
||||
self.max_hp = None
|
||||
self.round_steps = [] # 当前结算轮窗口内的 step 行缓冲
|
||||
self.battle = None
|
||||
self.battles = []
|
||||
self.battle_fifo = [] # battle_end 延迟发出且不带 depth,按 FIFO 对账
|
||||
self.late_battle_ends = 0 # 未能配对的 battle_end(数据质量)
|
||||
self.boss_node_seen = False
|
||||
self.rounds_total = 0
|
||||
self.rounds_with_enemy = 0
|
||||
self.damage_total = 0
|
||||
self.heals_total = 0
|
||||
self.zero_narrow = 0 # 零承伤轮(窄口径,AC 验收主指标)
|
||||
self.attack_landed_rounds = 0
|
||||
self.zero_loss_wide = 0 # 零掉血轮(宽口径,参考)
|
||||
self.snapshot_buffs_nonempty = 0 # 盲区暴露面:快照 buffs 恒空计数
|
||||
self.snapshot_buffs_total = 0
|
||||
self.round_ledger = [] # --rounds 逐轮流水
|
||||
self.roll_rows = [] # 命中掷点候选字段的 step 行
|
||||
self._prev_hp = None # 上一结算快照的主角 HP(宽口径逐轮差分用)
|
||||
self._last_enemy_present = False
|
||||
|
||||
# -- 事件入口 ---------------------------------------------------------
|
||||
|
||||
def on_run_row(self, e):
|
||||
self.seed = e.get("seed", self.session_seed)
|
||||
self.policy = "" if e.get("policy") is None else str(e.get("policy"))
|
||||
self.level_index = e.get("level_index")
|
||||
|
||||
def on_node(self, e):
|
||||
t = e.get("type", "")
|
||||
if t in BATTLE_TYPES:
|
||||
# 先收掉上一场(战斗的 settlement 严格落在相邻 node 行之间)
|
||||
self._close_battle()
|
||||
self.battle = BattleAcc(t, e.get("depth"))
|
||||
if t == "boss":
|
||||
self.boss_node_seen = True
|
||||
# rest / reward 节点不切战斗上下文(与 battle 同 index 出现)
|
||||
|
||||
def on_step(self, e):
|
||||
self.round_steps.append(e)
|
||||
if any(c in e for c in ROLL_FIELD_CANDIDATES):
|
||||
self.roll_rows.append(e)
|
||||
|
||||
def on_settlement(self, e):
|
||||
b = self.battle
|
||||
self.rounds_total += 1
|
||||
hero = e.get("hero", {})
|
||||
hp = int(hero.get("hp", 0))
|
||||
if hero.get("max_hp"):
|
||||
self.max_hp = int(hero.get("max_hp"))
|
||||
enemy_present = bool(e.get("enemies"))
|
||||
if enemy_present:
|
||||
self.rounds_with_enemy += 1
|
||||
# 盲区暴露面:快照 buffs 只计数对照,从不当作战斗期状态
|
||||
self.snapshot_buffs_total += 1
|
||||
if e.get("buffs"):
|
||||
self.snapshot_buffs_nonempty += 1
|
||||
self._finalize_round(e, b, hp, enemy_present)
|
||||
|
||||
def on_battle_end(self, e):
|
||||
# 延迟发出、不带 depth:只做 FIFO 对账(数据质量),核心统计不依赖它
|
||||
if self.battle_fifo:
|
||||
self.battle_fifo.pop(0)
|
||||
else:
|
||||
self.late_battle_ends += 1
|
||||
|
||||
def on_level_end(self, e):
|
||||
self._close_battle()
|
||||
self.result = e.get("result", "")
|
||||
self.end_hp = int(e.get("hp", 0))
|
||||
|
||||
# -- 轮窗口收口(核心口径都在这里)------------------------------------
|
||||
|
||||
def _finalize_round(self, snap, battle, hp, enemy_present):
|
||||
steps, self.round_steps = self.round_steps, []
|
||||
dmg = sum(int(s.get("value", 0)) for s in steps
|
||||
if s.get("type") == "damage" and s.get("source") == "enemy_action")
|
||||
heal = sum(int(s.get("value", 0)) for s in steps if s.get("type") == "heal")
|
||||
attacks = sum(1 for s in steps
|
||||
if s.get("type") == "damage" and s.get("source") == "enemy_action")
|
||||
charges = sum(1 for s in steps if s.get("type") == "charge")
|
||||
# 蓄力拍(type=charge,含空拍/白喂)不算"攻击落地"——验收口径硬性排除。
|
||||
# 战斗期增益复原:本结算轮窗口内的 effect 行(快照 buffs 恒空,禁用)
|
||||
buffs = Counter()
|
||||
for s in steps:
|
||||
if s.get("type") == "effect":
|
||||
key = TEXT_TO_BUFF.get(s.get("text", ""), s.get("text", ""))
|
||||
buffs[key or "<未登记effect文本>"] += 1
|
||||
|
||||
# 宽口径(整轮掉血 0):首轮用进场反推 HP 差分,之后用相邻快照差
|
||||
if self._prev_hp is None:
|
||||
entry_hp = hp + dmg - heal
|
||||
loss = entry_hp - hp
|
||||
else:
|
||||
loss = max(0, self._prev_hp - hp)
|
||||
self._prev_hp = hp
|
||||
|
||||
zero_narrow = attacks > 0 and dmg == 0 # 窄口径:有攻击落地且掉血 0
|
||||
zero_wide = loss == 0 # 宽口径:整轮 HP 不降(聚合时限敌战轮)
|
||||
|
||||
self.damage_total += dmg
|
||||
self.heals_total += heal
|
||||
if attacks > 0:
|
||||
self.attack_landed_rounds += 1
|
||||
if zero_narrow:
|
||||
self.zero_narrow += 1
|
||||
if zero_wide:
|
||||
self.zero_loss_wide += 1
|
||||
|
||||
rec = {
|
||||
"type": battle.node_type if battle else "?",
|
||||
"depth": snap.get("depth"), "round": snap.get("round"),
|
||||
"dmg": dmg, "heal": heal, "hp": hp,
|
||||
"attacks": attacks, "charges": charges,
|
||||
"zero_narrow": zero_narrow, "zero_wide": zero_wide,
|
||||
"buffs": dict(buffs),
|
||||
"path": (snap.get("round_facts") or {}).get("path", ""),
|
||||
"enemy_present": enemy_present,
|
||||
}
|
||||
self.round_ledger.append(rec)
|
||||
if battle is not None:
|
||||
battle.rounds.append(rec)
|
||||
battle.damage += dmg
|
||||
battle.heals += heal
|
||||
battle.attack_rounds += 1 if attacks > 0 else 0
|
||||
battle.max_hp = self.max_hp
|
||||
battle.last_hp = hp
|
||||
if battle.start_hp is None:
|
||||
battle.start_hp = hp + dmg - heal
|
||||
|
||||
def _close_battle(self):
|
||||
if self.battle is None:
|
||||
return
|
||||
b, self.battle = self.battle, None
|
||||
if b.rounds:
|
||||
# 有过敌人的战斗(任一轮出现过敌人或攻击落地)——宽口径分母用其全部轮数。
|
||||
# 不能按单轮快照 enemies 判定:敌人当轮被歼的轮快照为空,但轮仍属于敌战。
|
||||
b.enemy_flag = any(r["attacks"] > 0 or r["enemy_present"] for r in b.rounds)
|
||||
for rec in b.rounds:
|
||||
rec["enemy_battle"] = b.enemy_flag
|
||||
self.battles.append(b)
|
||||
self.battle_fifo.append(b)
|
||||
|
||||
# -- 派生量 -----------------------------------------------------------
|
||||
|
||||
def boss_battle(self):
|
||||
for b in self.battles:
|
||||
if b.node_type == "boss":
|
||||
return b
|
||||
return None
|
||||
|
||||
def boss_enter_hp(self):
|
||||
"""进 Boss HP = Boss 首结算快照 HP + 首轮承伤 − 首轮治疗(精确反推)。"""
|
||||
b = self.boss_battle()
|
||||
if b is None or not b.rounds:
|
||||
return None
|
||||
r0 = b.rounds[0]
|
||||
return r0["hp"] + r0["dmg"] - r0["heal"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 文件解析
|
||||
|
||||
class FileResult:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.lines = 0
|
||||
self.bad_lines = 0
|
||||
self.parse_seconds = 0.0
|
||||
self.session_seeds = []
|
||||
self.runs = []
|
||||
self.unknown_step_keys = Counter() # 未知 step 键 → 次数(AC 落批观察面)
|
||||
self.unknown_step_samples = {} # 未知 step 键 → 样例值
|
||||
self.dice_active = False
|
||||
|
||||
|
||||
def parse_file(path):
|
||||
res = FileResult(path)
|
||||
t0 = time.perf_counter()
|
||||
run = None
|
||||
with open(path, encoding="utf-8", errors="replace") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
res.lines += 1
|
||||
try:
|
||||
e = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
res.bad_lines += 1
|
||||
continue
|
||||
kind = e.get("kind", "")
|
||||
if kind == "session":
|
||||
res.session_seeds.append(e.get("seed"))
|
||||
if run is None:
|
||||
run = RunAcc(os.path.basename(path), e.get("seed"))
|
||||
res.runs.append(run)
|
||||
elif kind == "run":
|
||||
if run is not None and (run.rounds_total or run.result or run.battles):
|
||||
run = RunAcc(os.path.basename(path), res.session_seeds[-1] if res.session_seeds else None)
|
||||
res.runs.append(run)
|
||||
elif run is None:
|
||||
run = RunAcc(os.path.basename(path), None)
|
||||
res.runs.append(run)
|
||||
run.on_run_row(e)
|
||||
elif run is None:
|
||||
# 无 session/run 头的残缺文件:兜底开一局,保证数据不丢
|
||||
run = RunAcc(os.path.basename(path), None)
|
||||
res.runs.append(run)
|
||||
_feed(run, res, kind, e)
|
||||
else:
|
||||
_feed(run, res, kind, e)
|
||||
if run is not None:
|
||||
run._close_battle()
|
||||
res.parse_seconds = time.perf_counter() - t0
|
||||
res.dice_active = any(r.roll_rows for r in res.runs)
|
||||
return res
|
||||
|
||||
|
||||
def _feed(run, res, kind, e):
|
||||
if kind == "node":
|
||||
run.on_node(e)
|
||||
elif kind == "step":
|
||||
run.on_step(e)
|
||||
for k in e:
|
||||
if k not in STEP_KNOWN_KEYS:
|
||||
res.unknown_step_keys[k] += 1
|
||||
res.unknown_step_samples.setdefault(k, repr(e[k])[:60])
|
||||
elif kind == "settlement":
|
||||
run.on_settlement(e)
|
||||
elif kind == "battle_end":
|
||||
run.on_battle_end(e)
|
||||
elif kind == "level_end":
|
||||
run.on_level_end(e)
|
||||
# action / ux / diagnosis 等其余 kind:本工具当前无消费面,静默跳过
|
||||
|
||||
|
||||
def resolve_inputs(arg):
|
||||
"""文件 / 目录 / 通配 → 排序后的 playtest_*.jsonl 列表。"""
|
||||
if os.path.isdir(arg):
|
||||
return sorted(glob.glob(os.path.join(arg, "playtest_*.jsonl")))
|
||||
if any(ch in arg for ch in "*?["):
|
||||
return sorted(glob.glob(arg))
|
||||
return [arg]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 聚合与渲染
|
||||
|
||||
def fmt(x, nd=1):
|
||||
return "—" if x is None else f"{x:.{nd}f}"
|
||||
|
||||
|
||||
def aggregate_levels(runs):
|
||||
"""关卡级:按(关卡, 档位)聚合。"""
|
||||
groups = defaultdict(list)
|
||||
for r in runs:
|
||||
groups[(r.level_index, r.policy)].append(r)
|
||||
rows = []
|
||||
for (lvl, policy), rs in sorted(groups.items(), key=lambda kv: (str(kv[0][0]), str(kv[0][1]))):
|
||||
n = len(rs)
|
||||
wins = sum(1 for r in rs if r.result == "victory")
|
||||
boss_rs = [r for r in rs if r.boss_enter_hp() is not None]
|
||||
win_rs = [r for r in rs if r.result == "victory"]
|
||||
rows.append({
|
||||
"level": lvl, "policy": policy or "—", "runs": n,
|
||||
"clear_rate": wins / n if n else None,
|
||||
"avg_rounds": sum(r.rounds_total for r in rs) / n if n else None,
|
||||
"avg_boss_hp": (sum(r.boss_enter_hp() for r in boss_rs) / len(boss_rs)) if boss_rs else None,
|
||||
"avg_end_hp": (sum(r.end_hp for r in win_rs) / len(win_rs)) if win_rs else None,
|
||||
"avg_dmg": sum(r.damage_total for r in rs) / n if n else None,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def aggregate_battles(runs):
|
||||
"""战斗级:按(关卡, 节点类型, 深度)分组,普通场与 Boss 场分开对照承伤带。"""
|
||||
groups = defaultdict(list)
|
||||
for r in runs:
|
||||
for b in r.battles:
|
||||
if b.max_hp:
|
||||
groups[(r.level_index, b.node_type, b.depth)].append(b)
|
||||
rows = []
|
||||
for (lvl, ntype, depth), bs in sorted(
|
||||
groups.items(), key=lambda kv: (str(kv[0][0]), kv[0][1], str(kv[0][2]))):
|
||||
n = len(bs)
|
||||
avg_dmg = sum(b.damage for b in bs) / n
|
||||
avg_ratio = sum(b.damage / b.max_hp for b in bs) / n
|
||||
band = BAND_BOSS if ntype == "boss" else BAND_NORMAL
|
||||
rows.append({
|
||||
"level": lvl, "type": ntype, "depth": depth, "count": n,
|
||||
"avg_dmg": avg_dmg, "avg_ratio": avg_ratio,
|
||||
"band": band, "pass": avg_ratio <= band,
|
||||
"avg_rounds": sum(len(b.rounds) for b in bs) / n,
|
||||
"avg_attack_rounds": sum(b.attack_rounds for b in bs) / n,
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def aggregate_rounds(runs):
|
||||
t = sum(r.rounds_total for r in runs)
|
||||
e = sum(r.rounds_with_enemy for r in runs)
|
||||
a = sum(r.attack_landed_rounds for r in runs)
|
||||
zn = sum(r.zero_narrow for r in runs)
|
||||
# 宽口径分母 = 有过敌人的战斗的全部轮数(含敌人当轮被歼的收尾轮)
|
||||
enemy_battle_rounds = 0
|
||||
zw = 0
|
||||
for r in runs:
|
||||
for b in r.battles:
|
||||
if getattr(b, "enemy_flag", False):
|
||||
enemy_battle_rounds += len(b.rounds)
|
||||
zw += sum(1 for rec in b.rounds if rec["zero_wide"])
|
||||
return {"rounds": t, "with_enemy": e, "attack_landed": a,
|
||||
"zero_narrow": zn, "zero_loss_wide": zw,
|
||||
"enemy_battle_rounds": enemy_battle_rounds}
|
||||
|
||||
|
||||
def derive_tier(roll, ac):
|
||||
"""缺口 = AC − 掷点读数 → 档位(1-3/4-7/8+ → 75/50/25%,达标=全额)。"""
|
||||
gap = ac - roll
|
||||
if gap <= 0:
|
||||
return "全额"
|
||||
if gap <= 3:
|
||||
return "×0.75"
|
||||
if gap <= 7:
|
||||
return "×0.5"
|
||||
return "×0.25"
|
||||
|
||||
|
||||
def aggregate_dice(res_files):
|
||||
"""掷点面(AC 批落地后激活)。字段名以 FIELD_CANDIDATES 命中为准。"""
|
||||
rolls = [] # (direction, roll, tier, crit)
|
||||
for res in res_files:
|
||||
for run in res.runs:
|
||||
for row in run.roll_rows:
|
||||
roll = _first(row, ROLL_FIELD_CANDIDATES)
|
||||
if not isinstance(roll, int):
|
||||
continue
|
||||
direction = "主角→敌" if row.get("source") == "hero_attack" else "敌→主角"
|
||||
ac = _first(row, AC_FIELD_CANDIDATES)
|
||||
ac = int(ac) if isinstance(ac, int) else 10 # 基线 AC10
|
||||
tier_v = _first(row, TIER_FIELD_CANDIDATES)
|
||||
tier = tier_v if tier_v in TIER_MULTIPLIER else derive_tier(roll, ac)
|
||||
crit_v = _first(row, CRIT_FIELD_CANDIDATES)
|
||||
crit = bool(crit_v) if crit_v is not None else (roll == 20)
|
||||
rolls.append((direction, roll, tier, crit))
|
||||
if not rolls:
|
||||
return None
|
||||
hist = Counter(r for _, r, _, _ in rolls)
|
||||
tiers = Counter(t for _, _, t, _ in rolls)
|
||||
crits = sum(1 for *_, c in rolls if c)
|
||||
ev_red = 1 - sum(TIER_MULTIPLIER[t] for _, _, t, _ in rolls) / len(rolls)
|
||||
by_dir = defaultdict(list)
|
||||
for d, r, t, c in rolls:
|
||||
by_dir[d].append((r, t, c))
|
||||
return {"hist": hist, "tiers": tiers, "n": len(rolls),
|
||||
"crit_rate": crits / len(rolls), "ev_reduction": ev_red, "by_dir": by_dir}
|
||||
|
||||
|
||||
def render_dice(dice, out):
|
||||
print("== 掷点面(AC 批验收统计)==", file=out)
|
||||
if dice is None:
|
||||
print(" 不适用(AC 批前日志):本日志无掷点字段。", file=out)
|
||||
print(" 其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。", file=out)
|
||||
return
|
||||
print(f" 样本 {dice['n']} 次掷点", file=out)
|
||||
print(" 掷点直方图(d20):", file=out)
|
||||
for v in range(1, 21):
|
||||
c = dice["hist"].get(v, 0)
|
||||
if c:
|
||||
print(f" {v:2d}: {'#' * min(c, 60)}{c if c > 60 else ''}", file=out)
|
||||
n = dice["n"]
|
||||
print(" 档位占比:", file=out)
|
||||
for t in ("全额", "×0.75", "×0.5", "×0.25"):
|
||||
c = dice["tiers"].get(t, 0)
|
||||
print(f" {t:>6}: {c:6d} ({c / n * 100:5.1f}%)", file=out)
|
||||
cr = dice["crit_rate"]
|
||||
ok = abs(cr - THEORY_CRIT_RATE) <= CRIT_TOLERANCE
|
||||
print(f" nat20 暴击率: {cr * 100:.2f}%(理论 {THEORY_CRIT_RATE * 100:.0f}% ±{CRIT_TOLERANCE * 100:.0f}pp)"
|
||||
f" → {'健康' if ok else '超界,需排查'}", file=out)
|
||||
print(f" 观察 EV 减免: {dice['ev_reduction'] * 100:.2f}%"
|
||||
f"(理论 AC10 = {THEORY_EV_REDUCTION_AC10 * 100:.2f}%)", file=out)
|
||||
print(" 按攻击方向:", file=out)
|
||||
for d, items in sorted(dice["by_dir"].items()):
|
||||
rr = [r for r, _, _ in items]
|
||||
cc = sum(1 for *_, c in items if c)
|
||||
red = 1 - sum(TIER_MULTIPLIER[t] for _, t, _ in items) / len(items)
|
||||
print(f" {d}: n={len(items)} 均值掷点={sum(rr) / len(rr):.2f} "
|
||||
f"暴击率={cc / len(items) * 100:.2f}% EV减免={red * 100:.2f}%", file=out)
|
||||
|
||||
|
||||
def render(res_files, args, out):
|
||||
total_lines = sum(r.lines for r in res_files)
|
||||
total_secs = sum(r.parse_seconds for r in res_files)
|
||||
all_runs = [run for res in res_files for run in res.runs]
|
||||
|
||||
print("== 输入概况 ==", file=out)
|
||||
print(f" 文件 {len(res_files)} 个,共 {total_lines} 行,解析 {total_secs:.2f}s"
|
||||
f"(大文件单文件耗时见下)", file=out)
|
||||
for res in res_files:
|
||||
dice_tag = "掷点面:激活" if res.dice_active else "掷点面:本日志无掷点字段(AC 批前日志,已降级)"
|
||||
print(f" {os.path.basename(res.path)}: {res.lines} 行 / {res.parse_seconds:.2f}s / "
|
||||
f"{len(res.runs)} 局 / {dice_tag}", file=out)
|
||||
print(file=out)
|
||||
|
||||
render_dice(aggregate_dice(res_files), out)
|
||||
print(file=out)
|
||||
|
||||
print("== 关卡级统计 ==", file=out)
|
||||
print(" 口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;", file=out)
|
||||
print(" 进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。", file=out)
|
||||
hdr = f" {'关卡':>6} {'档位':>8} {'局数':>5} {'通关率':>7} {'平均轮数':>8} {'进BossHP均':>10} {'通关剩余HP均':>12} {'整局承伤均':>10}"
|
||||
print(hdr, file=out)
|
||||
for row in aggregate_levels(all_runs):
|
||||
print(f" {str(row['level']):>6} {row['policy']:>8} {row['runs']:>5} "
|
||||
f"{fmt(row['clear_rate'] * 100 if row['clear_rate'] is not None else None):>6}% "
|
||||
f"{fmt(row['avg_rounds']):>8} {fmt(row['avg_boss_hp']):>10} "
|
||||
f"{fmt(row['avg_end_hp']):>12} {fmt(row['avg_dmg']):>10}", file=out)
|
||||
print(file=out)
|
||||
|
||||
print("== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)==", file=out)
|
||||
print(f" {'关卡':>6} {'类型':>6} {'深度':>4} {'场数':>5} {'场均轮数':>8} {'场均承伤':>8} {'承伤/max_hp':>11} {'承伤带':>7} {'判定':>4}", file=out)
|
||||
for row in aggregate_battles(all_runs):
|
||||
print(f" {str(row['level']):>6} {row['type']:>6} {str(row['depth']):>4} {row['count']:>5} "
|
||||
f"{fmt(row['avg_rounds']):>8} {fmt(row['avg_dmg']):>8} {fmt(row['avg_ratio'] * 100):>10}% "
|
||||
f"{'≤' + format(row['band'] * 100, '.0f') + '%':>7} {'✓' if row['pass'] else '✗':>4}", file=out)
|
||||
print(file=out)
|
||||
|
||||
rr = aggregate_rounds(all_runs)
|
||||
print("== 轮级统计 ==", file=out)
|
||||
print(f" 总轮数 {rr['rounds']}|敌战轮(有过敌人的战斗全部轮数){rr['enemy_battle_rounds']}|有攻击落地轮 {rr['attack_landed']}", file=out)
|
||||
zn_pct = rr['zero_narrow'] / rr['attack_landed'] * 100 if rr['attack_landed'] else 0.0
|
||||
ebr = rr['enemy_battle_rounds']
|
||||
zw_pct = rr['zero_loss_wide'] / ebr * 100 if ebr else 0.0
|
||||
print(f" 零承伤轮(窄口径,AC 验收主指标): {rr['zero_narrow']} / 有攻击落地轮 {rr['attack_landed']}"
|
||||
f"({zn_pct:.1f}%)——AC 批落地含'伤害下限 1'后该值应趋零", file=out)
|
||||
print(f" 零掉血轮(宽口径,参考): {rr['zero_loss_wide']} / 敌战轮 {ebr}"
|
||||
f"({zw_pct:.1f}%)", file=out)
|
||||
print(file=out)
|
||||
|
||||
# 增益战斗期复原
|
||||
buff_totals = Counter()
|
||||
buff_settlements = 0
|
||||
for run in all_runs:
|
||||
seen_in_run = False
|
||||
for rec in run.round_ledger:
|
||||
for k, v in rec["buffs"].items():
|
||||
buff_totals[k] += v
|
||||
seen_in_run = True
|
||||
if seen_in_run:
|
||||
buff_settlements += sum(1 for rec in run.round_ledger if rec["buffs"])
|
||||
snap_total = sum(r.snapshot_buffs_total for r in all_runs)
|
||||
snap_nonempty = sum(r.snapshot_buffs_nonempty for r in all_runs)
|
||||
print("== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)==", file=out)
|
||||
print(f" 各增益出现总层数: {dict(buff_totals) if buff_totals else '(无 effect 行)'}", file=out)
|
||||
print(f" 有增益激活的结算数: {buff_settlements}(每结算层数明细见 --rounds 流水)", file=out)
|
||||
print(f" 快照 buffs 非空: {snap_nonempty}/{snap_total}"
|
||||
f"(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露)", file=out)
|
||||
print(file=out)
|
||||
|
||||
if args.per_run:
|
||||
print("== 逐局明细(--per-run)==", file=out)
|
||||
print(f" {'文件':<28} {'种子':>6} {'档位':>8} {'结果':>8} {'轮数':>4} {'承伤':>5} {'零承伤轮':>8} {'进BossHP':>8} {'终局HP':>6}", file=out)
|
||||
for run in all_runs:
|
||||
print(f" {run.src_file:<28} {str(run.seed):>6} {run.policy or '—':>8} "
|
||||
f"{run.result or '—':>8} {run.rounds_total:>4} {run.damage_total:>5} "
|
||||
f"{run.zero_narrow:>8} {fmt(run.boss_enter_hp(), 0):>8} {str(run.end_hp):>6}", file=out)
|
||||
print(file=out)
|
||||
|
||||
if args.rounds:
|
||||
print("== 逐轮流水(--rounds)==", file=out)
|
||||
print(" 列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路", file=out)
|
||||
for run in all_runs:
|
||||
for rec in run.round_ledger:
|
||||
buffs = ",".join(f"{k}×{v}" for k, v in sorted(rec["buffs"].items())) or "—"
|
||||
print(f" {run.seed} {rec['type']} {rec['depth']} r{rec['round']} | "
|
||||
f"{rec['attacks']} {rec['charges']} {rec['dmg']} {rec['heal']} {rec['hp']:>3} | "
|
||||
f"{'Y' if rec['zero_narrow'] else '-'} {'Y' if rec['zero_wide'] else '-'} | "
|
||||
f"{buffs} | {rec['path']}", file=out)
|
||||
print(file=out)
|
||||
|
||||
print("== 数据质量 ==", file=out)
|
||||
bad = sum(r.bad_lines for r in res_files)
|
||||
late = sum(run.late_battle_ends for res in res_files for run in res.runs)
|
||||
print(f" 解析失败行: {bad}|未配对 battle_end(延迟发出越局): {late}", file=out)
|
||||
if res_files and any(res.unknown_step_keys for res in res_files):
|
||||
print(" step 行未知字段观测面(AC 批落批后的适配入口——新掷点字段会先出现在这里):", file=out)
|
||||
for res in res_files:
|
||||
for k, c in res.unknown_step_keys.most_common():
|
||||
print(f" {os.path.basename(res.path)}: {k} ×{c} 样例 {res.unknown_step_samples[k]}", file=out)
|
||||
else:
|
||||
print(" step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段)", file=out)
|
||||
print(" AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。", file=out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- 入口
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description="试玩 JSONL 日志三级统计分析器(关卡/战斗/轮次 + AC 掷点预留面)")
|
||||
ap.add_argument("input", help="jsonl 文件 | 目录 | 通配(目录按 playtest_*.jsonl 收取)")
|
||||
ap.add_argument("--per-run", action="store_true", help="附加逐局明细")
|
||||
ap.add_argument("--rounds", action="store_true", help="附加逐轮流水")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
files = resolve_inputs(args.input)
|
||||
files = [f for f in files if os.path.isfile(f)]
|
||||
if not files:
|
||||
print(f"未找到输入:{args.input}", file=sys.stderr)
|
||||
return 2
|
||||
res_files = [parse_file(f) for f in files]
|
||||
render(res_files, args, sys.stdout)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,99 @@
|
||||
# log_analysis.py 指标口径说明
|
||||
|
||||
试玩日志分析器(`tools/log_analysis.py`,Python 3 标准库单文件)的指标定义、
|
||||
自验对账记录与 AC 掷点批适配点。schema 事实源 = `presentation/playtest_logger.gd`。
|
||||
|
||||
## 用法
|
||||
|
||||
```
|
||||
python log_analysis.py <jsonl 文件 | 目录 | 通配> [--per-run] [--rounds]
|
||||
```
|
||||
|
||||
- 目录 = 收取其下全部 `playtest_*.jsonl`;通配/目录 = 批量汇总(跨文件聚合)。
|
||||
- `--per-run` 附加逐局明细表;`--rounds` 附加逐轮流水(含每轮增益复原)。
|
||||
|
||||
## 局的切分
|
||||
|
||||
- 批量日志一文件多局:按 `run` 行切局(run 行携带该局种子/档位)。
|
||||
- 旧日志无 `run` 行:整文件一局,种子取 `session` 行。
|
||||
- 无 `session`/`run` 头的残缺文件:兜底开局,数据不丢。
|
||||
|
||||
## 关卡级指标
|
||||
|
||||
| 指标 | 定义 |
|
||||
| --- | --- |
|
||||
| 局数 | run 行切出的局数(旧日志 = 文件数)。 |
|
||||
| 通关率 | `level_end.result == "victory"` 的局占比。 |
|
||||
| 平均轮数/局 | 每局 `settlement` 快照数(=该局回合总数,快照每结算必发)。 |
|
||||
| 进 Boss HP 均 | **快照反推法**:Boss 首结算快照 HP + 首轮承伤 − 首轮治疗。快照是回合末状态,用首轮回合内净额反推进场 HP,精确且自包含;仅统计到达 Boss 的局。 |
|
||||
| 通关剩余 HP 均 | 通关局 `level_end.hp` 均值。 |
|
||||
| 整局承伤均 | 每局全部敌方落地伤害之和的均值(见战斗级"承伤")。 |
|
||||
|
||||
## 战斗级指标
|
||||
|
||||
- 分组键 =(关卡,节点类型,节点深度);`battle` 与 `boss` 分开输出。
|
||||
- **承伤** = 该场全部 `step` 行中 `source=enemy_action` 且 `type=damage` 的 `value` 合计
|
||||
(即敌方攻击实际落地伤害;含"被减伤归零"的 0 值行)。
|
||||
- **占生效 max_hp 比** = 承伤 ÷ 该场快照 `hero.max_hp`("生效"指取快照实时值,非写死 50)。
|
||||
- 承伤带判定:普通场 ≤20%、Boss 场 ≤50%,均值超带在"判定"列标 ✗。
|
||||
- `battle_end` 行由核心延迟发出(落在下一 node 行之后)且不带深度,本工具仅按 FIFO
|
||||
对账其配对数(数据质量面),核心统计不依赖它——战斗上下文以 node 行开、相邻 node/局末收。
|
||||
|
||||
## 轮级指标(AC 批验收硬指标所在)
|
||||
|
||||
- **零承伤轮(窄口径,验收主指标)**:本轮**有敌方攻击落地**(≥1 行 `enemy_action`×`damage`)
|
||||
且主角掉血为 0(该轮敌方落地伤害合计 = 0)。分母 = 有攻击落地的轮数。
|
||||
- 蓄力拍(`type=charge`,含空拍/踩雷白喂)**不计**为"有攻击落地"——口径硬性排除。
|
||||
- AC 批落地含"伤害下限 1"后,"被减伤归零"型轮将变为 1 点实伤,该值应趋零。
|
||||
- **零掉血轮(宽口径,参考)**:本轮主角 HP 不降(首回合按进场反推 HP 差分,
|
||||
其后按相邻快照差)。分母 = **敌战轮**——"有过敌人的战斗"(任一轮出现敌人或攻击落地)
|
||||
的全部轮数。不能按单轮快照 enemies 判定:敌人当轮被歼的收尾轮快照 enemies 为空,
|
||||
但该轮仍属敌战(5678 局按单轮判定会得 4/7,按敌战轮才是 8/11)。
|
||||
|
||||
## 战斗期增益复原(盲区补偿面)
|
||||
|
||||
- **事实**:`settlement` 快照在衰减段之后发出、且现有状态寿命全部为 1 结算
|
||||
→ 快照 `buffs` 字段恒为空(本次自验 7,473 个快照 0 非空)。**禁止把快照 buffs
|
||||
当战斗期状态使用**。
|
||||
- **复原口径**:每结算的"轮窗口"= 上一 `settlement` 之后至本 `settlement` 之间的全部
|
||||
`step` 行;窗口内 `type=effect` 的行按显示文本聚为增益键(增伤→attack_up、
|
||||
削弱→weaken、减伤→defend_up;未登记文本原样作键,如 4200 批浮现的 失衡/暴走/乏力/敌方伏击+2 层),
|
||||
行数即层数。逐层数明细看 `--rounds` 流水的"增益复原"列。
|
||||
- 快照 buffs 非空计数仍照常输出(当前恒 0),作为盲区持续暴露面:将来衰减时序改动
|
||||
若使快照变非空,这里会第一个显示出来。
|
||||
|
||||
## 掷点面(AC 批预留,当前降级)
|
||||
|
||||
- **激活条件**:step 行出现候选掷点字段(`ROLL/TIER/CRIT/AC_FIELD_CANDIDATES`)。
|
||||
- 字段存在 → 输出:d20 直方图、档位占比(缺口=AC−读数:达标=全额,1-3→×0.75,
|
||||
4-7→×0.5,8+→×0.25;优先读落批直供的档位字段)、nat20 暴击率(理论 5% ±2pp 健康检查)、
|
||||
观察 EV 减免 vs 理论值(AC10 → 21.25%)、按攻击方向(主角→敌 / 敌→主角)分组。
|
||||
- 字段不存在(AC 批前日志)→ 显示"不适用(AC 批前日志)",其余指标照常。
|
||||
- **落批适配入口(一行)**:【数据质量】节的"step 行未知字段观测面"会自动列出全部新键及样例;
|
||||
把真实字段名补进 `FIELD_CANDIDATES` 常量即可,其余零改动。
|
||||
|
||||
## 已登记差异(等落批后核对)
|
||||
|
||||
1. 任务背景称 step 行含分量字段 `baseline/attach/normal/pierce/magic`——
|
||||
**现行日志(2026-09-16 schema)没有这些字段**:`_on_step_row` 只落
|
||||
`kind/type/source/cell/round/text/value/highlight` 八键。本工具的未知键观测面
|
||||
会在它们出现时自动捕获;届时按落批语义接入,不动 AC 批范围内文件。
|
||||
2. 掷点字段名以落批实际为准(候选名单只作探测,不作断言)。
|
||||
|
||||
## 自验对账记录(2026-09-16,真实日志)
|
||||
|
||||
| 局 | 人工对账 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 5678(perfect 通关) | 整关掉血合计 = 深度2 场 1 + 深度3 场 1 + Boss 场 4 = **6** | 工具输出 6 ✓ |
|
||||
| 5678 | 零掉血轮宽口径:敌战 11 轮 − 掉血 3 轮 = **8/11** | 工具输出 8/11 ✓ |
|
||||
| 5678 | 零承伤轮窄口径:被减伤归零型 3 轮(深度1 r1 / Boss r1 / Boss r4) | 工具输出 3 ✓ |
|
||||
| 1234(perfect 通关) | 独立脚本计数:敌方伤害 12、治疗 8、11 轮(HP 守恒 50−12+8=50) | 工具输出 12/11/终局 50 ✓ |
|
||||
| 4200 批(233,254 行) | run 行 600、level_end 600(500 胜 100 败) | 切 600 局,normal 66.7% + perfect 100% = 500 胜 ✓ |
|
||||
|
||||
- 大文件全量解析耗时:**233,254 行 / 1.35–1.53s**(python 3.13,单遍流式,含 json 解析与全部聚合)。
|
||||
- `battle_end` FIFO 对账:两种子局 + 600 局批均 0 未配对。
|
||||
|
||||
## 边界
|
||||
|
||||
- 本工具只读日志,不写任何游戏文件;与 `playtest_logger.gd` 的字段集对齐以 2026-09-16 版为准。
|
||||
- 快照 buffs、`battle_end`、`action`/`ux` 行族的更多消费面留待需要时扩展。
|
||||
@@ -0,0 +1,37 @@
|
||||
== 输入概况 ==
|
||||
文件 1 个,共 233254 行,解析 1.35s(大文件单文件耗时见下)
|
||||
playtest_4200_20260916T003953.jsonl: 233254 行 / 1.35s / 600 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级)
|
||||
|
||||
== 掷点面(AC 批验收统计)==
|
||||
不适用(AC 批前日志):本日志无掷点字段。
|
||||
其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。
|
||||
|
||||
== 关卡级统计 ==
|
||||
口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;
|
||||
进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。
|
||||
关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均
|
||||
1 normal 300 66.7% 13.0 67.7 45.7 65.1
|
||||
1 perfect 300 100.0% 11.8 75.0 73.7 8.5
|
||||
|
||||
== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)==
|
||||
关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定
|
||||
1 battle 0 600 1.8 5.1 7.7% ≤20% ✓
|
||||
1 battle 1 598 2.2 5.8 8.7% ≤20% ✓
|
||||
1 battle 2 589 1.7 2.2 3.2% ≤20% ✓
|
||||
1 battle 3 584 2.5 4.4 6.5% ≤20% ✓
|
||||
1 boss 4 562 4.7 20.8 29.3% ≤50% ✓
|
||||
|
||||
== 轮级统计 ==
|
||||
总轮数 7449|敌战轮(有过敌人的战斗全部轮数)6988|有攻击落地轮 3749
|
||||
零承伤轮(窄口径,AC 验收主指标): 1334 / 有攻击落地轮 3749(35.6%)——AC 批落地含'伤害下限 1'后该值应趋零
|
||||
零掉血轮(宽口径,参考): 4259 / 敌战轮 6988(60.9%)
|
||||
|
||||
== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)==
|
||||
各增益出现总层数: {'失衡': 2821, 'defend_up': 8180, 'weaken': 8103, '暴走': 3287, 'attack_up': 9893, '乏力': 2075, '敌方伏击 +2 层': 2459}
|
||||
有增益激活的结算数: 7396(每结算层数明细见 --rounds 流水)
|
||||
快照 buffs 非空: 0/7449(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露)
|
||||
|
||||
== 数据质量 ==
|
||||
解析失败行: 0|未配对 battle_end(延迟发出越局): 0
|
||||
step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段)
|
||||
AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。
|
||||
@@ -0,0 +1,54 @@
|
||||
== 输入概况 ==
|
||||
文件 1 个,共 340 行,解析 0.00s(大文件单文件耗时见下)
|
||||
playtest_1234_20260916T004132.jsonl: 340 行 / 0.00s / 1 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级)
|
||||
|
||||
== 掷点面(AC 批验收统计)==
|
||||
不适用(AC 批前日志):本日志无掷点字段。
|
||||
其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。
|
||||
|
||||
== 关卡级统计 ==
|
||||
口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;
|
||||
进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。
|
||||
关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均
|
||||
1 perfect 1 100.0% 11.0 50.0 50.0 12.0
|
||||
|
||||
== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)==
|
||||
关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定
|
||||
1 battle 0 1 2.0 0.0 0.0% ≤20% ✓
|
||||
1 battle 1 1 2.0 0.0 0.0% ≤20% ✓
|
||||
1 battle 2 1 2.0 0.0 0.0% ≤20% ✓
|
||||
1 battle 3 1 2.0 8.0 16.0% ≤20% ✓
|
||||
1 boss 4 1 3.0 4.0 8.0% ≤50% ✓
|
||||
|
||||
== 轮级统计 ==
|
||||
总轮数 11|敌战轮(有过敌人的战斗全部轮数)11|有攻击落地轮 6
|
||||
零承伤轮(窄口径,AC 验收主指标): 4 / 有攻击落地轮 6(66.7%)——AC 批落地含'伤害下限 1'后该值应趋零
|
||||
零掉血轮(宽口径,参考): 9 / 敌战轮 11(81.8%)
|
||||
|
||||
== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)==
|
||||
各增益出现总层数: {'attack_up': 18, 'defend_up': 15, 'weaken': 14}
|
||||
有增益激活的结算数: 11(每结算层数明细见 --rounds 流水)
|
||||
快照 buffs 非空: 0/11(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露)
|
||||
|
||||
== 逐局明细(--per-run)==
|
||||
文件 种子 档位 结果 轮数 承伤 零承伤轮 进BossHP 终局HP
|
||||
playtest_1234_20260916T004132.jsonl 1234 perfect victory 11 12 4 50 50
|
||||
|
||||
== 逐轮流水(--rounds)==
|
||||
列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路
|
||||
1234 battle 0 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×2,weaken×2 | ap_exhausted
|
||||
1234 battle 0 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted
|
||||
1234 battle 1 r1 | 2 0 0 0 50 | Y Y | attack_up×2,weaken×1 | ap_exhausted
|
||||
1234 battle 1 r2 | 0 0 0 0 50 | - Y | attack_up×2,defend_up×2 | ap_exhausted
|
||||
1234 battle 2 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×2 | ap_exhausted
|
||||
1234 battle 2 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×1,weaken×3 | ap_exhausted
|
||||
1234 battle 3 r1 | 1 0 8 0 42 | - - | attack_up×2 | ap_exhausted
|
||||
1234 battle 3 r2 | 0 0 0 4 46 | - Y | attack_up×2,defend_up×2,weaken×2 | ap_exhausted
|
||||
1234 boss 4 r1 | 1 0 0 0 50 | Y Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted
|
||||
1234 boss 4 r2 | 1 0 4 0 46 | - - | attack_up×2,defend_up×1 | ap_exhausted
|
||||
1234 boss 4 r3 | 0 0 0 4 50 | - Y | attack_up×1,defend_up×2,weaken×2 | cleared
|
||||
|
||||
== 数据质量 ==
|
||||
解析失败行: 0|未配对 battle_end(延迟发出越局): 0
|
||||
step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段)
|
||||
AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。
|
||||
@@ -0,0 +1,55 @@
|
||||
== 输入概况 ==
|
||||
文件 1 个,共 380 行,解析 0.00s(大文件单文件耗时见下)
|
||||
playtest_5678_20260916T004134.jsonl: 380 行 / 0.00s / 1 局 / 掷点面:本日志无掷点字段(AC 批前日志,已降级)
|
||||
|
||||
== 掷点面(AC 批验收统计)==
|
||||
不适用(AC 批前日志):本日志无掷点字段。
|
||||
其余指标照常输出;落批后重跑本工具,先看【数据质量】的未知字段观测面。
|
||||
|
||||
== 关卡级统计 ==
|
||||
口径:局=run 行切分(无 run 行的旧日志整文件一局);通关=result=victory;
|
||||
进BossHP=Boss首结算快照HP+首轮承伤−首轮治疗(精确反推);平均轮数=每局settlement数。
|
||||
关卡 档位 局数 通关率 平均轮数 进BossHP均 通关剩余HP均 整局承伤均
|
||||
1 perfect 1 100.0% 12.0 50.0 50.0 6.0
|
||||
|
||||
== 战斗级统计(按 关卡/节点类型/深度 分组;对照承伤带 普通≤20%、Boss≤50% max_hp)==
|
||||
关卡 类型 深度 场数 场均轮数 场均承伤 承伤/max_hp 承伤带 判定
|
||||
1 battle 0 1 1.0 0.0 0.0% ≤20% ✓
|
||||
1 battle 1 1 2.0 0.0 0.0% ≤20% ✓
|
||||
1 battle 2 1 2.0 1.0 2.0% ≤20% ✓
|
||||
1 battle 3 1 2.0 1.0 2.0% ≤20% ✓
|
||||
1 boss 4 1 5.0 4.0 8.0% ≤50% ✓
|
||||
|
||||
== 轮级统计 ==
|
||||
总轮数 12|敌战轮(有过敌人的战斗全部轮数)11|有攻击落地轮 6
|
||||
零承伤轮(窄口径,AC 验收主指标): 3 / 有攻击落地轮 6(50.0%)——AC 批落地含'伤害下限 1'后该值应趋零
|
||||
零掉血轮(宽口径,参考): 8 / 敌战轮 11(72.7%)
|
||||
|
||||
== 增益战斗期复原(来自 step 行,按结算轮窗口聚合)==
|
||||
各增益出现总层数: {'weaken': 18, 'defend_up': 12, 'attack_up': 21}
|
||||
有增益激活的结算数: 12(每结算层数明细见 --rounds 流水)
|
||||
快照 buffs 非空: 0/12(已知盲区:快照在衰减段后发出且状态寿命=1 结算 → 恒空;此行仅作盲区持续暴露)
|
||||
|
||||
== 逐局明细(--per-run)==
|
||||
文件 种子 档位 结果 轮数 承伤 零承伤轮 进BossHP 终局HP
|
||||
playtest_5678_20260916T004134.jsonl 5678 perfect victory 12 6 3 50 50
|
||||
|
||||
== 逐轮流水(--rounds)==
|
||||
列: 种子 节点 深度 轮 | 攻击落地 蓄力 承伤 治疗 结算HP | 零承伤 零掉血 | 增益复原 | 出路
|
||||
5678 battle 0 r1 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×2,weaken×1 | ap_exhausted
|
||||
5678 battle 1 r1 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×3 | ap_exhausted
|
||||
5678 battle 1 r2 | 0 0 0 0 50 | - Y | attack_up×1,defend_up×1,weaken×1 | ap_exhausted
|
||||
5678 battle 2 r1 | 1 0 1 0 49 | - - | attack_up×2,defend_up×2,weaken×1 | ap_exhausted
|
||||
5678 battle 2 r2 | 0 0 0 1 50 | - Y | attack_up×2,defend_up×1,weaken×3 | ap_exhausted
|
||||
5678 battle 3 r1 | 1 0 1 0 49 | - - | attack_up×2,weaken×1 | ap_exhausted
|
||||
5678 battle 3 r2 | 0 0 0 1 50 | - Y | attack_up×1,defend_up×2,weaken×3 | ap_exhausted
|
||||
5678 boss 4 r1 | 1 0 0 0 50 | Y Y | attack_up×2,weaken×2 | ap_exhausted
|
||||
5678 boss 4 r2 | 1 0 4 0 46 | - - | attack_up×2,defend_up×2 | ap_exhausted
|
||||
5678 boss 4 r3 | 0 1 0 4 50 | - Y | attack_up×2,weaken×1 | ap_exhausted
|
||||
5678 boss 4 r4 | 1 0 0 0 50 | Y Y | attack_up×2,defend_up×1,weaken×2 | ap_exhausted
|
||||
5678 boss 4 r5 | 0 0 0 0 50 | - Y | attack_up×2 | ap_exhausted
|
||||
|
||||
== 数据质量 ==
|
||||
解析失败行: 0|未配对 battle_end(延迟发出越局): 0
|
||||
step 行未知字段: 无(当前日志 = 2026-09-16 schema,无掷点/分量字段)
|
||||
AC 批落批后:新字段先出现在上面观测面 → 在 FIELD_CANDIDATES 登记真实字段名 → 掷点面自动激活。
|
||||
Reference in new issue
Block a user