diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 206061fa..cb97bd95 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -43,7 +43,9 @@ var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control var _inter_label: Label -var route_overlay: RouteOverlay # 节点图选路覆盖层(分支节点图批——零自动推进停点) +var level_screen: LevelMapScreen # 关卡画面(独立屏——2026-09-28 形态升格批:激活期战斗屏宿主整体隐藏;零自动推进停点语义不变) +var _battle_root: Control # 战斗屏宿主(B/C/D 三区容器——独立屏激活期 visible=false 级互斥) +var _level_screen_active := false # 独立屏激活状态(覆盖兑付覆盖层在屏域内弹出期间——战斗屏保持隐藏) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null @@ -99,6 +101,7 @@ func _ready() -> void: func _build_ui() -> void: var root := VBoxContainer.new() add_child(root) + _battle_root = root # 独立屏互斥面(2026-09-28 形态升格批) var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db) stage = battle_ui.stage log_view = battle_ui.log_view @@ -132,15 +135,16 @@ func _build_ui() -> void: shop_overlay = overlays.shop_overlay camp_overlay = overlays.camp_overlay tune_panel = overlays.tune_panel - route_overlay = RouteOverlay.new() - add_child(route_overlay) - # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——选路层须夹在节点层与商店层之间 - move_child(route_overlay, node_overlay.get_index() + 1) + level_screen = LevelMapScreen.new() + add_child(level_screen) + # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——关卡画面须夹在节点层与商店层之间 + #(兑付覆盖层在本屏域内弹出:模态栈打开低层时自动隐藏高层本屏、关闭自动恢复——零机制改动复用) + move_child(level_screen, node_overlay.get_index() + 1) legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay)) review_overlay.closed.connect(_on_modal_closed.bind(review_overlay)) node_overlay.continue_pressed.connect(_on_node_continue) - route_overlay.route_chosen.connect(_on_route_chosen) - route_overlay.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119) + level_screen.route_chosen.connect(_on_route_chosen) + level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119) shop_overlay.buy_requested.connect(_on_shop_buy) shop_overlay.leave_pressed.connect(_on_shop_leave) camp_overlay.buy_requested.connect(_on_camp_buy) @@ -151,7 +155,7 @@ func _build_ui() -> void: tune_panel.apply_requested.connect(_on_tune_apply) tune_panel.simulate_requested.connect(_on_tune_simulate) _modals = ModalStack.new() - for m: Control in [legend_overlay, review_overlay, node_overlay, route_overlay, _end_overlay, shop_overlay, camp_overlay, tune_panel]: + for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, _end_overlay, shop_overlay, camp_overlay, tune_panel]: _register_modal(m) _tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp)) @@ -205,7 +209,7 @@ func _on_modal_closed(m: Control) -> void: func _input_locked() -> bool: # 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定) return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \ - or node_overlay.visible or route_overlay.visible or shop_overlay.visible or tune_panel.visible \ + or node_overlay.visible or level_screen.visible or shop_overlay.visible or tune_panel.visible \ or _start_overlay.visible or _inter_overlay.visible or settlement == null @@ -231,7 +235,7 @@ func _start_adventure() -> void: adventure.adventure_finished.connect(_on_adventure_finished) adventure.level_started.connect(_on_adventure_level_started) _level_active = true - route_overlay.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板) + level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板) _reset_run_scope() log_view.clear() var seed_val := _seed_value() @@ -304,7 +308,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: _level_active = false var result: String = String(summary.get("result", "")) settlement = null - _close_modal(route_overlay) # 终局兜底(放弃入口新场景:选路层在开态——走模态栈恢复其遮挡面;未开过时 close 容错) + _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 _sync_settlement_visuals() # → 全关 stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体 2026-09-26 迁 B 区站桩) stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) @@ -572,17 +576,36 @@ func _on_choices_presented(candidates: Array) -> void: if runner == null: return log_view.append_bb(Narrator.route_line(candidates, runner)) - _open_modal(route_overlay) - route_overlay.open_choices(candidates, runner) + _enter_level_screen() + level_screen.open_choices(candidates, runner) _refresh_presentations() func _on_route_chosen(node_id: int) -> void: - _close_modal(route_overlay) + _close_modal(level_screen) + if not level_screen.visible: + _exit_level_screen() # 战斗/Boss 节点:切回战斗屏;奖励/恢复/商店:本屏保持激活(T3 接管域内弹出) if runner != null and not runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) +# ---- 独立屏屏级互斥(形态升格批 2026-09-28——单一出口:进屏隐战斗屏 / 出屏显战斗屏)---- +# 兑付覆盖层(节点/商店)在本屏激活域内弹出期间保持隐藏态不变——模态栈打开低层时隐藏本屏、 +# 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 + +func _enter_level_screen() -> void: + _level_screen_active = true + _battle_root.visible = false + _open_modal(level_screen) + + +func _exit_level_screen() -> void: + _level_screen_active = false + _battle_root.visible = true + if level_screen.visible: + _close_modal(level_screen) + + func _on_abandon_requested() -> void: # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, diff --git a/game/presentation/views/route_overlay.gd b/game/presentation/views/level_map_screen.gd similarity index 92% rename from game/presentation/views/route_overlay.gd rename to game/presentation/views/level_map_screen.gd index f343aa34..e80c3718 100644 --- a/game/presentation/views/route_overlay.gd +++ b/game/presentation/views/level_map_screen.gd @@ -7,14 +7,16 @@ # ③ 放弃冒险二次确认(布局决策 3)——首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击提交 # ④ 顶栏语境:关卡名(level_row.display_name)+ GP 常驻徽章(2026-09-26 二轮拍板去向落地面) # ⑤ 底部状态栏(布局决策 4):生命/属性/本关加成三组常读(零加成隐藏),放弃按钮右端异位 -# 节点图选路覆盖层(模态):兑付确认后由组合根打开。零自动推进的玩家停点语义:唯一前进通道 = +# 关卡画面(独立屏,Level Map Screen——2026-09-28 形态升格批 M-2 迭代 3 T2:原 RouteOverlay 模态 +# 覆盖层升格独立屏,spec level-map.md 形态章节同批改写):全屏不透明独占呈现——激活期战斗屏宿主 +# 由组合根整体隐藏(非暗幕半透、零透出)。零自动推进的玩家停点语义不变:唯一前进通道 = # 图上点候选节点 → route_chosen(node_id) → 组合根转 runner.pick_next_node。 # 高内聚边界:只读 runner 成文出口(nodes()/visited_ids()/node_index/next_candidate_ids()/level_row/ # hero_hp/gp/effective_*/bonus)做渲染,无任何写面;图形为程序化占位质量(占位纪律)。 # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——台账 119):选路停点 = 天然非战斗期(接口契约: # 战斗中放弃一律拒绝),按钮常驻底栏右端;可见性归组合根(只在冒险模式开——放弃的语义对象是 # "整局冒险";调试两层无节点图天然不可达)。 -class_name RouteOverlay +class_name LevelMapScreen extends Control signal route_chosen(node_id: int) @@ -54,13 +56,23 @@ var _confirm_left := 0.0 # 确认态剩余秒数(_process 倒计 func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(0, 0, 0, 0.6) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) + # 独立屏底(形态升格 2026-09-28——spec Layout:深底+石纹底噪铺满;底纹缺位回退纯色——load_tex 守卫) + var base := ColorRect.new() + base.color = UiKit.BG_ROOT + base.set_anchors_preset(Control.PRESET_FULL_RECT) + add_child(base) + var noise := UiKit.load_tex(UiKit.TEX_BACKDROP_STONE) + if noise != null: + var tex := TextureRect.new() + tex.texture = noise + tex.stretch_mode = TextureRect.STRETCH_TILE + tex.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + tex.set_anchors_preset(Control.PRESET_FULL_RECT) + tex.mouse_filter = Control.MOUSE_FILTER_IGNORE + base.add_child(tex) var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) + base.add_child(center) var box := VBoxContainer.new() box.add_theme_constant_override("separation", 10) center.add_child(box) @@ -195,6 +207,7 @@ func _process(delta: float) -> void: # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) func open_choices(cands: Array, runner) -> void: + var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) _runner = runner _hover_id = -1 _reset_abandon_confirm() @@ -213,6 +226,11 @@ func open_choices(cands: Array, runner) -> void: _fit_canvas(_runner.nodes()) _refresh_status() visible = true + if not was_visible: + # 独立屏淡入(spec Transitions:约 200ms;alpha 淡入非明度变化——art-bible §2 明度恒定) + modulate.a = 0.0 + var ftw := create_tween() + ftw.tween_property(self, "modulate:a", 1.0, UiKit.motion(0.2)) # 画布随图自适应(level-map.md Layout:画布宽随层数、高随同层最大节点数——公式与 _layout 同源; diff --git a/game/presentation/views/route_overlay.gd.uid b/game/presentation/views/level_map_screen.gd.uid similarity index 100% rename from game/presentation/views/route_overlay.gd.uid rename to game/presentation/views/level_map_screen.gd.uid diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index f3a4d66f..77a45eb7 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -90,8 +90,8 @@ func _ready() -> void: await get_tree().process_frame # 停点前移批 2026-09-23:关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战(候选按钮退役 2026-09-28) #(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装) - if main.route_overlay.visible: - main.route_overlay.press_candidate(0) + if main.level_screen.visible: + main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var t_entry := Time.get_ticks_msec() - t1 @@ -113,9 +113,9 @@ func _ready() -> void: main.node_overlay.press_continue() _flags_placed.clear() continue - if main.route_overlay.visible: + if main.level_screen.visible: # 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造) - main.route_overlay.press_candidate(_route_clicks % 2) + main.level_screen.press_candidate(_route_clicks % 2) _route_clicks += 1 _flags_placed.clear() continue diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index 914ab986..12299d0d 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -31,8 +31,8 @@ func _ready() -> void: # push_input 真实鼠标路径 motion+click → 悬停预承诺置位 → route_chosen 到达组合根。 # 由来=真机 bug:深层节点(L1 Boss 内容 x=802)在固定画布命中区外,press_candidate 直调 # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 - if main.route_overlay.visible: - var ro = main.route_overlay + if main.level_screen.visible: + var ro = main.level_screen var cands: Array = main.runner.next_candidate_ids() var nds: Array = main.runner.nodes() var tgt := {} @@ -56,13 +56,13 @@ func _ready() -> void: main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame - if main.route_overlay.visible or main.runner.awaiting_choice(): - _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.route_overlay.visible), str(main.runner.awaiting_choice())]) + if main.level_screen.visible or main.runner.awaiting_choice(): + _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) - var ro2 = main.route_overlay + var ro2 = main.level_screen var nds2: Array = main.runner.nodes() var layer2 := {} var inbound2 := {} diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 996f1a3a..b463be4c 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -15,7 +15,7 @@ func _ready() -> void: await get_tree().process_frame var problems: Array = [] # ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(非冒险模式无冒险可放弃——开始层/调试两层)---- - if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible: + if main.level_screen.abandon_btn == null or main.level_screen.abandon_btn.visible: print("[ui_probe] FAIL — 放弃按钮默认应隐藏(非冒险模式关)") get_tree().quit(1) return @@ -63,20 +63,20 @@ func _ready() -> void: await get_tree().process_frame # ---- 入口选路停点(停点前移批 2026-09-23:关卡开始即出节点图——level_started 后、首战前弹选路层; # press_candidate 图上命中路径 pick 入口单候选进首战——候选按钮退役 2026-09-28)---- - if not main.route_overlay.visible: + if not main.level_screen.visible: problems.append("关卡开始未弹入口选路停点(route_overlay 应可见)") elif main.runner == null or not main.runner.awaiting_choice() \ or main.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: - main.route_overlay.press_candidate(0) + main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame # ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)---- - if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible: + if main.level_screen.abandon_btn == null or not main.level_screen.abandon_btn.visible: problems.append("冒险模式放弃按钮未开放(选路停点入口在案)") else: - main.route_overlay.abandon_btn.pressed.emit() + main.level_screen.abandon_btn.pressed.emit() await get_tree().process_frame if main.adventure == null or String(main.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") diff --git a/game/smoke/ui_tune_probe.gd b/game/smoke/ui_tune_probe.gd index c392b676..336f2182 100644 --- a/game/smoke/ui_tune_probe.gd +++ b/game/smoke/ui_tune_probe.gd @@ -65,8 +65,8 @@ func _ready() -> void: await get_tree().process_frame await get_tree().process_frame # 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 - if main.route_overlay.visible: - main.route_overlay.press_candidate(0) + if main.level_screen.visible: + main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame var hero: Dictionary = main.settlement.presentation_snapshot().hero