diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 9fef4244..17a48750 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -6,11 +6,12 @@ class_name ActionEconomy extends RefCounted -signal round_end_stopped(payload: Dictionary) # 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构) +signal round_end_stopped(payload: Dictionary) ## 双发射源之停手路径(行动经济 直发;payload 与 排雷玩法 棋盘路径同构) const PHASE_IDLE := "Idle" const PHASE_ACTIVE := "Active" const PHASE_WAITING := "Waiting" +const CHARGE_LAYER_CAP := 3 # 蓄力层上限(机制结构常量,非玩法表值——整备军备面) # 相位轴:内部 enum + match,外部 phase getter 保持 # String 面(presentation_snapshot 与冒烟链的 "Idle"/"Active"/"Waiting" 对照契约)。 @@ -182,14 +183,14 @@ func grant_reserve_ap(n: int) -> void: # 轮末停手转存决策(round_end 发射点调用——编排器授予段尾):transfer=true=剩余 AP 转蓄力层 -# (上限 3,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1; +# (上限 CHARGE_LAYER_CAP,整备军备面);false=按转存率转存 RAP(transfer_pct 百分比口径缺省 100=1:1; # 储备罐 rap_transfer_mult magnitude 125=1.25:1,整数除法向下取整=每 4 AP 多转 1)。 # 非停手轮(cleared/耗尽)_last_stop_ap=0 两分支均零操作。转蓄后达上限的沿发 charge_full(R6 框架先行——现无订阅者)。 func finish_round_stop(transfer: bool, transfer_pct: int = 100) -> void: if transfer: var prev_layers := _charge_layers - _charge_layers = mini(3, _charge_layers + _last_stop_ap) - if _fx != null and _charge_layers >= 3 and prev_layers < 3: + _charge_layers = mini(CHARGE_LAYER_CAP, _charge_layers + _last_stop_ap) + if _fx != null and _charge_layers >= CHARGE_LAYER_CAP and prev_layers < CHARGE_LAYER_CAP: _fx.dispatch(_ctx, "charge_full", {"layers": _charge_layers}) else: _rap += _last_stop_ap * maxi(1, transfer_pct) / 100 diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 4823b21e..9929346e 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -6,10 +6,10 @@ class_name AdventureRunner extends RefCounted -signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 -signal curio_choice_presented(payload: Dictionary) # 专长三选一停点(非末关通关恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} -signal curio_resolved(result: Dictionary) # 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index} -signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops) +signal level_started(level_index: int, display_name: String) ## 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 +signal curio_choice_presented(payload: Dictionary) ## 专长三选一停点(非末关通关恰一次;通道白名单后候选恒专长+回血在载荷)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared} +signal curio_resolved(result: Dictionary) ## 三选一兑付完成(pick_curio/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index} +signal adventure_finished(summary: Dictionary) ## 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops) # ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;S6 经 _forward_* 方法)---- signal node_entered(node: Dictionary, info: Dictionary) signal choices_presented(candidates: Array) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 3cdf75d9..13a61ca2 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -7,10 +7,10 @@ class_name BattleOrchestrator extends RefCounted signal step_row(row: Dictionary) -signal battle_finished(payload: Dictionary) # {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计 +signal battle_finished(payload: Dictionary) ## {result, hero_hp, gp_dropped}——hero 在 despawn 前捕获(呈现层销毁后读状态的时序防护);gp_dropped = 战斗掉落合计 signal round_started(round_index: int) signal board_changed() -signal settlement_snapshot(snap: Dictionary) # 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析) +signal settlement_snapshot(snap: Dictionary) ## 每结算末尾的全量状态快照(playtest_logger 消费——JSONL 数值分析) # 明细流水行型协议(战斗结算 Core Rule;9 类; # 清单改动须同步 battle_log_view 的 _format_row 与本文档头注): diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index a3b5d1ae..c5d4db2b 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -1,7 +1,7 @@ ## VERTICAL SLICE - NOT FOR PRODUCTION ## 确定性棋盘生成器:生成-验证循环出图,不达标走接受最优/最不坏诚实降级(垂直切片验证:每张图可推理可归因)。 ## 只消费传入的单条 rng 流(重试共用,attempt 边界非 scope 事件)——同流同参逐位复现。 -## generate/legality 的 db 参数鸭子型(不标类型)——冒烟测量面注入桩表依赖此形态,勿收紧签名。 +## generate 的 db 参数鸭子型(不标类型)——冒烟测量面注入桩表依赖此形态,勿收紧签名。 class_name BoardGenerator extends RefCounted @@ -10,6 +10,11 @@ const METRIC_VERSION := 1 # ==================== 生成入口 ==================== +# 目标雷数 N = round(d×(W×H−9))(params 快照键:density/w/h)——布雷定数与产物断言两处的单点公式 +static func target_mine_count(params: Dictionary) -> int: + return int(round(float(params.density) * float(int(params.w) * int(params.h) - BoardData.SPAWN_AREA_CELLS))) + + # rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。 # db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表 func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub) @@ -50,7 +55,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat # R₀≤cap 重掷闸触发计数(迭代14① T3——尝试事实,与合法性判定独立记录;占比=T5 闸触发率读数) if int(params.r0_cap) > 0 and b.compute_r0().size() > int(params.r0_cap): r0_cap_rejected += 1 - if legality(b, params, db): + if legality(b, params): var m: Dictionary[Variant, Variant] = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次) var inf: float = m["inferability"] if best_legal == null or inf > best_legal_score: @@ -94,7 +99,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar for c in b.all_cells(): if not spawn.has(c): domain.append(c) - var n_mines: int = int(round(params.density * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) + var n_mines: int = target_mine_count(params) # 部分 Fisher-Yates(确定性,消费 board_gen 流) for i in range(0, mini(n_mines, domain.size())): var j := rng_stream.randi_range(i, domain.size() - 1) @@ -278,7 +283,7 @@ func is_clearable(b: BoardData) -> bool: return true -func legality(b: BoardData, params: Dictionary, db) -> bool: # type-hint: ignore(同 generate——测量面 stub 注入) +func legality(b: BoardData, params: Dictionary) -> bool: var reasons := _legality_faults(b, params) return reasons.is_empty() @@ -331,7 +336,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int: func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: # type-hint: ignore(同 generate——测量面 stub 注入) var m := measure_inferability(b) # 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命 - var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) + var n_target: int = target_mine_count(params) if b.mines.size() > n_target: _fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target]) var attempt_facts := b.metrics # 保留尝试期事实(ring_removed) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 4b316db8..dbdfe8fa 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -342,6 +342,20 @@ func shop_price_b() -> float: return float(shop.price_b) if shop != null else 0.0 +# relic 保底卡基价 / 散折(economy_spend 激活时购买价折扣——表未入库 = 表 schema 缺省值) +func shop_relic_base_price() -> float: + return float(shop.relic_base_price) if shop != null else 0.0 + + +func shop_spend_discount() -> float: + return float(shop.spend_discount) if shop != null else 0.0 + + +# 英雄显示名(单位快照读取面单源——与敌人行 display_name 同轴) +func hero_display_name() -> String: + return String(hero.display_name) if hero != null else "" + + # 行族数组读取(ShopTable 属性名 = 行族名;表未已入库/属性缺失 = 空集非错误态) func _shop_rows(family: String) -> Array: if shop == null: diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 18c0303d..6d0571df 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -212,7 +212,7 @@ func _on_unit_died(u: Dictionary) -> void: _halted = true # 主角倒下——flush 余单作废;Game Over 归判定段既有 hp 读数路径 return for t in _queue: - if t.source_unit == u: + if is_same(t.source_unit, u): t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 300534b3..71c5c4b9 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -7,13 +7,13 @@ class_name LevelRunner extends RefCounted -signal node_entered(node: Dictionary, info: Dictionary) # info 按 type:battle/boss={} · reward={}(内容待定) · rest={healed,hp,max_hp} · shop={gp} -signal choices_presented(candidates: Array) # 选路口(兑付确认后——候选节点引用数组,零自动推进) -signal step_row(row: Dictionary) # 转发当前 战斗结算 结算行 +signal node_entered(node: Dictionary, info: Dictionary) ## info 按 type:battle/boss={} · reward={}(内容待定) · rest={healed,hp,max_hp} · shop={gp} +signal choices_presented(candidates: Array) ## 选路口(兑付确认后——候选节点引用数组,零自动推进) +signal step_row(row: Dictionary) ## 转发当前 战斗结算 结算行 signal round_started(round_index: int) signal board_changed() -signal level_finished(payload: Dictionary) # {result, hp, node_index, node_id, node_type} -signal wallet_changed(event: String, gp: int, extra: Dictionary) # GP 钱包事件投影:gain{amount}/buy{product,price}/clear——试玩日志与商店覆盖层刷新源(GP+商店房实施批) +signal level_finished(payload: Dictionary) ## {result, hp, node_index, node_id, node_type} +signal wallet_changed(event: String, gp: int, extra: Dictionary) ## GP 钱包事件投影:gain{amount}/buy{product,price}/clear——试玩日志与商店覆盖层刷新源(GP+商店房实施批) var db: ConfigDb var rng: RngServiceCore @@ -758,12 +758,12 @@ func current_price(prow: Dictionary) -> int: + int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", "")))) -# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×80% provisional, +# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×spend_discount〔表 knob,缺省 0.8〕, # floor 取整偏向玩家、下限 1 防 1 元商品折成 0;范围=商品标价定义边界〔legacy 商品面 current_price # + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折) func _spend_discounted(price: int) -> int: if _tree_rules.has("economy_spend"): - return maxi(1, int(floor(float(price) * 0.8))) + return maxi(1, int(floor(float(price) * db.shop_spend_discount()))) return price @@ -874,12 +874,12 @@ func pool_stock(kind: String) -> int: # 现价 = ceil(基价 × b^(关序−1) × A^(本次进店已购次数))(P4 甲 ceil 向上单一取整——玩家不利向; # A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外) -# 基价 provisional 15 固定锚(台账披露待追认——shop.tres 词表扩列归数值批)。 +# 基价 provisional 15(表 knob relic_base_price——shop.tres 词表扩列归数值批)。 func pool_price(kind: String) -> int: var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind) if row.is_empty(): if kind == "relic": - return _spend_discounted(int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))) + return _spend_discounted(int(ceil(db.shop_relic_base_price() * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))) return 999999 var base := float(maxi(1, int(row.get("base_price", 1)))) var price_f: float = base * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) \ diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 2db833ac..fbf979fe 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -5,9 +5,23 @@ class_name MinesweeperRules extends RefCounted +## 揭示事实流:每格首次进入已开集恰发一次(已开格零事件)。 +## 载荷键集(四处发射位并集):cell: Vector2i/kind: "safe"|"mine"/digit: int(安全格邻雷数;雷格 -1)/ +## variant_ids: Array(地形变异身份,雷格空集)/channel: "initial"|"dig"|"chain"|"fx_reveal"/ +## resource: bool(格面资源兑付资格:initial=false,其余 true)/dug_under_seal: bool(首格带封印直挖标记—— +## initial 通道不发此键)。 +## 发送时点:initial=install_board 初始展开逐格;dig/chain=_do_dig 揭示闭包内(本格 AP 扣费在其后); +## fx_reveal=fx_reveal 闭包内(零 AP 消耗零相位要求)。 +## 消费方依赖:战斗结算 _on_cell_revealed 以 resource 分流缓冲(fx_reveal 由兑付点直入账,禁二次缓冲)、 +## 以 kind 驱动积点计数、以 cell 取奖励快照——禁改键名与语义;事件序 cell_revealed 批先于 seal_judged 批。 signal cell_revealed(evt: Dictionary) +## 封印判定事实流:轮末事务开始时的封印位逐格判定(行主序确定性枚举,被拔的不判)。 +## 载荷键集:cell: Vector2i(封印格)/hit: bool(该格是否为雷)。 +## 发送时点:停手路径 _do_stop 委托行动经济之前;棋盘路径揭示闭包收尾(phase 非 Active 时)—— +## 全路径统一先于 round_end 发射(事件序契约 seal_judged < round_end)。 +## 消费方依赖:战斗结算 _on_seal_judged 收批后统一开封印命中单——载荷缺键即错判,禁改键名与语义。 signal seal_judged(evt: Dictionary) -signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发) +signal round_end_board(payload: Dictionary) ## 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发) var db: ConfigDb var action_economy: ActionEconomy diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 813b5a18..e1433e10 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -56,9 +56,9 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int "stack": for inst in _instances: if inst.effect_id == effect_id: - if inst.layers >= max_stack: + if int(inst.layers) >= max_stack: return false # 满层拒绝原子——容器零写入(拒收由战斗结算记"奖励未生效"行) - inst.layers += 1 # stack 授予不触碰 R(spec 决议) + inst.layers = int(inst.layers) + 1 # stack 授予不触碰 R(spec 决议) return true _instances.append(_new_instance(effect_id, 1, dur, source, snap_m, snap_unit, snap_display)) return true @@ -99,7 +99,7 @@ func query_active(side: String) -> Array: "display_name": inst.snap_display, "snap_magnitude": inst.snap_m, "snap_unit": inst.snap_unit, - "effective_value": inst.layers * inst.snap_m, # L×m 加算(percent 越域不 clamp——收口归 战斗结算) + "effective_value": int(inst.layers) * int(inst.snap_m), # L×m 加算(percent 越域不 clamp——收口归 战斗结算) }) return out @@ -111,7 +111,7 @@ func decay_round() -> int: var k := 0 var survivors: Array[Dictionary] = [] for inst in _instances: - inst.remaining -= 1 + inst.remaining = int(inst.remaining) - 1 if inst.remaining <= 0: k += 1 else: diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index e3ec1fc6..fb0ab1b9 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -52,14 +52,6 @@ func inventory_count() -> int: return _items.size() -# 持有数(同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有) -# 同调位守卫谓词(读取源 transitional_sync_slots 过渡键已随表退役; -# 容量闸现行事实源 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏—— -# 冒烟「过渡键读取点归零」断言覆盖) - - -# 持有件记录(奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) -# 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含) # 整图零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级): # 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用); # 纯预读行 = 板上还有未开格才可用。(积点件不入使用面——自动兑付无玩家入口) diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 2a3416c6..4b89a5e6 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -52,7 +52,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides return false # max_hp = 生效上限(非入参 hero_hp)——跨战斗携带 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": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面) + _hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": db.hero_display_name(), "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(读注入值,空串回退锻造工坊配置默认值 normal/light;甲效矩阵消费面) _enemies.clear() # 难度曲线与Boss DifficultyTable 修饰:施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15) var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth) var hp_mult: float = float(mod.get("hp_mult", 1.0)) @@ -254,7 +254,7 @@ func apply_damage(u: Dictionary, n: int) -> bool: if u.is_empty() or _despawned or not u.alive: _diag("apply_damage 失效/死亡句柄") return false - u.hp = maxi(0, u.hp - n) + u.hp = maxi(0, int(u.hp) - n) if u.hp == 0: u.alive = false # Dead 不可逆(死亡判定时机归 战斗结算) unit_died.emit(u) # 死亡事件——单位自行宣布 @@ -267,7 +267,7 @@ func heal(u: Dictionary, n: int) -> int: _diag("heal 非法/失效句柄") return 0 var actual: int = mini(n, int(u.max_hp) - int(u.hp)) - u.hp += actual + u.hp = int(u.hp) + actual return actual diff --git a/game/data/schemas/hero_table.gd b/game/data/schemas/hero_table.gd index 11f8176f..e567d56d 100644 --- a/game/data/schemas/hero_table.gd +++ b/game/data/schemas/hero_table.gd @@ -8,6 +8,7 @@ extends Resource @export var max_hp: int = 50 # [1,999] knob(provisional,溯源 = 原型锚) @export var atk: int = 6 # [1,999] knob——基础攻击(溯源 = 概念原型 HERO_BASE_ATK,provisional 归 难度曲线与Boss);消费面 = 战斗结算·战斗段先手攻击 @export var armor_class: int = 10 # [0,999] knob——防守方护甲等级基准(暂定 10 归 难度曲线与Boss);消费面 = 战斗结算掷点判定阈值基数(ac_up 效果加算其上) +@export var display_name: String = "探险者" # 英雄单位显示名(与敌人行 display_name 同轴;单位快照读取面单源) func validate() -> Array: @@ -18,4 +19,6 @@ func validate() -> Array: errs.append({"category": "domain", "message": "atk 越域 [1,999]: %d" % atk}) if armor_class < 0 or armor_class > 999: errs.append({"category": "domain", "message": "armor_class 越域 [0,999]: %d" % armor_class}) + if display_name.strip_edges() == "": + errs.append({"category": "domain", "message": "display_name 为空"}) return errs diff --git a/game/data/schemas/shop_table.gd b/game/data/schemas/shop_table.gd index 4868d8da..27837afe 100644 --- a/game/data/schemas/shop_table.gd +++ b/game/data/schemas/shop_table.gd @@ -12,6 +12,8 @@ extends Resource @export var pool_shelf: Array[Dictionary] = [] # Array[Dictionary] 键 kind_id @export var price_a: float = 0.0 # 进店已购次数爬价因子(provisional——0 = 未投值即致命域) @export var price_b: float = 0.0 # 跨关递增因子(provisional) +@export var relic_base_price: float = 15.0 # relic 保底卡基价(provisional——池令牌非货架种,词表外专枝) +@export var spend_discount: float = 0.8 # 财富之道·散 economy_spend 激活时的购买折扣(provisional) const GP_TIERS := ["normal", "elite", "boss"] const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"] @@ -102,4 +104,8 @@ func validate() -> Array: errs.append({"category": "domain", "message": "price_a 越域 [1.0,3.0]: %f" % price_a}) if price_b < 1.0 or price_b > 2.0: errs.append({"category": "domain", "message": "price_b 越域 [1.0,2.0]: %f" % price_b}) + if relic_base_price < 1.0 or relic_base_price > 99.0: + errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price}) + if spend_discount <= 0.0 or spend_discount > 1.0: + errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount}) return errs diff --git a/game/data/tables/hero.tres b/game/data/tables/hero.tres index b7889025..33e9d1ad 100644 --- a/game/data/tables/hero.tres +++ b/game/data/tables/hero.tres @@ -7,6 +7,7 @@ script = ExtResource("1") max_hp = 26 atk = 2 armor_class = 10 +display_name = "探险者" ; 〔2026-10-12 迭代 23 锻造工坊实装批 T2——两根类型轴删除(契约 2 所有权移交):armor_type/ ; attack_type 两字段随本批从静态表删除,默认值归锻造工坊配置常驻持有(forge.tres diff --git a/game/data/tables/shop.tres b/game/data/tables/shop.tres index 8d502b2f..60b6f6a1 100644 --- a/game/data/tables/shop.tres +++ b/game/data/tables/shop.tres @@ -92,6 +92,8 @@ pool_shelf = [{ }] price_a = 1.8 price_b = 1.1 +relic_base_price = 15.0 +spend_discount = 0.8 ; GP+商店房实施批(2026-09-20 用户拍板,设计权威 design/research/proposal-gp-shop-room-2026-09-20.md 裁决记录): ; 掉落=均值±方差掷定(gp_drop 封闭流)+关级加值;精英行为预留行(当前无消费方,随精英节点内容批启用); diff --git a/game/presentation/debug_session.gd b/game/presentation/debug_session.gd index d8702bc3..595a0adb 100644 --- a/game/presentation/debug_session.gd +++ b/game/presentation/debug_session.gd @@ -4,8 +4,8 @@ ## 加成生效值经注入 Callable 现算、随 encounter 显式传参——零表写。 extends RefCounted -signal battle_created(settlement, level: int, carry_hp: int) # begin_battle 前——组合根完成信号接线 -signal battle_ready(settlement, level: int, carry_hp: int) # begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现 +signal battle_created(settlement, level: int, carry_hp: int) ## begin_battle 前——组合根完成信号接线 +signal battle_ready(settlement, level: int, carry_hp: int) ## begin_battle 完成后——组合根 rebuild 棋盘 + 刷新呈现 var db var rng # /root/RngService(组合根注入) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 976a2006..1b919528 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -4,9 +4,9 @@ class_name BoardView extends Control -signal action_requested(cell: Vector2i, kind: String) # kind ∈ {dig, move, seal_place, seal_pull} -signal hover_forbidden(cell: Vector2i) # 非相邻未开格悬停(首遇 toast 归组合根去重) -signal chord_attempted(cell: Vector2i) # 双击快开尝试(chord 肌肉记忆提示锚) +signal action_requested(cell: Vector2i, kind: String) ## kind ∈ {dig, move, seal_place, seal_pull} +signal hover_forbidden(cell: Vector2i) ## 非相邻未开格悬停(首遇 toast 归组合根去重) +signal chord_attempted(cell: Vector2i) ## 双击快开尝试(chord 肌肉记忆提示锚) const _CELL_UNIT := preload("res://presentation/views/board_cell_unit.gd") const _BOARD_WORLD := preload("res://presentation/views/board_world.gd") diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 543c0380..36d95259 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -7,12 +7,12 @@ extends Control signal buy_requested(attr: String) signal closed -signal attune_toggled(item_id: String) # 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement) -signal tree_buy_requested(node_id: String) # 天赋树购买意图(写口唯一归 MetaProgression.tree_buy) -signal tree_fork_toggled(group: String, side: String) # 双态切换意图(一节点双态——零币切态) -signal forge_buy_requested(route_id: String) # 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) -signal forge_unlock_requested(route_id: String) # 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) -signal forge_select_requested(route_id: String) # 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor) +signal attune_toggled(item_id: String) ## 同调装配切换意图(写口唯一归 MetaProgression.toggle_attunement) +signal tree_buy_requested(node_id: String) ## 天赋树购买意图(写口唯一归 MetaProgression.tree_buy) +signal tree_fork_toggled(group: String, side: String) ## 双态切换意图(一节点双态——零币切态) +signal forge_buy_requested(route_id: String) ## 锻造路线购买意图(写口唯一归 MetaProgression.forge_buy_level) +signal forge_unlock_requested(route_id: String) ## 锻造路线解锁意图(写口唯一归 MetaProgression.forge_buy_unlock) +signal forge_select_requested(route_id: String) ## 锻造路线选中意图(写口唯一归 MetaProgression.forge_select_weapon/armor) # 方向稿取色(柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index cf46d111..7fb16c8d 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -4,8 +4,8 @@ class_name DebugOverlay extends Control -signal apply_requested(atk_pct: int, hp_pct: int) # 应用并关闭——加成落表归组合根单一应用路径 -signal simulate_requested(count: int) # 批量模拟编排归组合根(表基线复位 + 单路径施加) +signal apply_requested(atk_pct: int, hp_pct: int) ## 应用并关闭——加成落表归组合根单一应用路径 +signal simulate_requested(count: int) ## 批量模拟编排归组合根(表基线复位 + 单路径施加) signal debug_levels_requested # 两层调试对局入口(编排归 debug_session.gd) signal diagnose_requested # 诊断转储入口(卡死取证编排归组合根) signal closed diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index bc020fff..7b1c5ca5 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -8,7 +8,7 @@ const COL_INK := UiKit.INK var _atk_label: Label var _armor_label: Label -signal treasure_slot_clicked(handle: int) # 槽点击(组合根消费:AP 类即用/空间类进选中态) +signal treasure_slot_clicked(handle: int) ## 槽点击(组合根消费:AP 类即用/空间类进选中态) var item_slots: HBoxContainer # 秘宝槽(库存条常读+两步交互第一步) var _slot_panels: Array[Variant] = [] # 槽 PanelContainer 引用(选中高亮/置灰重刷) var _selected_handle := 0 # 选中态(0=无;组合根回写维持单源) diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 3b905b40..2db37149 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -4,7 +4,7 @@ class_name ShopOverlay extends Control -signal buy_requested(product_id: String) # legacy=product_id / pool=kind_id(组合根按模式分发) +signal buy_requested(product_id: String) ## legacy=product_id / pool=kind_id(组合根按模式分发) signal leave_pressed const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表) diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index 2aba0e3c..400f58a0 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -29,6 +29,7 @@ func _init() -> void: _test_five_recipes_and_replay(db) _test_policy_immediate_mode(db) _test_source_death_void(db) + _test_identical_content_dual_enemy(db) _test_requires_gate_and_zero(db) _test_hero_death_halt(db) _test_ac_merge(db) @@ -211,6 +212,46 @@ func _test_source_death_void(db) -> void: Kit.check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty()) +# ---- 同内容双敌:单据归属按实例身份判定(生产 handle_id 使内容全同不可达——夹具显式钉住语义面) ---- + +# 单位字典键集复刻(剥离 handle_id:生产键集含句柄,`==` 误命的可达条件被破坏,夹具刻意不携句柄) +func _unit_fixture() -> Dictionary: + return { + "enemy_id": "fixture_enemy", + "display_name": "哥布林", + "hp": 5, "max_hp": 5, "atk": 1, + "pool": [], "pending_skill": "", "roll_seq": 0, "basic_streak": 0, + "armor_type": "light", "armor_class": 0, "attack_type": "normal", + "alive": true, "is_hero": false, + } + + +func _test_identical_content_dual_enemy(db) -> void: + var a := _unit_fixture() + var b := _unit_fixture() + Kit.check("前置:双敌字典内容全同且非同一实例", a == b and not is_same(a, b)) + var h = _harness(db) + if not h.spawn_ok: + return + var ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", + "同容双敌单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1) + ticket["source_unit"] = a + var t: int = h.ctrl.submit(ticket) + h.units.unit_died.emit(b) # 真实死亡事件(单位自行宣布)——同容异实例 + h.ctrl.flush() + Kit.check("同内容异实例敌死亡不误废他人单据〔wd1〕", h.ctrl.outcome(t) == "redeemed") + var h2 = _harness(db) + if not h2.spawn_ok: + return + var ticket2: Dictionary = h2.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", + "同容双敌单·自亡", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 1) + ticket2["source_unit"] = a + var t2: int = h2.ctrl.submit(ticket2) + h2.units.unit_died.emit(a) + h2.ctrl.flush() + Kit.check("来源敌自身死亡单据作废(正向对照)〔wd1〕", h2.ctrl.outcome(t2) == "voided") + + # ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ---- func _test_requires_gate_and_zero(db) -> void: diff --git a/game/tests/unit/core/board_gen_reward_test.gd b/game/tests/unit/core/board_gen_reward_test.gd index 69a0d3b6..b8756e49 100644 --- a/game/tests/unit/core/board_gen_reward_test.gd +++ b/game/tests/unit/core/board_gen_reward_test.gd @@ -157,7 +157,7 @@ func _smoke_board_generator() -> void: if b1 == null: continue Kit.check("L%d clearable(安全格 8 邻连通)" % L, board_generator.is_clearable(b1)) - Kit.check("L%d legality 全过" % L, board_generator.legality(b1, row, _db)) + Kit.check("L%d legality 全过" % L, board_generator.legality(b1, row)) var inf := float(b1.metrics.get("inferability", 0.0)) var mode := String(b1.metrics.get("accept_mode", "")) print(" L%d 实测:inferability=%.3f / attempts_used=%d / accept_mode=%s / target=%.2f" % [ diff --git a/game/tests/unit/core/board_generator_test.gd b/game/tests/unit/core/board_generator_test.gd index d35262af..3e7cc062 100644 --- a/game/tests/unit/core/board_generator_test.gd +++ b/game/tests/unit/core/board_generator_test.gd @@ -18,6 +18,7 @@ func _init() -> void: quit(Kit.finish("board_generator_test")) return _test_generate_shape_and_mines() + _test_target_mine_count_single_source() _test_digits_equal_neighbor_mine_count() _test_same_seed_bit_identical() _test_clearable_and_inferability_selfconsistent() @@ -54,6 +55,34 @@ func _test_generate_shape_and_mines() -> void: Kit.check("生成期指标集已落盘(metrics 非空)", not b.metrics.is_empty()) +# 目标雷数公式单点(d14):同参同值对拍原式 + 真实产图指标读数同源 +func _test_target_mine_count_single_source() -> void: + var bad := 0 + var checked := 0 + for L in [1, 2, 3]: + var row: Dictionary = _db.resolve_board_gen(L, 0) + if row.is_empty(): + continue + var params := { + "w": int(row.board_width), "h": int(row.board_height), + "density": float(row.mine_density), + } + var from_fn: int = BoardGeneratorScript.target_mine_count(params) + var from_expr: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) + checked += 1 + if from_fn != from_expr: + bad += 1 + Kit.check("target_mine_count 与原式同参同值(%d 档对拍全等)〔wd14〕" % checked, checked > 0 and bad == 0) + var b := _generate(1, 0, 74000) + if b == null: + Kit.check("生成返回棋盘(雷数单点段)", false) + return + var row1: Dictionary = _db.resolve_board_gen(1, 0) + Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕", + int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({ + "w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)})) + + func _test_digits_equal_neighbor_mine_count() -> void: var b := _generate(1, 1, 72000) if b == null: diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 9f4049e7..a0de0c92 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -242,6 +242,7 @@ func _init() -> void: if d.severity == "error" or d.severity == "fatal": push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) # 套执行序沿源编排(基点 _run_core_smoke 调用序——本组套的相对序) + _smoke_table_migration() _smoke_level() _smoke_level_graph() _smoke_gp_wallet() @@ -254,6 +255,20 @@ func _init() -> void: quit(Kit.finish("level_shop_economy_test")) +# ---- 数值入表等价证明(d11/d16a——原字面量迁移:表值 == 迁移前字面量,消费端读表)---- + +func _smoke_table_migration() -> void: + print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---") + Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.shop_relic_base_price() == 15.0) + Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.shop_spend_discount() == 0.8) + Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.hero_display_name() == "探险者") + # 消费端读表:spawn 真实构造链的英雄单位显示名与表值同 + var units = load("res://core/unit_container.gd").new(_db) + var spawned: bool = units.spawn(1, 0, int(_db.hero.max_hp)) + Kit.check("spawn 后英雄单位 display_name 读表同值(探险者)〔wd16〕", + spawned and String(units.hero_unit().get("display_name", "")) == _db.hero_display_name()) + + # ---- 生成图深度扩面覆盖断言(迭代 35 卡② S5/V11——节点源=卡① 生成器产图, # 敌侧/板侧解析一律走 ConfigDB 真实消费链,禁直读行集自算)---- @@ -1274,15 +1289,19 @@ func _smoke_shop_pool() -> void: var r8 = scen["r"] # ② 货架投影形制:kind 词表封闭 + 现价公式推导(ceil(base×b^(关序−1)×A^0)——核心现算,呈现零复算) var l2 := int(r8.level_row.level_index) + # 现价等价证明(d11——消费端读表推导 vs 迁移前硬编码字面量推导:同输入逐位一致) + var relic_price_now: int = r8.pool_price("relic") + var relic_price_pre: int = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) + Kit.check("relic 现价 = ceil(15.0×b^(关序−1))(读表与迁移前字面量逐位一致,%d)〔wd11〕" % relic_price_now, relic_price_now == relic_price_pre) for c in r8.shop_shelf(): # T6 商店保底后 relic 卡合法入架(保底占位)——词表断言放行 relic;价格断言分叉 - # (数值卡走 shop_pool_row 推导 / relic 卡走 15 固定锚——同公式不同基价) + # (数值卡走 shop_pool_row 推导 / relic 卡走表 knob relic_base_price——同公式不同基价) var ckind := String(c.get("kind_id", "")) var is_relic := ckind == "relic" Kit.check("货架 kind 在封闭词表或保底 relic 卡(%s)" % ckind, is_relic or shelf_kinds.has(ckind)) var expect_p: int if is_relic: - expect_p = int(ceil(15.0 * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) + expect_p = int(ceil(_db.shop_relic_base_price() * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0)) else: var prow: Dictionary = _db.shop_pool_row(ckind) expect_p = int(ceil(float(int(prow.get("base_price", 0))) * pow(_db.shop_price_b(), float(l2 - 1)) * 1.0))