iter8-T2/T3/T4 卡②③④:继续上局按钮(有断点亮起+文本带关卡序号+真实信号路径续玩)+新冒险互斥二次确认(首击换面3s回落二击提交+提交即弃旧断点)+T3 三结局清断点(_settle 统一收口)+最小设置面(SettingsStore 独立存储 user://settings.cfg 三节+SettingsOverlay 音量 master bus 实时/全屏/减动效转正/退出游戏+DS_DEBUG 调试门控+调整板旧开关退役)——探针隔离补全(click/input/layout/ui_probe/ui_tune 五探针 main.save_manager 隔离+首次布尔置位,真实档探针污染事故两起已清〔教训:断点写盘面随 save_manager 引用走,probe 换 meta 不换 save_manager 即漏〕)——闸:冒烟 593/0+五探针全绿+click 断点段问题=0——代拍板:互斥确认文案/设置面排布/确认窗口 3s(追认清单呈报)+设置与本地化 v0.2 随批

This commit is contained in:
DKChan committed 2026-09-29 19:25:19 +08:00
1 parent 04ef6a54ee
commit d599743433
15 files changed
+452 -21

No files matched your search

@@ -93,7 +93,7 @@ MVP 不要求本系统的闭环:[不做清单](../../05-不做清单.md)把设
| 优先级 | 内容 | 后置原因 |
|---|---|---|
| P1 | 已承诺设置项落位:减动效开关、音量控制入口 | 上游已承诺,随打磨期先落这两项 |
| ~~P1~~ | ~~已承诺设置项落位:减动效开关、音量控制入口~~ | **已兑现**(2026-09-29 迭代 8 卡④最小设置面:音量 master bus 实时+持久、减动效开关转正〔测试调整板旧开关退役,设置面唯一入口〕、全屏切换、退出游戏;载体 `presentation/settings_store.gd` 独立存储 + `views/settings_overlay.gd`;键位映射/多语言仍按后置表排期) |
| P1 | 多语言生产流程与首批语言清单 | 生产流程未定:文案怎么管理、谁负责校对 |
| P2 | 设置项全集(画面 / 键位 / 可访问性 / 语言各类别) | 全集无来源,待设计问题清单裁决后展开 |
| P2 | 结算风味词输入的承接 | 归档只登记了存在,各归各的时机 |
@@ -129,3 +129,4 @@ MVP 不要求本系统的闭环:[不做清单](../../05-不做清单.md)把设
| 版本 | 日期 | 改动 |
|---|---|---|
| v0.1 | 2026-09-15 | 自归档线索整合为范围草案(旧体系未单独成文) |
| v0.2 | 2026-09-29 | **最小设置面落地(迭代 8 卡④)**:P1 两项(音量入口+减动效开关)随迭代 8 兑现——三件面(音量 master bus 实时生效+即改即存/全屏/减动效转正)+退出游戏入口+调试入口 DS_DEBUG 门控;独立存储落地=`user://settings.cfg`(ConfigFile 三节 audio/display/motion,与存档两分按 2026-09-19 拍板);减动效开关自测试调整板退役(设置面唯一入口,battle.md 呈现承诺不变) |
+3 -1
View File
@@ -223,7 +223,9 @@ func _start_current_level() -> bool:
Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state)
if ok:
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
if resume_state.is_empty():
# 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现;
# 中途停点(node_id>=0)走停点呈现(restore_aux_items 先于呈现=C 区库存就位)。
if resume_state.is_empty() or int(resume_state.get("node_id", -1)) < 0:
_runner.present_entry_choice()
else:
_runner.restore_aux_items(_resume_bp.get("aux_items", []) as Array)
+92 -5
View File
@@ -39,6 +39,9 @@ var _adventure_mode := false # 冒险模式标记(inter 层确认分派
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _level_active := false
var _seed_edit: LineEdit
var _lvbtn: Button # 开始冒险按钮句柄(迭代 8 卡③——有断点时互斥二次确认文案换面)
var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮)
var _lv_start_armed := false # 新冒险二次确认态(首击置位、二击提交、3s 超时回落——仿放弃确认)
var _start_overlay: Control
var _end_overlay: BattleEndOverlay
var _inter_overlay: Control
@@ -70,6 +73,8 @@ var _adventure_gp_final := 0 # 收束清零拍暂存的最终合计
var _aux_use_selection := 0 # 魔法道具选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取)
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——工坊解锁×活行交集,冒险开始组装)
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效)
var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面)
var tune_panel: TunePanel
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
@@ -91,6 +96,10 @@ func _ready() -> void:
# (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。
save_manager = SaveManagerScript.new()
save_manager.load_from_disk()
# 系统级设置(迭代 8 卡④——独立存储不随存档):启动即恢复音量/全屏/减动效引擎态
settings_store = SettingsStore.new()
settings_store.load_settings()
settings_store.apply()
# 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点
meta = MetaProgressionScript.new(db, save_manager)
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
@@ -98,6 +107,7 @@ func _ready() -> void:
_debug.battle_created.connect(_on_debug_battle_created)
_debug.battle_ready.connect(_on_debug_battle_ready)
_build_ui()
_refresh_resume_button() # 迭代 8 卡②——启动读盘后按断点在档态点亮「继续上局」
_start_overlay.visible = true
@@ -162,18 +172,26 @@ func _build_ui() -> void:
tune_panel.closed.connect(_close_tune_panel)
tune_panel.apply_requested.connect(_on_tune_apply)
tune_panel.simulate_requested.connect(_on_tune_simulate)
settings_overlay = SettingsOverlay.new(settings_store) # 迭代 8 卡④——最小设置面(音量/全屏/减动效/退出)
add_child(settings_overlay)
settings_overlay.closed.connect(_on_settings_closed)
_modals = ModalStack.new()
for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, _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, settings_overlay]:
_register_modal(m)
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
var start: Dictionary = UiAssembler.build_start_overlay(self, db)
_start_overlay = start.overlay
_seed_edit = start.seed_edit
_lvbtn = start.lvbtn
_resume_btn = start.resumebtn
start.lvbtn.pressed.connect(_start_adventure_from_menu)
start.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口
start.tunebtn.pressed.connect(_show_tune_panel)
start.dbgbtn.pressed.connect(_start_debug_levels)
start.dbgbtn.visible = OS.get_environment("DS_DEBUG") == "1" # 迭代 8 卡④——调试入口默认隐藏门控(DS_DEBUG=1 点亮)
start.campbtn.pressed.connect(_open_camp)
start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
_inter_overlay = inter.overlay
@@ -224,13 +242,60 @@ func _input_locked() -> bool:
# ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)====================
func _start_adventure_from_menu() -> void:
# 互斥二次确认(迭代 8 卡②③):有断点时开新冒险须两击——首击转确认态(文案换面+3s 超时回落,
# 仿放弃确认形态),二击才提交;提交即弃旧断点(clear+写盘——「开新局」覆盖语义对盘面即时生效)
if save_manager != null and save_manager.has_breakpoint() and not _lv_start_armed:
_lv_start_armed = true
if _lvbtn != null:
_lvbtn.text = "已有未完冒险——再点一次开新冒险(旧断点将被覆盖)"
get_tree().create_timer(3.0).timeout.connect(_reset_lv_confirm)
return
if save_manager != null:
save_manager.clear_breakpoint()
save_manager.write_to_disk()
_lv_start_armed = false
_start_overlay.visible = false
_start_adventure()
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根——ADR-0001)
func _start_adventure() -> void:
func _reset_lv_confirm() -> void:
_lv_start_armed = false
if _lvbtn != null:
_lvbtn.text = "▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)"
# 继续上局按钮可见性 = 断点在档态(迭代 8 卡②——启动读盘后与每次回开始菜单刷新;确认态随面复位)
func _refresh_resume_button() -> void:
if _resume_btn == null or save_manager == null:
return
var has: bool = save_manager.has_breakpoint()
_resume_btn.visible = has
if has:
var bp: Dictionary = save_manager.breakpoint_snapshot()
_resume_btn.text = "▶ 继续上局(第 %d 关 · 自最近停点恢复)" % maxi(1, int(bp.get("level_index", 1)))
_reset_lv_confirm()
# 继续上局(迭代 8 卡②——卡① resume_adventure 的消费入口):断点重建至最近停点
func _resume_from_menu() -> void:
if save_manager == null or not save_manager.has_breakpoint():
return
var bp: Dictionary = save_manager.breakpoint_snapshot()
_start_overlay.visible = false
_wire_adventure()
_level_active = true
level_screen.set_abandon_available(true)
_reset_run_scope()
log_view.clear()
logger.start_session(int(bp.get("master_seed", 0)))
adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct())
if not adventure.resume_adventure(bp, {"hero_override": meta.hero_override() if meta != null else {}, "aux_unlocked": _aux_unlocked_set()}):
_fatal_screen("续玩恢复失败——见输出日志")
return
# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根,ADR-0001;断点注入同口)
func _wire_adventure() -> void:
adventure = AdventureRunner.new(db, get_node("/root/RngService"))
_adventure_mode = true
adventure.step_row.connect(_on_step_row)
@@ -242,6 +307,13 @@ func _start_adventure() -> void:
adventure.big_reward_paid.connect(_on_big_reward)
adventure.adventure_finished.connect(_on_adventure_finished)
adventure.level_started.connect(_on_adventure_level_started)
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根——ADR-0001)
func _start_adventure() -> void:
_wire_adventure()
_level_active = true
level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板)
_reset_run_scope()
@@ -252,7 +324,6 @@ func _start_adventure() -> void:
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
# 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签
var first_script: bool = not save_manager.is_first_adventure_done()
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux_unlocked_set()}):
_fatal_screen("冒险启动失败——见输出日志")
return
@@ -370,6 +441,7 @@ func _on_battle_end_continue() -> void:
_adventure_mode = false
tune_panel.reset_sliders()
_start_overlay.visible = true
_refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新(收束后已清=隐藏)
_refresh_presentations()
@@ -675,9 +747,23 @@ func _open_camp() -> void:
func _on_camp_closed() -> void:
camp_overlay.visible = false
_start_overlay.visible = true
_refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新
_refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
# ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)----
func _open_settings() -> void:
_start_overlay.visible = false
settings_overlay.visible = true
func _on_settings_closed() -> void:
settings_overlay.visible = false
_start_overlay.visible = true
_refresh_resume_button()
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
func _camp_data() -> Dictionary:
var forge_rows: Array[Dictionary] = []
@@ -1037,6 +1123,7 @@ func _show_tune_panel() -> void:
func _close_tune_panel() -> void:
tune_panel.visible = false
_start_overlay.visible = true
_refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新
func _on_tune_apply(atk_pct: int, hp_pct: int) -> void:
+50
View File
@@ -0,0 +1,50 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-29(迭代 8 卡④ 最小设置面——设置与本地化.md P1 两项兑现:音量入口+减动效开关)
# 系统级设置独立存储(设置与本地化.md——2026-09-19 用户拍板:一安装一份、删游戏才消失,
# 不进存档与生涯数据两分)。三件最小面:音量(master bus)/全屏/减动效。
# 生效侧分工:入口与持久化归本类;音量对声音的生效口径归 音频.md(master bus 单点);
# 减动效对动画的分档兑现归 battle.md §Transitions(UiKit.motion_scale 既有承载,结算逐行
# 兑现=信息载体只降速不跳过)。
# 探针隔离:path 可注入(冒烟/探针禁碰真实 user://settings.cfg——同存档隔离纪律)。
class_name SettingsStore
extends RefCounted
const DEFAULT_PATH := "user://settings.cfg"
var path: String = DEFAULT_PATH
var volume_linear := 1.0 # master bus 线性 0-1(0=静音)
var fullscreen := false
var reduced_motion := false
func _init(path_override: String = "") -> void:
if path_override != "":
path = path_override
func load_settings() -> void:
var cf := ConfigFile.new()
if cf.load(path) != OK:
return # 缺文件 = 出厂默认(合法态——非损坏不兜底)
volume_linear = clampf(float(cf.get_value("audio", "volume_linear", 1.0)), 0.0, 1.0)
fullscreen = bool(cf.get_value("display", "fullscreen", false))
reduced_motion = bool(cf.get_value("motion", "reduced", false))
func save_settings() -> bool:
var cf := ConfigFile.new()
cf.set_value("audio", "volume_linear", volume_linear)
cf.set_value("display", "fullscreen", fullscreen)
cf.set_value("motion", "reduced", reduced_motion)
return cf.save(path) == OK
# 应用到引擎态(启动恢复与实时调节同一口——headless 下窗口面跳过)
func apply() -> void:
var master := AudioServer.get_bus_index("Master")
AudioServer.set_bus_mute(master, volume_linear <= 0.001)
AudioServer.set_bus_volume_db(master, linear_to_db(maxf(volume_linear, 0.001)))
UiKit.motion_scale = 0.5 if reduced_motion else 1.0
if DisplayServer.get_name() != "headless":
DisplayServer.window_set_mode(
DisplayServer.WINDOW_MODE_FULLSCREEN if fullscreen else DisplayServer.WINDOW_MODE_WINDOWED)
+1
View File
@@ -0,0 +1 @@
uid://clxn7dqtxo313
+7 -2
View File
@@ -131,6 +131,9 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
seed_row.add_child(seed_edit)
# 唯一试玩入口 = 正式「开始冒险」(三关序列);单关测试直入口(L2/L3)退役(2026-09-23 用户拍板)
var resumebtn := UiKit.button("▶ 继续上局(自最近停点恢复)") # 迭代 8 卡②——有断点才亮(可见性归组合根刷新)
resumebtn.visible = false
box.add_child(resumebtn)
var lvbtn := UiKit.button("▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)")
box.add_child(lvbtn)
var tunebtn := UiKit.button("测试用 · 角色数值调整板(模拟天赋树属性提升)")
@@ -139,6 +142,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
box.add_child(dbgbtn)
var campbtn := UiKit.button("营地 · 永久强化(钥石换永久属性——跨局成长账本)") # 元进度实现批——主菜单营地入口(双入口框架的主菜单一侧)
box.add_child(campbtn)
var settingsbtn := UiKit.button("设置(音量 / 全屏 / 减动效 / 退出游戏)") # 迭代 8 卡④——最小设置面入口
box.add_child(settingsbtn)
# 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化")
var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [
@@ -148,8 +153,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary:
box.add_child(help)
return {"overlay": overlay, "box": box, "seed_edit": seed_edit,
"lvbtn": lvbtn, "tunebtn": tunebtn, "dbgbtn": dbgbtn,
"campbtn": campbtn}
"lvbtn": lvbtn, "resumebtn": resumebtn, "tunebtn": tunebtn, "dbgbtn": dbgbtn,
"campbtn": campbtn, "settingsbtn": settingsbtn}
# 调试两层模式的层间过渡覆盖层(历史入口专用)。返回 {overlay, label, next_btn}——接线归组合根
+121
View File
@@ -0,0 +1,121 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-29(迭代 8 卡④ 最小设置面)
# 设置覆盖层(纯视图边界规范 1.3 同款):音量滑杆/全屏/减动效三件+退出游戏+返回。
# 生效与持久化全经 SettingsStore 单点(实时应用+即改即存);本类零核心触碰。
# 减动效转正承载(测试调整板旧开关退役——设置面唯一入口,卡④ AC③)。
# 探针接缝:retarget(store) = 隔离存档换注口(同 camp 换注先例);控件引用公有只读供断言。
class_name SettingsOverlay
extends Control
signal closed
var _store
var _volume: HSlider
var _volume_val: Label
var _fs_btn: CheckButton
var _motion_btn: CheckButton
func _init(store) -> void:
_store = store
_build()
_sync()
visible = false
# 探针隔离换注(冒烟/探针禁碰真实 user://settings.cfg——换注后按新 store 重挂控件态)
func retarget(store) -> void:
_store = store
_sync()
func _build() -> void:
var dim := ColorRect.new()
dim.color = UiKit.BG_OVERLAY
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
add_child(dim)
var center := CenterContainer.new()
center.set_anchors_preset(Control.PRESET_FULL_RECT)
dim.add_child(center)
var box := VBoxContainer.new()
box.custom_minimum_size = Vector2(560, 0)
box.add_theme_constant_override("separation", 8)
center.add_child(box)
var title := UiKit.label("设置", 26, true)
title.add_theme_color_override("font_color", UiKit.WARM)
box.add_child(title)
var hint := UiKit.label("系统级设置——独立存储不随存档(一安装一份,删游戏才消失)", 13, false)
hint.add_theme_color_override("font_color", UiKit.WARM_SOFT)
box.add_child(hint)
box.add_child(HSeparator.new())
# 音量(master bus 实时生效——入口归设置、生效口径归 音频.md)
var vol_row := HBoxContainer.new()
box.add_child(vol_row)
vol_row.add_child(UiKit.label("音量", 15, false))
_volume = HSlider.new()
_volume.min_value = 0
_volume.max_value = 100
_volume.step = 1
_volume.custom_minimum_size = Vector2(280, 24)
_volume.value_changed.connect(_on_volume_changed)
vol_row.add_child(_volume)
_volume_val = UiKit.label("100%", 15, true)
_volume_val.custom_minimum_size = Vector2(60, 0)
vol_row.add_child(_volume_val)
# 全屏(art-bible 1920 档缩放策略冲突条目挂起注记——现行视口恒等拉伸,全屏切换即窗口模式切换)
_fs_btn = CheckButton.new()
_fs_btn.text = "全屏"
_fs_btn.toggled.connect(_on_fullscreen_toggled)
box.add_child(_fs_btn)
# 减动效(battle.md §Transitions 承诺转正——时长减半,结算逐行兑现=信息载体不整体跳过)
_motion_btn = CheckButton.new()
_motion_btn.text = "减动效(动画时长减半——运动敏感/演示用)"
_motion_btn.toggled.connect(_on_motion_toggled)
box.add_child(_motion_btn)
box.add_child(HSeparator.new())
var quit_btn := UiKit.button("退出游戏")
quit_btn.pressed.connect(_on_quit_pressed)
box.add_child(quit_btn)
var close_btn := UiKit.button("返回")
close_btn.pressed.connect(_emit_closed)
box.add_child(close_btn)
func _sync() -> void:
_volume.set_value_no_signal(roundf(_store.volume_linear * 100.0))
_volume_val.text = "%d%%" % roundi(_store.volume_linear * 100.0)
_fs_btn.set_pressed_no_signal(_store.fullscreen)
_motion_btn.set_pressed_no_signal(_store.reduced_motion)
func _on_volume_changed(v: float) -> void:
_store.volume_linear = v / 100.0
_store.apply()
_store.save_settings()
_volume_val.text = "%d%%" % int(v)
func _on_fullscreen_toggled(on: bool) -> void:
_store.fullscreen = on
_store.apply()
_store.save_settings()
func _on_motion_toggled(on: bool) -> void:
_store.reduced_motion = on
_store.apply()
_store.save_settings()
func _on_quit_pressed() -> void:
_store.save_settings()
get_tree().quit()
func _emit_closed() -> void:
closed.emit()
@@ -0,0 +1 @@
uid://b7570rrk1jl7h
+1 -12
View File
@@ -159,14 +159,7 @@ func _build() -> void:
var reset_btn := UiKit.button("重置为 0")
reset_btn.pressed.connect(_reset_btn_pressed)
btn_row.add_child(reset_btn)
# 减动效开关(battle.md §Transitions 内建承诺——切片期入口;正式设置层归设置批,两分制)
var motion_row := HBoxContainer.new()
box.add_child(motion_row)
motion_row.add_child(UiKit.label("减动效(动画时长减半——运动敏感/演示用)", 14, false))
var motion_btn := CheckButton.new()
motion_btn.button_pressed = UiKit.motion_scale < 1.0
motion_btn.toggled.connect(_on_motion_toggled)
motion_row.add_child(motion_btn)
# 减动效开关已转正迁出(迭代 8 卡④——设置面唯一入口,本面板退役该开关;motion_scale 引擎态由设置面承载)
box.add_child(HSeparator.new())
var sim_row := HBoxContainer.new()
@@ -221,10 +214,6 @@ func _scaled(base: int, pct: int) -> int:
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
func _on_motion_toggled(on: bool) -> void:
UiKit.motion_scale = 0.5 if on else 1.0
func _emit_simulate_requested() -> void:
simulate_requested.emit(int(_sim_count.value))
+149
View File
@@ -81,10 +81,78 @@ func _ready() -> void:
await get_tree().process_frame
if main.camp_overlay.visible or not main._start_overlay.visible:
_problems.append("营地关闭后未恢复开始层")
# ---- 设置面段(迭代 8 卡④——隔离 store 换注禁碰真实 user://settings.cfg;真实按钮信号路径)----
for p0 in ["user://smoke_click_probe_settings.cfg"]:
if FileAccess.file_exists(p0):
DirAccess.remove_absolute(p0)
var iso_s = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
iso_s.load_settings()
main.settings_store = iso_s
main.settings_overlay.retarget(iso_s)
var settings_btn: Button = _find_button_by_prefix(main._start_overlay, "设置")
if settings_btn == null:
_problems.append("设置入口按钮缺失(开始层 settingsbtn)")
else:
settings_btn.pressed.emit() # 真实按钮信号路径 → _open_settings
await get_tree().process_frame
if not main.settings_overlay.visible or main._start_overlay.visible:
_problems.append("设置面未打开(点击路径)")
else:
# 减动效转正双态(卡④ AC③):开=0.5 引擎态即时生效+落盘持久;关=1.0 回常态
var motion_btn: CheckButton = _find_checkbutton_by_text(main.settings_overlay, "减动效")
if motion_btn == null:
_problems.append("设置面减动效开关缺失")
else:
motion_btn.button_pressed = true
await get_tree().process_frame
if UiKit.motion_scale != 0.5:
_problems.append("减动效开未生效(motion_scale != 0.5)")
var chk = load("res://presentation/settings_store.gd").new("user://smoke_click_probe_settings.cfg")
chk.load_settings()
if not chk.reduced_motion:
_problems.append("减动效开未持久化(设置独立存储失守——卡④ AC⑤)")
motion_btn.button_pressed = false
await get_tree().process_frame
if UiKit.motion_scale != 1.0:
_problems.append("减动效关未恢复(motion_scale != 1.0)")
# 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读
var vol: HSlider = main.settings_overlay._volume
vol.value = 30
await get_tree().process_frame
var master_idx := AudioServer.get_bus_index("Master")
var expect_db := linear_to_db(0.3)
if absf(AudioServer.get_bus_volume_db(master_idx) - expect_db) > 0.1:
_problems.append("音量滑杆未实时生效(master bus db %f ≠ %f)" % [AudioServer.get_bus_volume_db(master_idx), expect_db])
if absf(iso_s.volume_linear - 0.3) > 0.001:
_problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear)
# 调试入口默认隐藏门控(卡④ AC④——探针环境 DS_DEBUG 未设)
var dbg_btn: Button = _find_button_by_prefix(main._start_overlay, "调试")
if dbg_btn != null and dbg_btn.visible:
_problems.append("调试入口未隐藏(DS_DEBUG 门控失守——卡④ AC④)")
var settings_close: Button = _find_button_by_prefix(main.settings_overlay, "返回")
if settings_close == null:
_problems.append("设置面返回按钮缺失")
else:
settings_close.pressed.emit()
await get_tree().process_frame
if main.settings_overlay.visible or not main._start_overlay.visible:
_problems.append("设置面关闭后未恢复开始层")
# 旧入口退役断言(卡④ AC③——测试调整板不再含减动效开关)
var legacy_motion = _find_checkbutton_by_text(main.tune_panel, "减动效")
if legacy_motion != null:
_problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)")
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json")
iso_reset.load_from_disk()
iso_reset.mark_first_adventure_done() # 对齐基线(真实档已有游玩史)——非首次脚本模式=逐关抽签模板
main.save_manager = iso_reset # 迭代 8 卡①——冒险断点写盘同步隔离(禁碰真实档 user://save.json)
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset)
main._refresh_resume_button()
var resume_btn0: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局")
if resume_btn0 == null:
_problems.append("继续上局按钮缺失(开始层 resumebtn——装配器)")
elif resume_btn0.visible:
_problems.append("无断点态继续上局按钮未隐藏(卡② AC①——新档合法空态)")
main.tune_panel.set_sliders(400, 200) # 高加成保 L1 可通关——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
var t1 := Time.get_ticks_msec()
main._start_adventure_from_menu()
@@ -177,6 +245,77 @@ func _ready() -> void:
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
_run_modal_probes_once()
_play_one_action()
# ---- 断点续玩段(迭代 8 卡②③——真实按钮信号路径;存档面已隔离 iso_reset)----
# 前提注记:本探针首局常态 = L1 战败收口(问题=0 基线口径)——断点段①借战败结局验 T3 清除断点。
await get_tree().process_frame
var bp_file := "user://smoke_click_probe_reset.json"
var iso_chk = load("res://core/save_manager.gd").new(bp_file)
iso_chk.load_from_disk()
# ① 战败结局清断点(T3 口径——三结局清断点之一):停点快照随局写入,战败结算须清
iso_chk.load_from_disk()
if iso_chk.has_breakpoint():
_problems.append("断点段:战败结算后断点未清(T3 三结局清断点失守)")
if not main._end_overlay.is_open():
_problems.append("断点段:战败后未出冒险终局面")
if main._end_overlay._btn == null:
_problems.append("断点段:终局面返回按钮缺失")
else:
main._end_overlay._btn.pressed.emit() # 真实按钮信号路径(_btn=继续/返回共用句柄)
await get_tree().process_frame
if not main._start_overlay.visible:
_problems.append("断点段:终局返回未回开始菜单")
var resume_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局")
if resume_btn != null and resume_btn.visible:
_problems.append("断点段:退出清断点后继续上局按钮未隐藏(卡②/T3 AC③)")
# ② 中断→续玩:新冒险达入口停点=断点在档 → 合成回菜单(模拟关游戏重开)→ 按钮点亮 → 点击恢复至停点
var lv_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 开始冒险")
if lv_btn == null or resume_btn == null:
_problems.append("断点段:开始冒险/继续上局按钮缺失")
else:
lv_btn.pressed.emit() # 无断点态单击直启(不触发二次确认)
await get_tree().process_frame
if not main.level_screen.visible:
_problems.append("断点段:无断点开新冒险未达入口停点")
else:
iso_chk.load_from_disk()
if not iso_chk.has_breakpoint():
_problems.append("断点段:入口停点断点未写盘(卡①写面失守)")
var cand_before: Array = main.runner.next_candidate_ids()
# 合成「关游戏重开回菜单」(探针专用——真实路径无此入口;断点已在盘)
main._exit_level_screen()
main._start_overlay.visible = true
main.adventure = null
main._refresh_resume_button()
await get_tree().process_frame
if not resume_btn.visible:
_problems.append("断点段:有断点态继续上局按钮未点亮(卡② AC②前置)")
else:
resume_btn.pressed.emit() # 真实按钮信号路径 → _resume_from_menu → resume_adventure
await get_tree().process_frame
await get_tree().process_frame
if main.adventure == null or main.runner == null or not main.level_screen.visible:
_problems.append("断点段:续玩未恢复至停点(关卡画面未现)")
else:
var cand_after: Array = main.runner.next_candidate_ids()
if cand_after.size() != cand_before.size() or (cand_after.size() > 0 and int(cand_after[0]) != int(cand_before[0])):
_problems.append("断点段:续玩候选与中断前不一致(停点重建失准)")
if not iso_chk.load_from_disk() or not iso_chk.has_breakpoint():
_problems.append("断点段:续玩误清断点(只在三结局清)")
# ③ 互斥二次确认(卡②③):断点在档时开新冒险首击=确认态、二击才提交
main._exit_level_screen()
main._start_overlay.visible = true
main.adventure = null
main._refresh_resume_button()
await get_tree().process_frame
lv_btn.pressed.emit()
await get_tree().process_frame
if not main._start_overlay.visible or main.adventure != null:
_problems.append("断点段:有断点首击即开新冒险(互斥二次确认失守卡②③)")
lv_btn.pressed.emit()
await get_tree().process_frame
await get_tree().process_frame
if main.adventure == null or main._start_overlay.visible:
_problems.append("断点段:二次确认后未开新冒险")
print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [
str(_done), str(action_budget), _problems.size()])
for p in _problems:
@@ -365,3 +504,13 @@ func _find_button_by_prefix(root: Node, prefix: String) -> Button:
if found != null:
return found
return null
func _find_checkbutton_by_text(root: Node, needle: String) -> CheckButton:
if root is CheckButton and String(root.text).contains(needle):
return root
for child in root.get_children():
var found := _find_checkbutton_by_text(child, needle)
if found != null:
return found
return null
+8
View File
@@ -25,6 +25,14 @@ func _ready() -> void:
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
# 存档隔离(迭代 8 卡①②——断点写盘面随冒险挂 main.save_manager;探针禁碰真实档 user://save.json,
# 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变)
var iso = load("res://core/save_manager.gd").new("user://smoke_input_probe_save.json")
iso.load_from_disk()
iso.mark_first_adventure_done()
main.save_manager = iso
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
main._refresh_resume_button()
main._start_adventure_from_menu()
await get_tree().process_frame
# ---- ⑫ 图上命中真实输入(关卡画面实装批 2026-09-28 回归闸):入口停点候选节点中心
+7
View File
@@ -10,6 +10,13 @@ func _ready() -> void:
var main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
# 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律)
var iso = load("res://core/save_manager.gd").new("user://smoke_layout_probe_save.json")
iso.load_from_disk()
iso.mark_first_adventure_done()
main.save_manager = iso
main.meta = load("res://core/meta_progression.gd").new(main.db, iso)
main._refresh_resume_button()
main._start_adventure_from_menu()
for i in range(3):
await get_tree().process_frame
+3
View File
@@ -57,7 +57,10 @@ func _ready() -> void:
DirAccess.remove_absolute(p)
var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json")
iso_zero.load_from_disk()
iso_zero.mark_first_adventure_done() # 对齐真实档游玩史口径(模板抽签路径不变)
main.save_manager = iso_zero # 迭代 8 卡①——断点写盘面同步隔离(禁碰真实档)
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero)
main._refresh_resume_button()
main._start_adventure_from_menu()
await get_tree().process_frame
await get_tree().process_frame
+6
View File
@@ -59,6 +59,12 @@ func _ready() -> void:
problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.hero.atk), str(main.db.hero.max_hp), base_atk, base_hp])
# ⑧ 单次加成生效回归断言:应用加成 + 启动单场战斗 → 呈现快照 hero 必须是"基线 × 单次加成"
# (攻击 ×2 / 生命 +50%)——旧缺陷(残留值被当基线再乘)在 encounter 参数链上同样被抓获
# 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律)
var iso_tune = load("res://core/save_manager.gd").new("user://smoke_tune_probe_save.json")
iso_tune.load_from_disk()
iso_tune.mark_first_adventure_done()
main.save_manager = iso_tune
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_tune)
main.tune_panel.set_sliders(100, 50)
main.tune_panel.press_apply_for_probe()
main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言)
@@ -0,0 +1 @@
uid://byrnh81s2nda1