From bc73fa3212aa0cbfdd424ee49539236b0df6d6e5 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Wed, 7 Oct 2026 02:14:44 +0800 Subject: [PATCH] =?UTF-8?q?T3=E7=BB=84C=C2=B7=E7=94=9F=E6=88=90=E4=B8=8E?= =?UTF-8?q?=E7=BC=96=E6=8E=92=E6=8D=A2=E4=BB=A3(=E6=95=8C=E4=BA=BA?= =?UTF-8?q?=E8=A1=A8=E8=A1=8C=E9=9B=86=E5=A4=9A=E8=A1=8C=E6=89=B9=E2=80=94?= =?UTF-8?q?=E2=80=94P1=E7=94=B2/Q8=E8=90=BD=E7=A0=81):=20unit=5Fcontainer.?= =?UTF-8?q?gd=20spawn=E8=A1=8C=E9=9B=86=E5=8F=97=E7=90=86(=E9=80=90?= =?UTF-8?q?=E8=A1=8C=E6=8C=89=E5=90=84=E8=87=AA=E6=95=B0=E9=87=8F=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E5=8C=96+=E7=94=9F=E6=88=90=E5=BA=8F=3D=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=A1=8C=E5=BA=8F+handle=5Fid=E8=BF=9E=E7=BB=AD?= =?UTF-8?q?=E6=B4=BE=E5=8F=91;unit=5Fcount=5Foverride/enemy=5Fatk=5Foverri?= =?UTF-8?q?de=E6=94=B9=E9=80=90=E8=A1=8C=E6=95=B0=E7=BB=84[=E4=B8=8B?= =?UTF-8?q?=E6=A0=87=3D=E8=A1=8C=E5=BA=8F,=E7=9F=AD=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E7=BC=BA=E7=9C=81-1=3D=E8=AF=A5=E8=A1=8C=E8=A1=8C=E5=80=BC?= =?UTF-8?q?=E8=B7=AF=E5=BE=84,=E7=A9=BA=E6=95=B0=E7=BB=84=3D=E5=85=A8?= =?UTF-8?q?=E8=A1=8C=E5=AE=9A=E5=80=BC=E6=97=A7=E8=B0=83=E7=94=A8=E7=82=B9?= =?UTF-8?q?=E9=9B=B6=E9=80=82=E9=85=8D];=E8=A1=8C=E9=9B=86=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E6=80=BB=E6=95=B0=E2=89=A4EnemyTable.ROWSET=5FINSTANC?= =?UTF-8?q?E=5FMAX=E5=AE=88=E5=8D=ABfail-fast[=E9=AA=B0=E5=90=8E=E5=80=BC?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=AD=A3=E7=A1=AE=E8=90=BD=E7=82=B9];?= =?UTF-8?q?=E9=9A=BE=E5=BA=A6=E4=BF=AE=E9=A5=B0=E5=AF=B9=E8=A1=8C=E9=9B=86?= =?UTF-8?q?=E5=85=A8=E9=83=A8=E8=A1=8C=E5=90=8C=E4=B9=98F9)+enemy=5Ftable.?= =?UTF-8?q?gd=E8=A1=A5ROWSET=5FINSTANCE=5FMAX=3D4=E5=B8=B8=E9=87=8F(?= =?UTF-8?q?=E4=B8=8E=E6=95=B0=E9=87=8F=E5=9F=9F=E5=90=8C=E4=BD=8D=E5=8D=95?= =?UTF-8?q?=E7=82=B9=E6=94=B6=E5=8F=A3)=EF=BC=9Bbattle=5Forchestrator.gd?= =?UTF-8?q?=E9=81=AD=E9=81=87=E9=AA=B0=E9=80=90=E8=A1=8C=E9=AA=B0=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3(Q8=E7=94=A8=E6=88=B7=E8=A3=81:=E6=AF=8F=E8=A1=8C?= =?UTF-8?q?=E5=90=84=E6=8E=B7=E8=87=AA=E5=B7=B1=E7=9A=84=E5=8C=BA=E9=97=B4?= =?UTF-8?q?=E9=AA=B0,=E5=85=88=E9=80=90=E8=A1=8C=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E9=AA=B0=E5=86=8D=E9=80=90=E8=A1=8Catk=E9=AA=B0,=E5=8D=95?= =?UTF-8?q?=E8=A1=8C=E8=A1=8C=E9=9B=86=E5=88=A4=E5=AE=9A/=E5=BC=80?= =?UTF-8?q?=E6=B5=81/=E6=B6=88=E8=B4=B9=E5=BA=8F/=E6=B6=88=E8=B4=B9?= =?UTF-8?q?=E6=AC=A1=E6=95=B0=E9=80=90=E4=BD=8D=E4=B8=80=E8=87=B4=3D?= =?UTF-8?q?=E9=9B=B6=E5=86=85=E5=AE=B9=E6=9C=9F=E9=9A=8F=E6=9C=BA=E6=B5=81?= =?UTF-8?q?=E9=9B=B6=E5=8F=98=E5=8C=96;atk=E5=8C=BA=E9=97=B4=E9=80=90?= =?UTF-8?q?=E8=A1=8C=E5=90=8C=E6=9E=84=E5=B1=9E=E6=AF=8F=E8=A1=8C=E4=B8=80?= =?UTF-8?q?=E9=AA=B0=E9=A2=84=E6=89=B9=E5=87=86=E8=AF=AD=E4=B9=89=E5=86=85?= =?UTF-8?q?=C2=B7=E4=BB=A3=E6=8B=8D=E6=9D=BF=E7=95=99=E7=97=95)=EF=BC=9Bba?= =?UTF-8?q?ttle=5Fnarrator=E5=8F=96=E9=A6=96=E8=A1=8C=E5=8F=A3=E5=BE=84T2?= =?UTF-8?q?=E5=B7=B2=E8=90=BD(T2=E6=B3=A8=E9=87=8A=E5=B7=B2=E7=99=BB?= =?UTF-8?q?=E8=AE=B0P4=E4=B9=99=E7=9F=A5=E6=83=85=E4=BB=A3=E4=BB=B7)?= =?UTF-8?q?=EF=BC=9B=E8=BD=BB=E9=97=B8=3D=E5=86=92=E7=83=9F805/0=E4=B8=89?= =?UTF-8?q?=E9=97=B8=E5=85=A8=E8=BF=87+probe=5Fgate=E5=90=ABclick=E5=85=A8?= =?UTF-8?q?=E7=BB=BF(=E6=AE=8B=E7=95=99Godot=3D0)=E2=80=94=E2=80=94?= =?UTF-8?q?=E9=9B=B6=E5=86=85=E5=AE=B9=E6=9C=9F=E6=96=AD=E8=A8=80=E9=80=90?= =?UTF-8?q?=E4=BD=8D=E9=9B=B6=E6=BC=82=E7=A7=BB=E4=BF=9D=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 43 ++++++++----- game/core/unit_container.gd | 104 ++++++++++++++++++------------- game/data/schemas/enemy_table.gd | 5 ++ 3 files changed, 95 insertions(+), 57 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index fa0678b1..775a6f38 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -178,30 +178,45 @@ func begin_battle(enc: Dictionary) -> bool: # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 - # 行集数组口径(2026-10-07 行集多行批——本卡 T2 机械适配取首行保持行为零变化:遭遇骰读首行 - # 数量区间 = 全行集骰域〔多行后首行区间即行集骰域,知情代价登记 敌人系统 v0.15〕; - # 每行各骰换代随批 T3) + # 遭遇骰(难度曲线与Boss GDD Core Rule 8——2026-09-12 用户拍板):行有 unit_count_max 时按 [unit_count, max] 均匀骰; + # 2026-09-14 敌方攻击区间批扩展:行有 atk_min 时按 [atk_min, atk_max] 骰 spawn 期整战持久值 + # (每拍抖动否决——数字可读性锚;中点 = 行值)。encounter 封闭流恰一 scope(数量骰先、atk 骰后); + # 挖掘决策域之外——P1 合规;敌人系统 零随机纪律保持(收 override 即确定值)。 + # 逐行骰换代(2026-10-07 行集多行批 Q8 用户裁 + P1 补充):混编行集**每行各掷自己的区间骰**—— + # 先按行序逐行数量骰、再按行序逐行 atk 骰(单行行集 = 既有一次数量骰 + 一次 atk 骰,判定条件、 + # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 + # 既有口径;atk 区间逐行同构 = 行值语义完整,代拍板随批留痕:属「每行一骰」预批准语义内的 + # 行字段区间骰逐行化,非新增随机源形态)。 var erowset_pre: Array = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) if erowset_pre.is_empty(): _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) return false - var erow_pre: Dictionary = erowset_pre[0] - var roll_lo: int = int(erow_pre.get("unit_count", 1)) - var roll_hi: int = int(erow_pre.get("unit_count_max", roll_lo)) - var needs_count: bool = roll_hi > roll_lo - var needs_atk: bool = erow_pre.has("atk_min") - var rolled := roll_lo - var rolled_atk := -1 + var count_overrides: Array = [] + var atk_overrides: Array = [] + var needs_count := false + var needs_atk := false + for r in erowset_pre: + if int(r.get("unit_count_max", int(r.get("unit_count", 1)))) > int(r.get("unit_count", 1)): + needs_count = true + if r.has("atk_min"): + needs_atk = true if needs_count or needs_atk: var estream: RandomNumberGenerator = rng.open_stream("encounter") if needs_count: - rolled = roll_lo + estream.randi_range(0, roll_hi - roll_lo) + for r in erowset_pre: + var lo: int = int(r.get("unit_count", 1)) + var hi: int = int(r.get("unit_count_max", lo)) + count_overrides.append((lo + estream.randi_range(0, hi - lo)) if hi > lo else lo) if needs_atk: - rolled_atk = int(erow_pre.atk_min) + estream.randi_range(0, int(erow_pre.atk_max) - int(erow_pre.atk_min)) + for r in erowset_pre: + if r.has("atk_min"): + atk_overrides.append(int(r.atk_min) + estream.randi_range(0, int(r.atk_max) - int(r.atk_min))) + else: + atk_overrides.append(-1) # E4 收口:加成经 encounter 显式携带(hero_atk/hero_max_hp;缺省 = 表值——冒烟/工具直呼路径) _hero_atk = int(enc.get("hero_atk", int(db.hero.atk))) - if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), rolled, - _hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk): + if not _enemy_system.spawn(int(enc.level_index), int(enc.node_depth), int(enc.hero_hp), count_overrides, + _hero_atk, int(enc.get("hero_max_hp", -1)), atk_overrides): _fatal("敌人系统 spawn 被拒") return false if not _install_new_board(): diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 46005ff3..ae00ed95 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -39,19 +39,21 @@ func _init(db_ref: ConfigDb) -> void: # 此前调用方写 db.hero 表值再由本类读表 = 共享可变单例传参(双写叠加 6→18→54 教训根源)。 # enemy_atk_override(2026-09-14 敌方攻击区间批):遭遇骰同流的 spawn 期整战持久 roll 结果 # (-1 = 表值定值路径;≥0 = 行值替换后再乘关卡 atk_mult)。敌人系统 零随机纪律保持。 -func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: int = -1, - atk_override: int = -1, max_hp_override: int = -1, enemy_atk_override: int = -1) -> bool: +# 行集受理换代(2026-10-07 行集多行批 P1 甲):unit_count_overrides / enemy_atk_overrides 改**逐行数组** +# (下标=行集行序,长度短于行集时缺省 -1 = 该行行值路径;空数组 = 全行定值——旧调用点零适配)。 +# 逐行实例化、生成序 = 文件行序(行序即契约,敌人系统 v0.15);行集实例总数(各行传入数量求和) +# ≤ ENEMY_ROWSET_INSTANCE_MAX 守卫 fail-fast。 +func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides: Array = [], + atk_override: int = -1, max_hp_override: int = -1, enemy_atk_overrides: Array = []) -> bool: if _spawned: push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)") return false - # 行集数组口径(2026-10-07 行集多行批——resolver 返回该坐标全部行;本卡 T2 先机械适配取首行 - # 保持行为零变化,行集逐行实例化换代随批 T3) + # 行集数组口径(2026-10-07 行集多行批——resolver 返回该坐标全部行、文件序) var rowset: Array = db.resolve_enemy_rowset(level_index, node_depth) if rowset.is_empty(): push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth]) assert(false, "遭遇行悬空(R1 前置守卫)") return false - var row: Dictionary = rowset[0] var eff_atk: int = atk_override if atk_override >= 1 else int(db.hero.atk) var eff_max_hp: int = max_hp_override if max_hp_override >= 1 else int(db.hero.max_hp) if hero_hp > eff_max_hp or hero_hp <= 0: @@ -60,48 +62,64 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: # max_hp = 生效上限(非入参 hero_hp)——2026-09-11 关卡扩展批修复:跨战斗携带 HP 时上限不得压扁 # (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清) _hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero", "armor_type": String(db.hero.get("armor_type")) if db.hero.get("armor_type") else "light", "attack_type": String(db.hero.get("attack_type")) if db.hero.get("attack_type") else "normal"} # 护甲/攻击类型轴快照(2026-09-18 营地扩展前置批——可配置;甲效矩阵消费面) - _enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10) + _enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15) var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth) var hp_mult: float = float(mod.get("hp_mult", 1.0)) var atk_mult: float = float(mod.get("atk_mult", 1.0)) - # unit_count_override ≥1 = 战斗结算 遭遇骰结果(敌人系统 零随机纪律——收到即确定值;-1 = 行值定值路径) - # 数量域常量单点 = EnemyTable.UNIT_COUNT_MIN/MAX(2026-10-02 修复批——与表校验同源) - var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count) - if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX: - push_warning("[敌人系统] 实例化数量越域: %d" % count) + # 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值; + # 行 i 数量 = overrides[i](≥1 时)否则该行 unit_count 行值) + var per_row_counts: Array[int] = [] + var total_instances := 0 + for row_i in range(rowset.size()): + var row_i_dict: Dictionary = rowset[row_i] + var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1 + var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count) + if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX: + push_warning("[敌人系统] 行 %d 实例化数量越域(单种数域 [%d,%d]): %d" % [row_i, EnemyTable.UNIT_COUNT_MIN, EnemyTable.UNIT_COUNT_MAX, count]) + return false + per_row_counts.append(count) + total_instances += count + # 行集实例总数守卫(行集构成纪律③——各行骰后数量求和 ≤4;骰后值加载期不可判,此处 fail-fast) + if total_instances > EnemyTable.ROWSET_INSTANCE_MAX: + push_warning("[敌人系统] 行集实例总数越域(≤%d): %d" % [EnemyTable.ROWSET_INSTANCE_MAX, total_instances]) return false - for i in range(count): - var eff_row_atk: int = enemy_atk_override if enemy_atk_override >= 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, - } - # Boss 半血解锁技(迭代14④ T3——unlock_hp_pct 随池模板快照进实例池;缺省无字段=无门槛; - # unlocked=单向闩标记,初始锁定) - if entry.has("unlock_hp_pct"): - snap_entry["unlock_hp_pct"] = int(entry.get("unlock_hp_pct", 0)) - snap_entry["unlocked"] = false - pool_snap.append(snap_entry) - _enemies.append({ - "handle_id": i, - "enemy_id": String(row.enemy_id), - "display_name": String(row.display_name), - "hp": maxi(1, int(round(float(row.hp) * hp_mult))), - "max_hp": maxi(1, int(round(float(row.hp) * hp_mult))), - "atk": maxi(0, int(round(float(eff_row_atk) * atk_mult))), - "pool": pool_snap, # 实例池(模板快照 + 冷却状态——独立摇号互不干涉) - "pending_skill": "", # 本拍摇定动作(开轮摇号写入;兑现前死亡 = 作废不 commit) - "roll_seq": 0, # 单敌摇号拍序(诊断标签——并流后不参与派生,ADR-0007 裁丙 2026-09-27) - "basic_streak": 0, # 保底计数:连续执行普攻的拍数(执行上报驱动) - "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 护甲等级掷点批) - "alive": true, - "is_hero": false, - }) + var handle := 0 + for row_i in range(rowset.size()): + var row: Dictionary = rowset[row_i] + 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, + } + # Boss 半血解锁技(迭代14④ T3——unlock_hp_pct 随池模板快照进实例池;缺省无字段=无门槛; + # unlocked=单向闩标记,初始锁定) + if entry.has("unlock_hp_pct"): + snap_entry["unlock_hp_pct"] = int(entry.get("unlock_hp_pct", 0)) + snap_entry["unlocked"] = false + pool_snap.append(snap_entry) + _enemies.append({ + "handle_id": handle, + "enemy_id": String(row.enemy_id), + "display_name": String(row.display_name), + "hp": maxi(1, int(round(float(row.hp) * hp_mult))), + "max_hp": maxi(1, int(round(float(row.hp) * hp_mult))), + "atk": maxi(0, int(round(float(eff_row_atk) * atk_mult))), + "pool": pool_snap, # 实例池(模板快照 + 冷却状态——独立摇号互不干涉) + "pending_skill": "", # 本拍摇定动作(开轮摇号写入;兑现前死亡 = 作废不 commit) + "roll_seq": 0, # 单敌摇号拍序(诊断标签——并流后不参与派生,ADR-0007 裁丙 2026-09-27) + "basic_streak": 0, # 保底计数:连续执行普攻的拍数(执行上报驱动) + "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 护甲等级掷点批) + "alive": true, + "is_hero": false, + }) + handle += 1 _spawned = true _despawned = false return true diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 3441f9b2..26e4316e 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -24,6 +24,11 @@ const ARMOR_TYPES := ["light", "ward", "heavy"] const UNIT_COUNT_MIN := 1 const UNIT_COUNT_MAX := 3 +# 行集实例总数上限(2026-10-07 行集多行批 P1 甲——行集构成纪律③:各行开战骰后数量求和 ≤4; +# 「总数」= 实例数非行数〔Q9 二裁〕,行数不设上限。骰后值加载期不可判——守卫在 单位容器 spawn, +# 常量单点在本表与数量域同位收口) +const ROWSET_INSTANCE_MAX := 4 + func validate() -> Array: var errs: Array[Dictionary] = []