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] = []