feat(flow): 停点前移批——LevelRunner 入口停点模式(opt-in defer+零RNG漂移)+关卡开局即出图选路+A 区进度条退役(转 C 区钱包行,底行 548)+GDD 四文件对齐三区+art-bible 连带+06 条目解决移除(用户拍板 2026-09-23,台账 142)

This commit is contained in:
DKChan committed 2026-09-24 00:59:14 +08:00
1 parent c22174274c
commit 8eddcdd5a7
19 files changed
+190 -208

No files matched your search

@@ -24,6 +24,12 @@
# continue_to_next_level 开始下一关,两步间为呈现层大奖励屏(冒烟/自动路径立即确认)。
# 结算受理只算一次(契约):state != "running" 的关卡结束宣布与放弃请求一律拒绝并留诊断,
# 账本与摘要零变化。
# 停点前移批(2026-09-23,用户拍板「关卡开始即出节点图选路、选节点才进战斗」):每关开始 =
# 入口选路停点——_start_current_level 以 defer_entry 起关,level_started 之后才 present_entry_choice
# (信号顺序契约:呈现层先重取 runner 实例,选路口才不会被空防御丢弃);pick 入口候选走同一
# _enter_node(entry)、呈现零 RNG 消耗——与旧「start 直进首战」逐位等价,测量面零漂移。
# 关卡切换(continue_to_next_level)后新 LevelRunner 实例重新走 defer 流程;abandon 在入口停点
# 天然非战斗期(current_battle 为空)——受理语义与选路停点一致。
# 纪律:ADR-0001 S6——连 runner 信号一律 _forward_* 方法 Callable(禁 lambda 捕获 self 成环);
# Core 层禁 UI 引用;数值零字面量——level_clear_hp_bonus / keystone_per_level 只读 db.adventure,
# 基础上限只读 db.hero。
@@ -153,11 +159,15 @@ func _start_current_level() -> bool:
_runner.level_finished.connect(_on_level_finished)
_runner.set_hero_override(int(_override[0]), int(_override[1]))
# 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好);
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)
# _base_override 共享引用随载荷传入(永久强化 flat——LevelRunner.effective_* 叠加序第二项)。
# 停点前移批 2026-09-23:defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在
# 该回调重取 runner 实例),后 present_entry_choice()(入口候选呈现;顺序反了选路口会被呈现层
# 的 runner 空防御静默丢弃)。呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
Callable(self, "apply_rest"), _base_override)
Callable(self, "apply_rest"), _base_override, true)
if ok:
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
_runner.present_entry_choice()
return ok
@@ -158,8 +158,15 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary =
# base_override = 永久强化 flat 共享引用(元进度实现批——缺省 {} 全零白板,与 B1 空参数路径逐位一致)。
# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)),
# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。
# 停点前移批 2026-09-23:defer_entry 尾参(缺省 false = 既有调用方零变更)——true 时不自动
# _enter_node(entry),改置选路态(awaiting_choice + 单候选 = 入口节点),由 AdventureRunner 在
# level_started 之后调 present_entry_choice() 呈现、玩家 pick_next_node 显式进首战。拆两步的
# 信号顺序契约:呈现层须先经 level_started 重取 runner 实例,choices_presented 才不会被
# _on_choices_presented 的 runner 空防御静默丢弃。入口呈现零 RNG 消耗、pick 走同一个
# _enter_node(entry)——与旧自动进入逐位等价(测量面零漂移红线)。
func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary,
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}) -> bool:
purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {},
defer_entry: bool = false) -> bool:
var row := _find_level_row(level_index)
if row.is_empty():
push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index)
@@ -187,9 +194,23 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti
push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)")
_running = false
return false
if defer_entry:
_awaiting_choice = true # 入口停点(node_index 保持 -1——进节点前无 current_node)
_candidates = [entry]
return true
return _enter_node(entry)
# 入口停点呈现(停点前移批 2026-09-23——defer_entry=true 的第二步):发出入口候选供呈现层弹
# 选路层;非停点态(缺省自动进入路径)拒绝。零 RNG 消耗——呈现与行进取号解耦。
func present_entry_choice() -> bool:
if not _running or not _awaiting_choice or _candidates.is_empty() or node_index != -1:
push_error("[关卡节点地图] present_entry_choice 拒绝——非入口停点态(defer_entry 未开或已 pick)")
return false
choices_presented.emit(_candidates)
return true
func _find_level_row(level_index: int) -> Dictionary:
for row in db.level.rows:
if int(row.level_index) == level_index:
@@ -48,17 +48,17 @@ var route_overlay: RouteOverlay # 节点图选路覆盖层(分支节点图
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
var _debug = null
# 视图(views/)——battle.md 四区(E 区道具面板随可选类删除下线)
var strip_view: LevelStripView # A 区
# 视图(views/)——battle.md 三区(A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役——节点链进度
# 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线)
var stage: BattleStageView # B 区(含 log_view 明细带)
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区
var info_panel: HeroInfoPanelView # C 区(含钱包行——原 A 区顶栏 GP/钥石余额常驻位)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下)
var gp_label: Label # A 区顶栏 GP 余额常驻位(GP+商店房实施批——文案装配归 HudPresenter)
var keystone_label: Label # A 区顶栏钥石余额常驻位(元进度实现批——文案装配归 HudPresenter)
var gp_label: Label # GP 余额常驻位(C 区钱包行——文案装配归 HudPresenter)
var keystone_label: Label # 钥石余额常驻位(C 区钱包行——文案装配归 HudPresenter)
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
var legend_overlay: LegendOverlay
var review_overlay: SettlementReviewOverlay
@@ -102,7 +102,6 @@ func _build_ui() -> void:
var root := VBoxContainer.new()
add_child(root)
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
strip_view = battle_ui.strip_view
gp_label = battle_ui.gp_label
keystone_label = battle_ui.keystone_label
stage = battle_ui.stage
@@ -248,10 +247,10 @@ func _start_adventure() -> void:
return
# 每关开始(冒险模式):重取 runner 实例 + 图条刷新 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达)
# 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达;
# 停点前移批 2026-09-23:本回调返回后才有 choices_presented 到达——入口选路层随即弹出)
func _on_adventure_level_started(level_index: int, display_name: String) -> void:
runner = adventure.runner()
strip_view.refresh(runner)
log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed),
int(db.hero.atk), int(db.hero.max_hp),
tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0))
@@ -291,7 +290,6 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
shop_overlay.open_shop(runner.shop_products(), int(runner.gp))
log_view.append_bb(Narrator.shop_line(int(runner.gp)))
logger.log_node(node, info, runner.node_index)
strip_view.refresh(runner)
_refresh_presentations()
@@ -329,7 +327,6 @@ func _on_adventure_finished(summary: Dictionary) -> void:
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
if runner != null:
runner.restore_hero_base()
strip_view.refresh(runner)
_open_modal(_end_overlay)
_refresh_presentations()
@@ -355,7 +352,6 @@ func _on_battle_end_continue() -> void:
adventure = null
_adventure_mode = false
tune_panel.reset_sliders()
strip_view.refresh(null)
_start_overlay.visible = true
_refresh_presentations()
@@ -7,39 +7,22 @@
extends RefCounted
# 战斗屏四区(A 进度条 / B 战斗画面 / C·D 两列——E 区道具面板已随可选类删除下线,2026-09-15 格面资源批)
# 战斗屏三区(B 战斗画面 / C·D 两列——A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役:节点链
# 进度由关卡画面本体(选路覆盖层整图)承载,28px 纵向空间拨给 C/D 底行 520→548;E 区道具面板
# 已随可选类删除下线,2026-09-15 格面资源批)。
# + 结算演出覆盖层(A–D 之上、模态之下——跨区弹道/光带载体,结算演出链批 2026-09-20)。
# 返回句柄字典:{strip_view, gp_label, stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
# 返回句柄字典:{gp_label, keystone_label, stage, log_view, info_panel, board_zone, board_view, ap_bar, settlement_fx}
static func build_battle_ui(root: Control, db) -> Dictionary:
# set_anchors_and_offsets_preset(非 set_anchors_preset):后者会把控件重置回最小内容尺寸
# (实测 927×613——缩顶、棋盘 45px 格 ×10 行超出 D 区 333px 高被裁,2026-09-13 用户报告回归)
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
# A 区:关卡进度条(全宽 × 28px 顶栏)+ GP 余额常驻位(GP+商店房实施批 2026-09-21——
# 顶栏邻接位,显示名「金币」;文案装配归 HudPresenter,此处只建节点)
var strip_panel := PanelContainer.new()
strip_panel.custom_minimum_size = Vector2(0, 28)
var sb := UiKit.panel_flat(UiKit.BG_ROOT, UiKit.BG_ROOT, 0, 0)
strip_panel.add_theme_stylebox_override("panel", sb)
root.add_child(strip_panel)
var strip_row := HBoxContainer.new()
strip_panel.add_child(strip_row)
var strip_view := LevelStripView.new()
strip_view.size_flags_horizontal = Control.SIZE_EXPAND_FILL
strip_row.add_child(strip_view)
var gp_label := UiKit.label("金币 0", 15, true)
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
strip_row.add_child(gp_label)
var keystone_label := UiKit.label("钥石 0", 15, true) # 元进度实现批——A 区顶栏钥石余额常驻(文案装配归 HudPresenter,此处只建节点)
keystone_label.add_theme_color_override("font_color", UiKit.GOLD)
strip_row.add_child(keystone_label)
# B 区:战斗画面(全宽 × 252px——常驻双舞台,布局决策 D1)
var stage := BattleStageView.new(db)
root.add_child(stage)
# C/D 两列(520px)——E 区库存面板删除后 D 区横向加宽
# C/D 两列(548px——停点前移批 2026-09-23:原 A 区 28px 顶栏纵向空间并入)
var bottom := HBoxContainer.new()
bottom.size_flags_vertical = Control.SIZE_EXPAND_FILL
bottom.add_theme_constant_override("separation", 4)
@@ -56,10 +39,11 @@ static func build_battle_ui(root: Control, db) -> Dictionary:
var settlement_fx := SettlementFxView.new()
root.add_child(settlement_fx)
# GP/钥石余额常驻位(GP+商店房实施批 2026-09-21 原居 A 区顶栏——顶栏退役后迁 C 区底行,
# 句柄经 info_panel 视图成员透出;文案装配归 HudPresenter,此处只透传引用)
return {
"strip_view": strip_view,
"gp_label": gp_label,
"keystone_label": keystone_label,
"gp_label": info_panel.gp_label,
"keystone_label": info_panel.keystone_label,
"stage": stage,
"log_view": stage.log_view,
"info_panel": info_panel,
@@ -1,5 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(D 区扫雷图 ~760×520 容器——顶部 AP 条 44px〔左:点阵+停手;
# 实现 GDD:design/ux/battle.md(D 区扫雷图 ~760×548 容器〔底行 520→548——停点前移批 2026-09-23,
# 原 A 区顶栏空间并入〕——顶部 AP 条 44px〔左:点阵+停手;
# 右:图例+弃图刷新〕、棋盘主体居中、toast 槽上缘内侧、演出层)。
# Date: 2026-09-12
# 高内聚边界:容器组合 + 信号转发——动作意图全部信号外发到组合根,零核心写调用。
@@ -1,5 +1,6 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×520——HP/攻击(+修正)/RAP 直角细线数据框
# 实现 GDD:design/ux/battle.md(C 区角色信息 240×548〔底行 520→548——停点前移批 2026-09-23,
# 原 A 区顶栏空间并入〕——HP/攻击(+修正)/RAP 直角细线数据框
# 〔修正来源角标〕、状态效果槽 ×4、小人立绘占位;RAP 清零可见反馈〔行动经济 义务〕;
# 状态槽空态 = 槽隐藏上移补位〔EC12〕)。
# Date: 2026-09-12
@@ -20,6 +21,8 @@ var _rap_label: Label
var _rap_feedback: Label
var _status_feedback: Label
var slot_row: StatusSlotRow
var gp_label: Label # 冒险钱包余额常驻位(停点前移批 2026-09-23——原 A 区顶栏迁入 C 区底行)
var keystone_label: Label # 钥石余额常驻位(同上——文案装配归 HudPresenter)
var _last_rap := 0 # 上次投影值(清零反馈判据——禁解析自身 label 文本当状态)
@@ -76,6 +79,20 @@ func _init() -> void:
portrait_glyph.set_anchors_preset(Control.PRESET_FULL_RECT)
portrait.add_child(portrait_glyph)
# 钱包行(停点前移批 2026-09-23:A 区 28px 顶栏退役——GP/钥石余额常驻位迁本面板底行,
# 建节点归视图、文案装配归 HudPresenter,句柄经 ui_assembler 透传组合根)
var wallet_row := HBoxContainer.new()
wallet_row.add_theme_constant_override("separation", 10)
col.add_child(wallet_row)
gp_label = UiKit.label("金币 0", 15, true)
gp_label.add_theme_color_override("font_color", UiKit.GOLD_DEEP)
wallet_row.add_child(gp_label)
keystone_label = UiKit.label("钥石 0", 15, true)
keystone_label.add_theme_color_override("font_color", UiKit.GOLD)
keystone_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
keystone_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
wallet_row.add_child(keystone_label)
func set_hero(hp: int, max_hp: int) -> void:
_hp_bar.max_value = maxi(1, max_hp)
@@ -1,98 +0,0 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 实现 GDD:design/ux/battle.md(A 区关卡进度条——全宽 × 28px 顶栏,当前节点高亮、已过节点暗金)。
# Date: 2026-09-12(battle.md 战斗屏改造批——对齐 28px 顶栏与高亮样式;基础 = 2026-09-11 关卡节点地图-lite 呈现)
# 2026-09-17 分支节点图批:图形态行集渲染 = 行进序列(已到暗金 → 当前金 → 可选下一跳亮边候选);
# 单链形态(第一关零迁移)保持全链横条渲染。
# 高内聚边界:只读 runner 的节点链/进度/候选做渲染,无任何写面。
class_name LevelStripView
extends HBoxContainer
const COL_DONE := UiKit.GOLD_DIM # 已过——暗金
const COL_CURRENT := UiKit.GOLD # 当前——奖励金高亮
const COL_UPCOMING := UiKit.DARK_UPCOMING
const COL_CANDIDATE := UiKit.GOLD_LIT # 候选下一跳——亮金边框
const COL_BOSS_BORDER := UiKit.FLAG_RED # Boss 边框——旗朱
const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21
func _init() -> void:
alignment = BoxContainer.ALIGNMENT_CENTER
add_theme_constant_override("separation", 6)
custom_minimum_size = Vector2(0, 28) # A 区 28px 顶栏(battle.md §Layout)
func refresh(runner) -> void:
for child in get_children():
child.queue_free()
if runner == null:
return
var nodes: Array = runner.nodes()
for node in nodes:
if node.has("next"):
refresh_graph(runner)
return
_refresh_chain(runner, nodes)
# 单链形态(退化观感 = 全链横条——第一关现状渲染)
func _refresh_chain(runner, nodes: Array) -> void:
for i in nodes.size():
var node: Dictionary = nodes[i]
var type := String(node.get("type", ""))
add_child(_node_chip(type, _node_color(i, runner), false))
if i < nodes.size() - 1:
add_child(_arrow())
# 图形态:行进序列 → 当前 → 可选下一跳(编号显示经 runner 编号空间读面)
func refresh_graph(runner) -> void:
var visited: Array = runner.visited
var cands: Array = runner.next_candidates()
for i in visited.size():
var node: Dictionary = visited[i]
var is_current: bool = i == visited.size() - 1 and String(runner.level_result) == ""
add_child(_node_chip(String(node.get("type", "")), COL_CURRENT if is_current else COL_DONE, false,
runner.node_id_of(node)))
add_child(_arrow())
for j in cands.size():
var cand: Dictionary = cands[j]
add_child(_node_chip(String(cand.get("type", "")), COL_UPCOMING, true, runner.node_id_of(cand)))
if j < cands.size() - 1:
add_child(_arrow())
func _arrow() -> Label:
var arrow := UiKit.label("→", 13, false)
arrow.add_theme_color_override("font_color", UiKit.STONE)
return arrow
func _node_color(i: int, runner) -> Color:
var finished: bool = i < int(runner.node_index) or String(runner.level_result) != ""
var is_current: bool = i == int(runner.node_index) and String(runner.level_result) == ""
if is_current:
return COL_CURRENT
if finished:
return COL_DONE
return COL_UPCOMING
func _node_chip(type: String, col: Color, candidate_border: bool, node_id: int = -1) -> PanelContainer:
var p := PanelContainer.new()
var border := col
var border_w := 0
if type == "boss":
border = COL_BOSS_BORDER
border_w = 2
elif candidate_border:
border = COL_CANDIDATE
border_w = 2
var sb := UiKit.panel_flat(col, border, 4, border_w, Vector4(9, 2, 9, 2))
p.add_theme_stylebox_override("panel", sb)
var text := String(NODE_LABELS.get(type, "?"))
if node_id >= 0:
text += str(node_id)
var l := UiKit.label(text, 15, true)
l.add_theme_color_override("font_color", UiKit.INK_TITLE if col != COL_UPCOMING else UiKit.DARK_TEXT_FAINT)
p.add_child(l)
return p
@@ -1 +0,0 @@
uid://mv5cxybk4mf0
@@ -67,6 +67,9 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
# 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle)
# 停点前移批 2026-09-23:入口停点(node_index 尚 -1 = 进节点前无 current_node)也经本分支驱动——
# 读 next_candidate_ids 候选(入口单候选)→ pick_next_node,同一 _enter_node 与旧自动进入逐位
# 等价(零 RNG 漂移);入口 pick 不计 st.choices(单候选强制选路,计数口径与旧测量面逐位同构)。
static func _drive_level(runner, st: Dictionary, logger, policy: String, route: String) -> void:
st["policy"] = policy
var guard_battles := 20
@@ -74,8 +77,9 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
while st.result == "" and guard_battles > 0 and guard_actions > 0:
guard_actions -= 1
if runner.awaiting_choice():
# 选路态(兑付确认/战斗胜利后的零自动推进停点)——route 策略显式选下一跳
st.choices = int(st.choices) + 1
# 选路态(入口停点 / 兑付确认 / 战斗胜利后的零自动推进停点)——route 策略显式选下一跳
if runner.node_index >= 0:
st.choices = int(st.choices) + 1
runner.pick_next_node(_route_pick(runner, route))
continue
guard_battles -= 1
@@ -120,14 +120,28 @@ func _smoke_adventure_table() -> void:
check(ks >= 0 and ks <= 999, "keystone_per_level ∈ [0,999](%d)" % ks)
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 放弃两态)
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
func _smoke_adventure_api() -> void:
print("--- 冒险结构 API 闸(重复开始拒绝 · 放弃两态受理)---")
print("--- 冒险结构 API 闸(重复开始拒绝 · 入口停点 · 放弃两态受理)---")
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42))
adr.set_hero_override(900, 400) # 首战速胜档(放弃的非战斗受理要走到选路态)
# 停点前移批 2026-09-23:入口停点观测(信号序 + 入口候选快照——容器写回规避 lambda 按值捕获)
var sig_seq: Array = []
var obs := {"entry_cands": []}
adr.level_started.connect(_api_mark_started.bind(sig_seq))
adr.choices_presented.connect(_api_mark_choices.bind(sig_seq, obs))
adr.node_entered.connect(_api_mark_node.bind(sig_seq))
check(adr.start_adventure(42), "冒险启动受理(主种子注入恰一次——state=running)")
check(not adr.start_adventure(43), "进行中重复 start_adventure 拒绝(重开须待结算完毕)")
check(adr.runner().current_battle() != null, "首战在飞(接口①开始关卡直达战斗节点)")
check(sig_seq.size() == 2 and String(sig_seq[0]) == "level_started" and String(sig_seq[1]) == "choices_presented",
"停点前移次序契约:level_started 先于入口候选呈现(反序选路口会被呈现层 runner 空防御丢弃)")
check(adr.runner().awaiting_choice() and (obs["entry_cands"] as Array).size() == 1 \
and adr.runner().current_battle() == null,
"入口停点:选路态 + 入口单候选 + 首战未开(呈现零 RNG 消耗——pick 前零战斗域)")
check(adr.runner().pick_next_node(int(adr.runner().next_candidate_ids()[0])),
"pick 入口候选受理(同一 _enter_node——与旧自动进入逐位等价)")
check(sig_seq.size() == 3 and String(sig_seq[2]) == "node_entered" and adr.runner().current_battle() != null,
"pick 后 node_entered 到达 + 首战在飞(接口①开始关卡——停点闭合直达战斗节点)")
check(not adr.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)")
# 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理
var autoplay = load("res://smoke/level_autoplay.gd")
@@ -146,6 +160,20 @@ func _smoke_adventure_api() -> void:
check(not adr.abandon(), "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)")
# 停点前移批 2026-09-23 观测桩(信号序采集——bind 容器写回,签名按各信号实参定制)
func _api_mark_started(_li: int, _dn: String, seq: Array) -> void:
seq.append("level_started")
func _api_mark_choices(cands: Array, seq: Array, obs: Dictionary) -> void:
seq.append("choices_presented")
obs["entry_cands"] = cands.duplicate()
func _api_mark_node(_node: Dictionary, _info: Dictionary, seq: Array) -> void:
seq.append("node_entered")
# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配通关/种子复现/大奖励/败北线/
# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃)
func _smoke_adventure() -> void:
@@ -244,6 +272,7 @@ func _smoke_adventure() -> void:
var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7))
adr6.set_hero_override(900, 400)
check(adr6.start_adventure(7), "重复拒绝场景:冒险启动(seed 7)")
adr6.runner().pick_next_node(int(adr6.runner().next_candidate_ids()[0])) # 停点前移批 2026-09-23:入口停点 pick 进首战
var b6 = adr6.runner().current_battle()
var st6 := _adv_st()
var g6 := 30
@@ -314,7 +343,9 @@ func _smoke_adventure() -> void:
# ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算)
var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15))
a10.set_hero_override(900, 400)
check(a10.start_adventure(15), "放弃场景:冒险启动(seed 15——首战在飞)")
check(a10.start_adventure(15), "放弃场景:冒险启动(seed 15)")
check(a10.runner().pick_next_node(int(a10.runner().next_candidate_ids()[0])),
"放弃场景:入口停点 pick 进首战(停点前移批 2026-09-23 驱动口径)")
check(not a10.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)")
var b10 = a10.runner().current_battle()
var st10 := _adv_st()
@@ -87,6 +87,13 @@ func _ready() -> void:
main.tune_panel.set_sliders(400, 200) # 高加成保 L1 可通关——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
var t1 := Time.get_ticks_msec()
main._start_adventure_from_menu()
await get_tree().process_frame
# 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战
#(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装)
if main.route_overlay.visible:
main.route_overlay.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
var t_entry := Time.get_ticks_msec() - t1
# 性能 AC:进入战斗屏到首格可点 ≤500ms(棋盘 install 完成口径);
# 场景实例化冷启动总时长(含全部脚本首载编译——引擎启动域,非 AC 口径)另列观测
@@ -2,8 +2,8 @@
# 实现 GDD:design/ux/battle.md(场景级 headless 探针——四区布局实例化断言;entry 17②③)。
# Date: 2026-09-12(战斗屏改造批扩展;基础 = 2026-09-11 首战棋盘探针——既有断言全保留)
# 项目运行模式(autoload 在位)实例化 main.tscn,直接驱动 _start_adventure_from_menu()(完整冒险——唯一试玩入口),
# 断言四区真实构建(棋盘/进度条/主角站桩/敌方站桩/AP 点阵/明细带/数据框;E 区道具面板已删除;
# 2026-09-20 全站桩拍板——英雄卡/敌人卡断言随站桩改造批换装)。
# 断言真实构建(棋盘/主角站桩/敌方站桩/AP 点阵/明细带/数据框;A 区进度条断言随 停点前移批 2026-09-23 退役;
# E 区道具面板已删除;2026-09-20 全站桩拍板——英雄卡/敌人卡断言随站桩改造批换装)。
# 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn
extends Node
@@ -61,6 +61,17 @@ func _ready() -> void:
main._start_adventure_from_menu()
await get_tree().process_frame
await get_tree().process_frame
# ---- 入口选路停点(停点前移批 2026-09-23:关卡开始即出节点图——level_started 后、首战前弹选路层;
# 真实按钮路径 pick 入口单候选进首战)----
if not main.route_overlay.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)
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:
problems.append("冒险模式放弃按钮未开放(选路停点入口在案)")
@@ -69,13 +80,11 @@ func _ready() -> void:
await get_tree().process_frame
if main.adventure == null or String(main.adventure.state) != "running":
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
# ---- 既有断言(回归基线)----
# ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面选路覆盖层承载)----
if main.board_view._cells.is_empty():
problems.append("棋盘格未生成(cells=0)")
if main.board_view.board() == null:
problems.append("board 未装载")
if main.strip_view.get_child_count() < 2 * main.runner.nodes().size() - 1:
problems.append("节点进度条未构建(%d 子节点)" % main.strip_view.get_child_count())
if main.info_panel.hp_text().begins_with("HP —") or main.info_panel.hp_text().contains("—"):
problems.append("C 区血量未显示(%s)" % main.info_panel.hp_text())
if main.settlement == null or main.settlement.battle_result != "":
@@ -183,9 +192,9 @@ func _ready() -> void:
main.board_view.set_desaturated(true)
main.board_view.set_desaturated(false)
if problems.is_empty():
print("[ui_probe] PASS — cells=%d board=%dx%d strip=%d hp='%s' enemies=%d ap_dots=%d" % [
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
main.board_view._cells.size(), main.board_view.board().width, main.board_view.board().height,
main.strip_view.get_child_count(), main.info_panel.hp_text(),
main.info_panel.hp_text(),
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
get_tree().quit(0)
else:
@@ -64,6 +64,11 @@ func _ready() -> void:
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
await get_tree().process_frame
await get_tree().process_frame
# 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照
if main.route_overlay.visible:
main.route_overlay.press_candidate(0)
await get_tree().process_frame
await get_tree().process_frame
var hero: Dictionary = main.settlement.presentation_snapshot().hero
if int(hero.get("atk", -1)) != boosted_atk or int(hero.get("max_hp", -1)) != boosted_hp:
problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 %d/%d)" % [