From 45faa92bc50e319eb4390c5164c13d31bd3b8ccf Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 27 Oct 2026 17:00:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E4=BB=B6=E6=89=B9v1.3-B3=E4=BC=B4?= =?UTF-8?q?=E9=9A=8F(=E7=A7=81=E6=9C=89=E5=8A=A9=E6=89=8B=E6=8A=BD?= =?UTF-8?q?=E5=8F=96=E4=B8=8E=E7=94=A8=E8=AF=AD=E7=BB=9F=E4=B8=80):=20unit?= =?UTF-8?q?=5Fcontainer=20=E6=8A=BD=20=5Fpool=5Fsnapshot(=E5=BC=80?= =?UTF-8?q?=E6=88=98=E5=AE=9E=E4=BE=8B=E6=B1=A0=E5=BF=AB=E7=85=A7=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E4=BD=93=E9=80=90=E5=AD=97=E8=BF=81=E7=A7=BB)?= =?UTF-8?q?=E4=B8=8E=20=5Fpct=5Ffact=5Fblocked(self/hero=20=E7=99=BE?= =?UTF-8?q?=E5=88=86=E6=AF=94=E4=BA=8B=E5=AE=9E=E9=98=88=E5=80=BC=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=E2=80=94=E2=80=94=E5=88=86=E6=94=AF=E9=80=90=E5=AD=97?= =?UTF-8?q?=E6=89=BF=E5=8E=9F,=E5=85=AD=E7=AE=97=E5=AD=90=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E5=9C=A8=E9=98=88=E5=80=BC=E4=B8=8A)=E3=80=81skill=5F?= =?UTF-8?q?table=20validate=20=E6=8A=BD=20=5Fvalidate=5Ftrigger/=5Fvalidat?= =?UTF-8?q?e=5Ftrigger=5Fdomain/=5Fvalidate=5Ftrigger=5Fvalue/=5Fvalidate?= =?UTF-8?q?=5Fbasic=5Frow=20=E5=9B=9B=E7=A7=81=E6=9C=89=E6=AE=B5(=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E9=A1=BA=E5=BA=8F=3D=E6=8A=A5=E5=91=8A=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E9=80=90=E5=AD=97=E6=89=BF=E5=8E=9F,=E8=A1=8C?= =?UTF-8?q?=E7=BC=BA=20trigger=20=E6=97=A9=E9=80=80=E7=AD=89=E4=BB=B7)?= =?UTF-8?q?=E3=80=81config=5Fdb=5Fcross=5Fvalidator=20=E6=8A=BD=20=5Fpool?= =?UTF-8?q?=5Fcast=5Fside=5Ferrs=20=E5=B9=B6=E4=BF=AE=E5=A4=8D=E6=8B=86?= =?UTF-8?q?=E5=88=86=E7=A8=BF=E7=BC=BA=E9=99=B7=E2=80=94=E2=80=94append=5F?= =?UTF-8?q?array=20=E8=AF=AF=E8=90=BD=E9=80=90=E8=A1=8C=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=86=85=E4=BC=9A=E6=8C=89=E6=8A=80=E8=83=BD=E8=A1=8C=E6=95=B0?= =?UTF-8?q?=E9=87=8D=E5=A4=8D=E8=BF=BD=E5=8A=A0=E5=AE=88=E5=8D=AB=E6=8A=A5?= =?UTF-8?q?=E9=94=99,=E7=A7=BB=E5=9B=9E=E5=85=A8=E8=A1=A8=E6=81=B0?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=81=A2=E5=A4=8D=E5=8D=95=E4=BB=BD=E6=8A=A5?= =?UTF-8?q?=E9=94=99=E5=BA=8F;curio=5Fdeck/effect=5Fcontext/level=5Fautopl?= =?UTF-8?q?ay=20=E6=B3=A8=E9=87=8A=E4=B8=8E=20enemy.tres/status=5Feffects.?= =?UTF-8?q?tres=20=E8=B0=83=E5=8F=82=E7=99=BB=E8=AE=B0=E6=B3=A8=E7=94=A8?= =?UTF-8?q?=E8=AF=AD=E7=BB=9F=E4=B8=80(=E9=80=9A=E5=85=B3=E2=86=92?= =?UTF-8?q?=E5=86=92=E9=99=A9=E6=88=90=E5=8A=9F/=E5=85=B3=E5=8D=A1?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=8E=87=E2=80=94=E2=80=94=E9=9B=B6=E6=95=B0?= =?UTF-8?q?=E5=80=BC=E9=9B=B6=E8=A1=8C=E4=B8=BA=E6=94=B9=E5=8A=A8)?= =?UTF-8?q?=E3=80=82=E9=AA=8C=E6=94=B6:=E5=85=A8=E9=87=8F=E5=A5=97?= =?UTF-8?q?=E4=BB=B6=2063=20=E7=BB=BF+round=5Fflow=20EV=20=E9=94=9A=20gobl?= =?UTF-8?q?in=20=E5=B7=B2=E7=9F=A5=E9=A2=84=E6=9C=9F=E7=BA=A2=201;bp=20?= =?UTF-8?q?=E5=AF=B9=E6=8B=8D=20292dumps=C3=9728=20=E9=94=AE=20mismatch=3D?= =?UTF-8?q?0(=E5=8F=8C=E5=9F=BA=E7=BA=BF);=E5=86=92=E7=83=9F=2017=20?= =?UTF-8?q?=E9=A1=B9=200=20=E5=A4=B1=E8=B4=A5;gdstyle=20=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E5=A4=96=E6=96=B0=E8=BF=9D=E8=A7=84=200/=E6=81=B6=E5=8C=96?= =?UTF-8?q?=E6=81=B0=202(=E5=AD=98=E9=87=8F=E5=80=BA)=E3=80=94=E9=A2=86?= =?UTF-8?q?=E5=9F=9F=E6=8B=86=E5=88=86=E6=89=B9=20V1=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/config_db_cross_validator.gd | 16 +++-- game/core/curio_deck.gd | 2 +- game/core/effect_context.gd | 2 +- game/core/unit_container.gd | 65 ++++++++++------- game/data/schemas/skill_table.gd | 98 ++++++++++++++++---------- game/data/tables/enemy.tres | 4 +- game/data/tables/status_effects.tres | 2 +- game/sim/level_autoplay.gd | 2 +- 8 files changed, 115 insertions(+), 76 deletions(-) diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index 7cc514f7..3f715f65 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -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 diff --git a/game/core/curio_deck.gd b/game/core/curio_deck.gd index 47257bbc..c69c4092 100644 --- a/game/core/curio_deck.gd +++ b/game/core/curio_deck.gd @@ -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): diff --git a/game/core/effect_context.gd b/game/core/effect_context.gd index e0fd9361..24dd509d 100644 --- a/game/core/effect_context.gd +++ b/game/core/effect_context.gd @@ -1,6 +1,6 @@ ## EffectContext——效果引擎四桶计数器引用束(无状态引擎的外部状态承载)。 ## 四桶按生命周期分:round(轮始整桶清空)/battle(随编排器生灭)/level(随 LevelRunner 生灭)/ -## adventure(随冒险应用层生灭——拆件批桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。 +## adventure(随冒险应用层生灭——桶归宿主字典)。本类零方法零行为,只持引用;桶容器由宿主对象 new,本类只挂引用。 class_name EffectContext extends RefCounted diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 50fc639a..066f5ca0 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -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 失效句柄") diff --git a/game/data/schemas/skill_table.gd b/game/data/schemas/skill_table.gd index 2150c77e..335d9664 100644 --- a/game/data/schemas/skill_table.gd +++ b/game/data/schemas/skill_table.gd @@ -44,36 +44,7 @@ func validate() -> Array: if not SKILL_TYPES.has(type): errs.append({"category": "enum", "message": "%s type 未知枚举: %s(域 %s)" % [sid, type, str(SKILL_TYPES)]}) continue - # trigger(触发条件,可省略——敌人系统 v1.2:四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动) - if row.has("trigger"): - var trig: Variant = row.get("trigger", null) - if not (trig is Dictionary): - errs.append({"category": "domain", "message": "%s trigger 须为 {fact,op,value,latch} 字典" % sid}) - else: - var t: Dictionary = trig - for tk in t.keys(): - if not TRIGGER_KEYS.has(String(tk)): - errs.append({"category": "domain", "message": "%s trigger 键白名单外: %s" % [sid, str(tk)]}) - var tf := String(t.get("fact", "")) - var top := String(t.get("op", "")) - if not TRIGGER_FACTS.has(tf): - errs.append({"category": "enum", "message": "%s trigger fact 出域: %s" % [sid, tf]}) - if not TRIGGER_OPS.has(top): - errs.append({"category": "enum", "message": "%s trigger op 出域: %s" % [sid, top]}) - if TRIGGER_ENUM_FACTS.has(tf) and not (top == "eq" or top == "neq"): - errs.append({"category": "domain", "message": "%s trigger 枚举事实 %s 算子限域 eq/neq: %s" % [sid, tf, top]}) - if tf != "" and TRIGGER_ENUM_FACTS.has(tf): - var ev = t.get("value", "") - if not (ev is String) or not TRIGGER_ENUM_VALUES.has(String(ev)): - errs.append({"category": "domain", "message": "%s trigger 枚举事实 value 越域: %s(域 %s)" % [sid, str(ev), str(TRIGGER_ENUM_VALUES)]}) - elif TRIGGER_PCT_FACTS.has(tf): - var pv := int(t.get("value", 0)) - if pv < 1 or pv > 99: - errs.append({"category": "domain", "message": "%s trigger 百分比事实 value 越域 [1,99]: %d" % [sid, pv]}) - elif tf != "" and not (t.get("value", 0) is int): - errs.append({"category": "domain", "message": "%s trigger 数值事实 value 须整数: %s" % [sid, str(t.get("value", 0))]}) - if t.has("latch") and not (t.get("latch", false) is bool): - errs.append({"category": "domain", "message": "%s trigger latch 须布尔: %s" % [sid, str(t.get("latch", false))]}) + _validate_trigger(sid, row, errs) # trigger(触发条件,可省略——四键白名单/词表出域/类型分派/普攻禁配;加载期报错止动) if type == "basic" and row.has("trigger"): errs.append({"category": "cross_field", "message": "%s 普攻行禁配 trigger(普攻恒可摇的结构前提)" % sid}) # 冷却域(全类型):施放后连续 N 拍不可摇中 @@ -83,14 +54,7 @@ func validate() -> Array: # 普通攻击两条域校验锁死特殊性:倍率恒 1、冷却恒 0——想配的就是伤害技(proposal §2.2) if type == "basic": has_basic = true - if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0): - errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]}) - if row.has("cooldown_beats") and cd != 0: - errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]}) - if row.has("buff_ref") or row.has("buff_duration"): - errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid}) - if row.has("rider_mult"): - errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid}) + _validate_basic_row(sid, row, cd, errs) # 伤害技:存倍率不存固定值——伤害 = 施放者攻击基线 × 技能倍率(三乘法链第一乘); # 可选 rider_mult = 必中附加分量(后置加法、不吃骰不出暴击——proposal-status-vocabulary §2) if type == "damage": @@ -136,6 +100,64 @@ func validate() -> Array: return errs +# trigger 域校验(敌人系统 v1.2 触发条件:四键白名单/词表出域/类型分派——枚举事实算子限域 eq/neq +# 且 value 限枚举串、百分比事实 value 域 [1,99]、数值事实 value 须整数、latch 须布尔。行缺 trigger +# 直返——可省略字段) +func _validate_trigger(sid: String, row: Dictionary, errs: Array[Dictionary]) -> void: + if not row.has("trigger"): + return + var trig: Variant = row.get("trigger", null) + if not (trig is Dictionary): + errs.append({"category": "domain", "message": "%s trigger 须为 {fact,op,value,latch} 字典" % sid}) + return + var t: Dictionary[Variant, Variant] = trig + for tk in t.keys(): + if not TRIGGER_KEYS.has(String(tk)): + errs.append({"category": "domain", "message": "%s trigger 键白名单外: %s" % [sid, str(tk)]}) + _validate_trigger_domain(t, sid, errs) + + +# trigger fact/op/value/latch 域校验(词表出域与类型分派——报错顺序=报告顺序,分派段单列) +func _validate_trigger_domain(t: Dictionary[Variant, Variant], sid: String, errs: Array[Dictionary]) -> void: + var tf := String(t.get("fact", "")) + var top := String(t.get("op", "")) + if not TRIGGER_FACTS.has(tf): + errs.append({"category": "enum", "message": "%s trigger fact 出域: %s" % [sid, tf]}) + if not TRIGGER_OPS.has(top): + errs.append({"category": "enum", "message": "%s trigger op 出域: %s" % [sid, top]}) + if TRIGGER_ENUM_FACTS.has(tf) and not (top == "eq" or top == "neq"): + errs.append({"category": "domain", "message": "%s trigger 枚举事实 %s 算子限域 eq/neq: %s" % [sid, tf, top]}) + _validate_trigger_value(sid, t, tf, errs) + if t.has("latch") and not (t.get("latch", false) is bool): + errs.append({"category": "domain", "message": "%s trigger latch 须布尔: %s" % [sid, str(t.get("latch", false))]}) + + +# trigger value 类型分派校验(枚举事实 value 限枚举串/百分比事实 value 域 [1,99]/数值事实 value 须整数) +func _validate_trigger_value(sid: String, t: Dictionary[Variant, Variant], tf: String, errs: Array[Dictionary]) -> void: + if tf != "" and TRIGGER_ENUM_FACTS.has(tf): + var ev = t.get("value", "") + if not (ev is String) or not TRIGGER_ENUM_VALUES.has(String(ev)): + errs.append({"category": "domain", "message": "%s trigger 枚举事实 value 越域: %s(域 %s)" % [sid, str(ev), str(TRIGGER_ENUM_VALUES)]}) + elif TRIGGER_PCT_FACTS.has(tf): + var pv := int(t.get("value", 0)) + if pv < 1 or pv > 99: + errs.append({"category": "domain", "message": "%s trigger 百分比事实 value 越域 [1,99]: %d" % [sid, pv]}) + elif tf != "" and not (t.get("value", 0) is int): + errs.append({"category": "domain", "message": "%s trigger 数值事实 value 须整数: %s" % [sid, str(t.get("value", 0))]}) + + +# 普攻行禁配校验(倍率恒 1、冷却恒 0、不带增益/rider 字段——「池恒非空」的结构前提) +func _validate_basic_row(sid: String, row: Dictionary, cd: int, errs: Array[Dictionary]) -> void: + if row.has("damage_mult") and not is_equal_approx(float(row.damage_mult), 1.0): + errs.append({"category": "cross_field", "message": "%s 普攻 damage_mult ≠ 1(禁配——要配就是伤害技): %s" % [sid, str(row.damage_mult)]}) + if row.has("cooldown_beats") and cd != 0: + errs.append({"category": "cross_field", "message": "%s 普攻 cooldown_beats ≠ 0(禁配——池恒非空的结构前提): %d" % [sid, cd]}) + if row.has("buff_ref") or row.has("buff_duration"): + errs.append({"category": "cross_field", "message": "%s 普攻不带增益字段" % sid}) + if row.has("rider_mult"): + errs.append({"category": "cross_field", "message": "%s 普攻不带必中附加分量(rider_mult 仅伤害技)" % sid}) + + func row_by_id(skill_id: String) -> Dictionary: for row in rows: if row.get("skill_id", "") == skill_id: diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index 9581a20e..2e549a1b 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -755,7 +755,7 @@ rows = [{ }] ; 切片调参登记 v3(2026-09-12 试玩批次 1 触发——F7 流程;C3 GDD F5 对照): -; 试玩反馈 = 基线满血全通关(模拟 80%/真人 100%)——难度严重不足,主杠杆 = atk 曲线上调(承伤侧): +; 试玩反馈 = 基线满血全冒险成功(模拟 80%/真人 100%)——难度严重不足,主杠杆 = atk 曲线上调(承伤侧): ; 4/4/6/4/7 → 7/5/9/6/9(约 +50%);HP 结构调整(D0 24→20 缩短战斗、Boss 130→150 用户拍板可上调)。 ; D0 注:20 HP 会被首轮盲挖粉尘秒杀(12+9)——回 24(4 轮战斗/承伤 42%,有过程有痛感)。 ; DND 命名(用户指令):goblin 地精(单拍杂鱼)→ bugbear 熊地精(三拍伏击环——DND 原型即伏击手)→ @@ -895,7 +895,7 @@ rows = [{ ; ; 数值修复批·调参二(2026-10-01 新尺 MC 驱动):L1 系八行 atk 2→1(行 15/35/54/77/99/121/144/166; ; 行 234 巨蜘蛛巢群主体=L2 域维持)——新尺读数:普通档 v3.4 死因=L1 四连战无治疗夹缝(雷伤 6/8/8 -; + 杂兵垫刀双压,L1 条件通关 1-2%,0/100 出第一关);完美档战役 50%(L1 93%)。刀位语义= +; + 杂兵垫刀双压,L1 条件关卡完成率 1-2%,0/100 出第一关);完美档战役 50%(L1 93%)。刀位语义= ; 「危险来自雷,不来自杂兵」:杂兵垫刀减半,雷直伤 6/8/8 维持痛感;L2/L3(atk 3 系)本刀不动。 ; 全旧带读数随新尺作废重测归本批。 ; 调参二补充:上述八行 atk 区间 1-3→0-2(中点恒等期望当量锚不变式——atk=1 需区间中点同步 1;0=掷点空刀,保随机手感)。 diff --git a/game/data/tables/status_effects.tres b/game/data/tables/status_effects.tres index 2787ec55..91f46885 100644 --- a/game/data/tables/status_effects.tres +++ b/game/data/tables/status_effects.tres @@ -97,7 +97,7 @@ rows = [{ }] ; 减伤墙调参批(2026-09-13,playtest-2026-09-12 报告 Q1 触发——defense_scan 实测墙轮 39%、 -; 真人局 12/12 轮零承伤满血通关):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽 +; 真人局 12/12 轮零承伤满血冒险成功):defense_up magnitude -50 → -30——叠满两层 = -60 减伤帽 ; (原拟 pool_clamp_lo=-60 被 schema 冻结域拦截,杠杆移至本表);石肤权重 24→16 随批(reward.tres)。 ; 承伤带对 C3 F4(≤20%/普通场、≤50%/Boss 场)复验责任随批;B3 GDD "-50" 文字同步随下次 B3 修订批。 ; 格面资源批(2026-09-15):新增 sapped(乏力——weaken 型负向,hero_offense 池,attack_up 镜像; diff --git a/game/sim/level_autoplay.gd b/game/sim/level_autoplay.gd index 6bf19d7a..bae08513 100644 --- a/game/sim/level_autoplay.gd +++ b/game/sim/level_autoplay.gd @@ -572,7 +572,7 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void # ①标雷误挖:把「封印已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错, # 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板) # ②安全挖跳过:把「挖已证安全格」动作转成本轮停手(少挖几个正确的雷——信息与推进双损失) -# ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 通关全败于慢玩 +# ③卡死赌挖:完美档无已证动作时刷新/停手拖轮次吃敌方垫刀(实证=完美档 41% 冒险成功;全败于慢玩 # 垫刀非踩雷)——真人会按概率赌一个边界格(用户实证口径「有些地方因为我看不到数字,也是盲挖 # 的」);普通档不刷新(用户自报全程没用刷新),赌挖替代刷新/停手 # 失误流 = st.prng(策略流 RNG,种子派生自局种子——同种子逐位可回放;核心七流零消费零扰动)