拆件批v1.3-B3伴随(私有助手抽取与用语统一): unit_container 抽 _pool_snapshot(开战实例池快照循环体逐字迁移)与 _pct_fact_blocked(self/hero 百分比事实阈值判定——分支逐字承原,六算子统一在阈值上)、skill_table validate 抽 _validate_trigger/_validate_trigger_domain/_validate_trigger_value/_validate_basic_row 四私有段(报错顺序=报告顺序逐字承原,行缺 trigger 早退等价)、config_db_cross_validator 抽 _pool_cast_side_errs 并修复拆分稿缺陷——append_array 误落逐行循环内会按技能行数重复追加守卫报错,移回全表恰一次恢复单份报错序;curio_deck/effect_context/level_autoplay 注释与 enemy.tres/status_effects.tres 调参登记注用语统一(通关→冒险成功/关卡完成率——零数值零行为改动)。验收:全量套件 63 绿+round_flow EV 锚 goblin 已知预期红 1;bp 对拍 292dumps×28 键 mismatch=0(双基线);冒烟 17 项 0 失败;gdstyle 基线外新违规 0/恶化恰 2(存量债)〔领域拆分批 V1〕

This commit is contained in:
DKChan committed 2026-10-27 17:00:47 +08:00
1 parent 012883f301
commit 45faa92bc5
8 files changed
+115 -76

No files matched your search

