diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index b0f88b79..694d9389 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -285,6 +285,11 @@ func _install_new_board(during_round := false) -> bool: var board: BoardData = _board_generator.generate(int(encounter.level_index), int(encounter.node_depth), stream, db) if board == null: return false + # 全局作用域变异决议(首发批 2026-10-04——装板序内前置于 roll_board:图级偏斜旋钮须在资源 + # 摇号前定型;全局门控与板无关〔节点身份+unlock+board_types〕,确定性取用零取号)。 + # boss/elite 图命中全局型 → 写 board_variants(_place_variants 见非空即跳 region=替换制); + # 全局池空 → 板级字段留空(_place_variants 原路走 region=兜底回落合法态)。 + _resolve_global_variant(board) var res: Dictionary = minesweeper.install_board(board, during_round) if not res.accepted: _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) @@ -318,6 +323,22 @@ func _install_new_board(during_round := false) -> bool: return true +# ---- 全局作用域变异 决议(首发批 2026-10-04——装板序内 roll_board 之前)---- +# boss/elite 图命中全局型:写板级 board_variants(恰一行)+ 注入资源面偏斜旋钮(图级投影在 +# RewardEconomy.roll_board 消费);不消费 variant_place 流(全局取用确定性——零取号合法态)。 +# 全局池空(含 L1 Boss 免疫/unlock 门控未达):字段留空、旋钮归零——_place_variants 原路走 +# region 布置 = 兜底回落(保守向后兼容,防配置缺行静默裸奔——provisional)。 +func _resolve_global_variant(board: BoardData) -> void: + board.board_variants.clear() + _reward_economy.board_skew = 0 + var row: Dictionary = db.global_variant_row(int(encounter.level_index), String(board.board_type), + String(encounter.get("node_type", ""))) + if row.is_empty(): + return + board.board_variants = [String(row.variant_id)] + _reward_economy.board_skew = int(row.get("effect_value", 0)) + + # ---- 地形变异 布置段(首批类型定稿批 2026-09-19——棋盘设置顺序第 4 步)---- # 候选池门控(unlock_level ≤ 关卡序号 + board_types 与本地形交集,∅ 通配)经 config_db.variant_pool; # 池空 = 合法态,静默零布置(框架 P0 口径不变)。零特判:本段只写格子变异数据快照,不改扫雷规则本体。 @@ -327,7 +348,11 @@ func _install_new_board(during_round := false) -> bool: # (关卡序号−1)×per_level_bonus——基数为候选池而非总格数(2026-09-21 用户拍板基数修正批:初始展开 # 占全板 56–67%,总格数基数使有效密度膨胀 4–6 倍、L1 小板极端全池覆盖;percent 语义 = 占可挖格比例, # 关卡递增由 per_level_bonus 绝对格数承载);连通片生长(8 邻),单片 ≤ pack_max(2×2 / 1×4 / 1×3+1 等形)。 +# 替换制(全局作用域变异首发批 2026-10-04):板级 board_variants 非空 = 本图已带全局型,跳过 region +# 布置(每图一型的板级推广:普通图=region,Boss/精英图=global);空 = 原路径(含 Boss 图全局池空回落)。 func _place_variants(board: BoardData) -> void: + if not board.board_variants.is_empty(): + return var pool: Array = db.variant_pool(int(encounter.level_index), String(board.board_type)) if pool.is_empty(): return @@ -1155,6 +1180,10 @@ func presentation_snapshot() -> Dictionary: "hero": hero, "enemies": enemies, "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(code-review 2026-09-29 P2-3——快照消费方直改不渗入状态容器) + # 板级变异投影(全局作用域变异首发批 2026-10-04——呈现横幅/常驻角标唯一读取面; + # 空数组 = 无全局型合法态:普通图与全局池空回落图恒空) + "board_variants": (minesweeper.current_board().board_variant_ids().duplicate() + if minesweeper.current_board() != null else ([] as Array[String])), } diff --git a/game/core/board_data.gd b/game/core/board_data.gd index 56dd7dd7..9160f6b6 100644 --- a/game/core/board_data.gd +++ b/game/core/board_data.gd @@ -16,6 +16,8 @@ var spawn_center := Vector2i.ZERO var mines := {} # Vector2i → true var digits := {} # Vector2i → int(安全格恒有值;数字恒真 = 8 邻雷数) var variants := {} # Vector2i → Array[String](生成期快照,抗表重载) +var board_variants: Array[String] = [] # 板级变异 id 集(scope=global 整图作用域——全局作用域变异批 2026-10-04: + # 战斗编排装板决议写入,随图生灭同 variants;普通图恒空) var board_type := "" # 图类型标签(词表 v1.0:dungeon/underdark;玩家侧显示名「地图类型」) var level_index: int = 1 var node_depth: int = 0 @@ -50,6 +52,11 @@ func variant_ids(c: Vector2i) -> Array: return variants.get(c, []) +# 板级变异只读出口(scope=global 行集——呈现快照/断言消费;空 = 无全局型合法态) +func board_variant_ids() -> Array[String]: + return board_variants + + # 中和格变异(迭代 12 T6 净化工兵——伤害型地形格变普通格;🔴 R4 甲仅限伤害型的过滤在调用方) func clear_variants(c: Vector2i) -> void: variants.erase(c) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 22a2acae..3423f028 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -197,10 +197,16 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: # ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐 # board_type 词表——biome_tags 系旧观念残留退役)---- +# 门控第三维 = 节点身份(全局作用域变异首发批 2026-10-04):scope=global 行仅 boss/elite 节点入池, +# 其余节点(battle 等)静默跳过——门控兜底 P0 口径(零报错零警告);elite 系前向词位(首发零表行)。 +# node_type 缺省 "" = 旧双参调用口径(global 行跳过)——既有测试/冒烟调用零扰动。 -func variant_pool(level_index: int, board_type: String) -> Array: +func variant_pool(level_index: int, board_type: String, node_type: String = "") -> Array: var pool: Array[Dictionary] = [] for row in variation.rows: + if String(row.get("scope", "region")) == "global" \ + and node_type != "boss" and node_type != "elite": + continue if int(row.get("unlock_level", 999)) > level_index: continue var tags: Array = row.get("board_types", []) @@ -212,6 +218,18 @@ func variant_pool(level_index: int, board_type: String) -> Array: return pool +# 全局型单行决议(战斗编排装板期消费——访问器纪律:消费面禁直扫表): +# boss/elite 节点取候选池内首个 scope=global 行(首发批全局池恒恰一行;多行为配置越界态, +# 按表序取首行确定性兜底);空 = 无全局型(调用方回落 region 布置——兜底回落合法态) +func global_variant_row(level_index: int, board_type: String, node_type: String) -> Dictionary: + if node_type != "boss" and node_type != "elite": + return {} + for row in variant_pool(level_index, board_type, node_type): + if String(row.get("scope", "region")) == "global": + return row + return {} + + func status_effect_row(effect_id: String) -> Dictionary: return status_effects.row_by_id(effect_id) diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 55f53d8c..09d9de60 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -179,13 +179,25 @@ var luck_bias: int = 0 # 稀有正向资源 kinds(底稿 §3——权重投影面,池结构零触碰) const LUCK_BIAS_KINDS := ["heal", "gp", "pierce", "magic"] +# 图级偏斜旋钮(全局作用域变异首发批 2026-10-04——battle_orchestrator 装板决议注入,全局型行 +# effect_value 语义=生存族权重下调百分比;0 = 中性 off 态。每板决议时显式重置/写入) +var board_skew: int = 0 + +# 偏斜族(任务书 §二:进攻族等比上浮/生存族按 v% 下调/白名单豁免恒不动——判定键=cell_type; +# 其余 cell_type(weaken/gp/action 白名单与未来扩型)= 豁免;负向行不入摇号已在 _rollable_rows 滤除) +const SKEW_DOWN_KINDS := ["heal", "defense"] +const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] + # 偏运硬币权重投影(底稿 §3:只动池内权重、雷安仍同池同分布;luck_bias=0 逐位返回原行集——off 态零扰动) +# 基座换源(首发批 2026-10-04):先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 +#(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) func _biased_cell_rows() -> Array: + var base: Array = _skewed_cell_rows() if luck_bias <= 0: - return _rollable_rows() + return base var out: Array = [] - for row in _rollable_rows(): + for row in base: if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): var biased: Dictionary = row.duplicate() biased["weight"] = int(row.weight) + luck_bias @@ -195,6 +207,81 @@ func _biased_cell_rows() -> Array: return out +# 图级资源面偏斜投影(首发批 T3——整数最大余数法:生存族按 board_skew% 下调,下调量按进攻族 +# 原权重比例整数重分配,合计恒等于原池合计 77;负向行与白名单三行逐位不动)。 +# board_skew<=0 逐位返回原行集 = off 态零扰动第一断言:不进投影域、行集/行序/取号域与基线逐位同。 +func _skewed_cell_rows() -> Array: + if board_skew <= 0: + return _rollable_rows() + var base: Array = _rollable_rows() + var down: Array = [] + var up: Array = [] + for row in base: + var k := String(row.get("cell_type", "")) + if SKEW_DOWN_KINDS.has(k): + down.append(row) + elif SKEW_UP_KINDS.has(k): + up.append(row) + if down.is_empty() or up.is_empty(): + return base # 结构缺族 = 无偏斜作用面(保守直通——池结构变更须走表加载校验另批) + var v := board_skew + # 下调总量 = round_half_up(Σ生存×v/100);逐行精确份额 w×v/100 最大余数取整(行序平局) + var drops := _largest_remainder_split(down, v, 100, (2 * _pool_weight(down) * v + 100) / 200) + # 上浮总量 = 下调总量(守恒),逐行精确份额 drop×w/Σ进攻 最大余数取整 + var gains := _largest_remainder_split(up, drops_total(drops), _pool_weight(up), drops_total(drops)) + var di := 0 + var ui := 0 + var out: Array = [] + for row in base: + var k := String(row.get("cell_type", "")) + var w := int(row.weight) + if SKEW_DOWN_KINDS.has(k): + w -= drops[di] + di += 1 + elif SKEW_UP_KINDS.has(k): + w += gains[ui] + ui += 1 + assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)") + var skewed: Dictionary = row.duplicate() + skewed["weight"] = w + out.append(skewed) + assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)") + return out + + +func drops_total(amounts: Array[int]) -> int: + var t := 0 + for a in amounts: + t += a + return t + + +# 最大余数整数分摊(行序平局——确定性):把 total 单位按精确份额 w_i×numer/denom 分到 rows +# (floor 后余数单位依次给分数最大者,平局取行序在前)——Σ结果恒 = total +static func _largest_remainder_split(rows: Array, numer: int, denom: int, total: int) -> Array[int]: + var floors: Array[int] = [] + var fracs: Array[int] = [] + var used := 0 + for row in rows: + var exact: int = int(row.weight) * numer + floors.append(int(exact / denom)) + fracs.append(exact % denom) + used += int(exact / denom) + var order: Array[int] = [] + for i in rows.size(): + order.append(i) + order.sort_custom(func(a: int, b: int) -> bool: + if fracs[a] != fracs[b]: + return fracs[a] > fracs[b] + return a < b) + var out: Array[int] = floors + var rest: int = total - used + assert(rest >= 0 and rest <= order.size(), "最大余数分摊余数越界(不应可达)") + for j in rest: + out[order[j]] += 1 + return out + + func weighted_pick(rows: Array, r: int) -> String: var total := _pool_weight(rows) assert(r >= 0 and r < total, "weighted_pick 取号越域(纯函数域守卫)") diff --git a/game/data/schemas/variation_table.gd b/game/data/schemas/variation_table.gd index ce5199a3..f8b19d63 100644 --- a/game/data/schemas/variation_table.gd +++ b/game/data/schemas/variation_table.gd @@ -20,8 +20,10 @@ extends Resource @export var pack_max: int = 4 const SCOPES := ["region", "global"] -const EFFECTS := ["none", "flat_damage", "hide_number"] # 效果词表:直伤 / 数字遮蔽(可扩充) -const TRIGGERS := ["on_open", "on_number_shown"] # 触发词表:被打开即触发 / 打开且显示数字时触发(可扩充) +# resource_face = 资源面偏斜(全局作用域变异首发批 2026-10-04——整图修摇号分布:生存族按 effect_value% +# 下调、进攻族等比上浮重归一,不动任何格子数据;effect_value 语义随行域切换,不适用 flat_damage [1,9]) +const EFFECTS := ["none", "flat_damage", "hide_number", "resource_face"] # 效果词表:直伤 / 数字遮蔽 / 资源面偏斜(可扩充) +const TRIGGERS := ["on_open", "on_number_shown", "always"] # 触发词表:被打开即触发 / 打开且显示数字时触发 / 板级恒在无触发时机(可扩充) const PLACEMENTS := ["closed_only", "any"] # 布置约束词表:只落布置时未开格 / 任意格(可扩充) @@ -65,6 +67,22 @@ func validate() -> Array: errs.append({"category": "cross_field", "message": "%s hide_number ⇒ trigger 必为 on_number_shown" % vid}) # 信息遮蔽型只允许落安全格(遮蔽对象是数字;雷格无数字可遮)——placement=any 时由读取方 # 呈现面按 kind=safe 自然短路,此处不设布置约束 + # 全局作用域型互锁(2026-10-04 首发批——resource_face ⟺ scope=global 三条 + 新值域; + # 判定键=effect/scope 词值,既有六 region 行(flat_damage/hide_number)不入本组——零误伤) + if row.get("effect", "") == "resource_face": + if row.get("scope", "") != "global": + errs.append({"category": "cross_field", "message": "%s resource_face ⇒ scope 必为 global(整图作用域)" % vid}) + # resource_face 行 effect_value = 生存族权重下调百分比(0=中性合法态——off 锚用) + var gev: int = int(row.get("effect_value", 0)) + if gev < 0 or gev > 50: + errs.append({"category": "domain", "message": "%s effect_value 越域 [0,50](资源面偏斜百分比): %d" % [vid, gev]}) + if row.get("scope", "") == "global": + if row.get("effect", "") != "resource_face": + errs.append({"category": "cross_field", "message": "%s scope=global ⇒ effect 必为 resource_face" % vid}) + if row.get("placement", "") != "any": + errs.append({"category": "cross_field", "message": "%s scope=global ⇒ placement 必为 any(整图无格子布置域)" % vid}) + if row.get("trigger", "") != "always": + errs.append({"category": "cross_field", "message": "%s scope=global ⇒ trigger 必为 always(板级恒在,无触发时机)" % vid}) return errs diff --git a/game/data/tables/variation.tres b/game/data/tables/variation.tres index 3d0895dd..d60de9b2 100644 --- a/game/data/tables/variation.tres +++ b/game/data/tables/variation.tres @@ -73,6 +73,17 @@ rows = [{ "placement": "any", "board_types": ["deep_underdark"], "counter_items": [] +}, { +"variant_id": "skewed_veins", +"display_name": "贫矿脉", +"unlock_level": 2, +"scope": "global", +"effect": "resource_face", +"effect_value": 30, +"trigger": "always", +"placement": "any", +"board_types": [], +"counter_items": [] }] ; 地形变异 首批类型定稿批(2026-09-19 用户拍板)——四行两轴×两关: @@ -98,3 +109,10 @@ rows = [{ ; 变异密度 9%→4%、完美 L3 0→34%,L3 无带记观察态;行值 provisional→正式随迭代 5 出口人闸追认)。 ; M-3 迭代 5 返工批(2026-09-29 U2 推翻 C3 裁 C1 两刀):percent_of_cells 4→5 回退;岩浆 1 与 per_level_bonus 0 维持——C1 终态; ; C1+v1 AC 组合读数归返工批全量确认(裁断全录 design/qa/m3-iter5-u2-gate-2026-09-29.md)。 +; +; 全局作用域变异首发批(2026-10-04,迭代 14 ③号卡):skewed_veins 贫矿脉——首发型 scope=global +; 案A 资源面·微贫向偏斜(P2+Q1 裁决):L2/L3 Boss 图恒定一型整图变异(P4 甲恒定制;board_types ∅ +; 通配,L2/L3 双关同型由 unlock_level=2+门控节点身份承载——Q2 甲),替换制:带全局型的 Boss 图不再 +; 布 region 片(P3 甲);effect_value=30=生存族权重下调百分比(provisional 调参项,域 [0,50], +; 0=中性 off 锚;总期望微降 3-5% 为 provisional 设计目标,精确方向与幅度归读数批只测不判); +; trigger=always=板级恒在无触发时机。行值全 provisional 归出口追认单。