+11 -5
View File
@@ -118,12 +118,9 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void:
# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。
# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表,
# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
# 施放类 side 守卫(buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接)
func _pool_cast_side_errs(skill_table: SkillTable, status_table: StatusEffectTable) -> Array[Dictionary]:
var errs: Array[Dictionary] = []
var skill_types := {}
for row in skill_table.rows:
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
# 施放类 side 守卫:buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接
for row in skill_table.rows:
var st: String = String(row.get("type", ""))
if st != "buff" and st != "debuff":
@@ -133,6 +130,15 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable,
var srow: Dictionary[Variant, Variant] = status_table.row_by_id(eff) if status_table != null else {}
if not srow.is_empty() and String(srow.get("target_side", "")) != want_side:
errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]})
return errs
func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array:
var errs: Array[Dictionary] = []
var skill_types := {}
for row in skill_table.rows:
skill_types[String(row.get("skill_id", ""))] = String(row.get("type", ""))
errs.append_array(_pool_cast_side_errs(skill_table, status_table)) # 施放类 side 守卫(buff→敌方侧/debuff→主角侧,防反向挂接——全表恰一次)
for row in enemy_table.rows:
var eid: String = String(row.get("enemy_id", ""))
var basic_n := 0
+1 -1
View File
@@ -106,7 +106,7 @@ func cap_ok(kind: String, feat_id: String = "") -> bool:
return int(taken.get(kind, 0)) < cap
# 专长凭证展示判定(四口径:一局限购一次/罕见只在概率不在价格/计步保底/触发清零。
# 专长凭证展示判定(四口径:一局限购一次/稀有只在概率不在价格/计步保底/触发清零。
# 调用即一次展示判定与记账——展示次数计 pity、配额与限购双闸)
func feat_ticket_consider(rg: RandomNumberGenerator) -> bool:
if ticket_sold >= int(db.adventure.feat_ticket_limit):
+1 -1
View File
@@ -1,6 +1,6 @@
## EffectContext——效果引擎四桶计数器引用束(无状态引擎的外部状态承载)。
## 四桶按生命周期分:round(轮始整桶清空)/battle(随编排器生灭)/level(随 LevelRunner 生灭)/
## adventure(随冒险应用层生灭——拆件批桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
## adventure(随冒险应用层生灭——桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。
class_name EffectContext
extends RefCounted
+38 -27
View File
@@ -80,22 +80,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
var eovr: int = int(enemy_atk_overrides[row_i]) if row_i < enemy_atk_overrides.size() else -1
for i in range(per_row_counts[row_i]):
var eff_row_atk: int = eovr if eovr >= 0 else int(row.atk)
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条快照纪律)
var pool_snap: Array[Dictionary] = []
for entry in row.get("pool", []):
var snap_entry := {
"skill_id": String(entry.get("skill_id", "")),
"weight": int(entry.get("weight", 0)),
"cooldown_left": 0,
}
# 触发条件(拆件批随迁:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(String(entry.get("skill_id", "")))
if srow_snap.has("trigger"):
snap_entry["trigger"] = (srow_snap.get("trigger", {}) as Dictionary).duplicate(true)
if bool((snap_entry["trigger"] as Dictionary).get("latch", false)):
snap_entry["trigger_latched"] = false
pool_snap.append(snap_entry)
var pool_snap: Array[Dictionary] = _pool_snapshot(row) # 实例池快照(开战模板快照——纪律见助手头注)
_enemies.append({
"handle_id": handle,
"enemy_id": String(row.enemy_id),
@@ -119,6 +104,26 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides
return true
# 实例池快照(proposal §2.2:开战把模板快照一份,冷却全零——与"生成拿到当时的表值快照"同一条
# 快照纪律。触发条件:技能行 trigger 是字段权威——快照随读;带 latch 行写单向闩标记
# trigger_latched 初始锁定;缺省无 trigger=无条件技)
func _pool_snapshot(row: Dictionary) -> Array[Dictionary]:
var pool_snap: Array[Dictionary] = []
for entry in row.get("pool", []):
var snap_entry := {
"skill_id": String(entry.get("skill_id", "")),
"weight": int(entry.get("weight", 0)),
"cooldown_left": 0,
}
var srow_snap: Dictionary[Variant, Variant] = db.skill_row(String(entry.get("skill_id", "")))
if srow_snap.has("trigger"):
snap_entry["trigger"] = (srow_snap.get("trigger", {}) as Dictionary).duplicate(true)
if bool((snap_entry["trigger"] as Dictionary).get("latch", false)):
snap_entry["trigger_latched"] = false
pool_snap.append(snap_entry)
return pool_snap
func query_alive(side: String) -> Array:
if _despawned:
_diag("Despawned 句柄查询(域闭)")
@@ -142,20 +147,12 @@ func _entry_blocked(e: Dictionary, u: Dictionary, facts: Dictionary) -> bool:
return false
if e.has("trigger_latched") and bool(e.get("trigger_latched", false)):
return false # 闩已过——不再拦(治疗抬血不回锁)
var t: Dictionary = e.get("trigger", {})
var t: Dictionary[Variant, Variant] = e.get("trigger", {})
var fact := String(t.get("fact", ""))
var op := String(t.get("op", ""))
var value: Variant = t.get("value", 0)
# 自类百分比事实(容器内直读——阈值式折算)
if fact == "self_hp_pct":
var threshold: int = int(round(float(int(u.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(u.hp), op, threshold)
if fact == "hero_hp_pct":
var hrow: Dictionary[Variant, Variant] = _hero
if hrow.is_empty():
return true # 无主角行——条件假(fail-safe)
var hthreshold: int = int(round(float(int(hrow.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(hrow.hp), op, hthreshold)
if fact == "self_hp_pct" or fact == "hero_hp_pct":
return _pct_fact_blocked(fact, value, op, u) # 自类/主角百分比事实(阈值式折算——v1.2)
# 全局事实(编排器组包——缺键=拦)
if not facts.has(fact):
return true
@@ -184,6 +181,20 @@ func _compare_str(lhs: String, op: String, rhs: String) -> bool:
# 可摇集合:冷却已尽的池条目投影 [{skill_id, weight}]。普通攻击冷却恒 0 → 恒非空(空池不可能的结构前提,
# 加载校验"每池恰一行普攻"兜底)。hero/失效句柄拒绝。锁定条目(半血未解锁)双面过滤不入。
# 百分比事实阈值判定(阈值式 v1.2:先折算绝对阈值 T=round(对应 max_hp × value/100)、
# 再以 hp op T 比较——六算子统一在该阈值上定义。self=容器内直读单位行;hero=主角行,
# 无主角行=条件假(拦——fail-safe))
func _pct_fact_blocked(fact: String, value: Variant, op: String, u: Dictionary) -> bool:
if fact == "self_hp_pct":
var threshold: int = int(round(float(int(u.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(u.hp), op, threshold)
var hrow: Dictionary[Variant, Variant] = _hero
if hrow.is_empty():
return true # 无主角行——条件假(fail-safe)
var hthreshold: int = int(round(float(int(hrow.max_hp)) * float(int(value)) / 100.0))
return not _compare(int(hrow.hp), op, hthreshold)
func rollable_skills(u: Dictionary, facts: Dictionary = {}) -> Array:
if u.is_empty() or _despawned or u.get("is_hero", false):
_diag("rollable_skills 失效句柄")