From a7f35c970f979299d0c92cd9ebaec2d8757b7123 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 04:44:17 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat(save):=20SaveManager=20=E5=AD=98?= =?UTF-8?q?=E6=A1=A3=E6=89=BF=E8=BD=BD=E8=90=BD=E5=9C=B0=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=8D=95=E6=A1=A3=20JSON=20=E8=AF=BB=E5=86=99/=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E8=BF=81=E7=A7=BB/=E6=8D=9F=E5=9D=8F=E5=85=9C?= =?UTF-8?q?=E5=BA=95+=E5=9F=9F=E8=AE=BF=E9=97=AE=E5=99=A8=EF=BC=88?= =?UTF-8?q?=E5=86=92=E7=83=9F=E6=AE=B5=E2=91=A0-=E2=91=A7=E2=91=A9=2026=20?= =?UTF-8?q?=E9=A1=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - core/save_manager.gd:契约 1-4(表档分家/域界干净/钥石不丢/高版本拒读禁写);tmp+rename 原子写; register_migration 迁移链骨架;类型化访问器 + apply_adventure_summary 原语(Boss 金额归元进度批) - smoke _smoke_save 段:往返/新档/拒花/出域拒/高版本拒读/损坏兜底/迁移链/域界/cap(437→463 全绿) - 摘要集成断言(run_adventure 驱动)待冒险结构批落 master 后补段 --- .../core/save_manager.gd | 228 ++++++++++++++++++ .../core/save_manager.gd.uid | 1 + .../smoke/smoke_all.gd | 122 ++++++++++ 3 files changed, 351 insertions(+) create mode 100644 prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd create mode 100644 prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd.uid diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd b/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd new file mode 100644 index 00000000..59775b4e --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd @@ -0,0 +1,228 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Validation Question: 关掉游戏之后玩家的东西还在吗——单档 JSON 存档的写入、读回、版本迁移与损坏 +# 兜底能否在零玩法依赖下独立成立,元进度四件(钥石余额/永久强化等级表/每关通关次数表/Boss 首杀标记位) +# 与最近冒险摘要(cap 10)能否跨进程存活(P0) +# Date: 2026-09-21(存档序列化批——设计权威 design/gdd/systems/表现与支撑/存档序列化.md) +# +# 存档技术契约承载方(Core 层零 UI、零玩法依赖——禁 preload/import 任何玩法脚本、禁碰 ConfigDB/ +# RngService,可独立实例化独立验证〔存档序列化.md 验收标准〕)。 +# +# 域清单与 owner 分工(内容 owner 定口径,本类照单承载、不替任何人定义进度): +# keystone_balance / forge_levels / item_unlocks / level_clear_counts / boss_first_kill +# —— 内容 owner = 元进度.md(营地三柜台:钥石余额/属性锻造/道具工坊/通关次数/首杀标记) +# recent_summaries(冒险摘要五件事副本)—— 内容 owner = 冒险结构.md(摘要五件事口径) +# 承载 owner = 存档序列化.md(格式/时机/迁移/损坏/域登记) +# +# 契约登记(存档序列化.md 核心规则——违反即返工): +# 契约 1 表与档分家:存档只记进度事实,禁止把数值配方写进存档(数值改版后旧档按加载时的表解释); +# 契约 2 域界干净:行动点/RAP/道具库存/生命账本/GP 不出现在存档内容里(反面清单,冒烟有断言); +# 契约 3 钥石不丢:写入/读回/迁移不得破坏元进度「钥石只增不减(消费除外)」判据—— +# tmp+rename 原子写(写一半断电不留烂档)+ 损坏兜底坏档备份; +# 契约 4 高版本拒读:version > SAVE_FORMAT_VERSION → 不加载且禁写盘(保护未来版本的档不被覆写), +# 内存以默认新档运行。 +# +# JSON 键纪律:字典键一律 String——JSON 键非字符串的坑,int 键(关卡序号)读写两侧都要 str() 换算, +# 混用必出静默 miss。 +class_name SaveManager +extends RefCounted + +const SAVE_FORMAT_VERSION := 1 # registry schema_version:MVP=1(owner=数据配置;高版本拒读 owner=存档序列化) +const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) + +const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台) + +var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径) +var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写) +var _data: Dictionary = {} # 内存数据模型(_init 即内置默认;load 后为档案内容) +var _migrations: Dictionary = {} # from_version → Callable(data)->data(升一版;生产链现为空——骨架先行) + + +func _init(path: String = "user://save.json") -> void: + _path = path + _data = _default_data() + + +# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)---- + +func _default_data() -> Dictionary: + return { + "version": SAVE_FORMAT_VERSION, + "meta": { + "keystone_balance": 0, + "forge_levels": {"max_hp": 0, "atk": 0, "armor_class": 0}, + "item_unlocks": {}, + "level_clear_counts": {"1": 0, "2": 0, "3": 0}, + "boss_first_kill": {}, + "recent_summaries": [], + }, + } + + +# ---- 读盘(启动时调用)---- +# 返回 true = 已就绪可用(含新档/损坏兜底/迁移成功);false = 高版本拒读(默认数据运行、禁写盘)。 +# 损坏兜底:原文件改名 .corrupt.bak(已存在则覆盖)+ push_warning + 默认数据继续——不静默丢档。 +func load_from_disk() -> bool: + _read_only = false + if not FileAccess.file_exists(_path): + _data = _default_data() # 新档合法态 + return true + var f := FileAccess.open(_path, FileAccess.READ) + if f == null: + return _corrupt_fallback("读打开失败(%s)" % error_string(FileAccess.get_open_error())) + var text := f.get_as_text() + f.close() + var parsed = JSON.parse_string(text) + if typeof(parsed) != TYPE_DICTIONARY: + return _corrupt_fallback("JSON 解析失败或顶层结构出域(非对象)") + if not parsed.has("version"): + return _corrupt_fallback("缺版本字段") + var v := int(parsed["version"]) + if v > SAVE_FORMAT_VERSION: + _read_only = true + _data = _default_data() + push_error("[存档序列化] 档案版本 %d 高于当前 %d——拒读禁写(保护未来档不被覆写,内存默认新档运行)" % [v, SAVE_FORMAT_VERSION]) + return false + while v < SAVE_FORMAT_VERSION: + if not _migrations.has(v): + return _corrupt_fallback("迁移链缺步骤 %d→%d" % [v, v + 1]) + var stepped = _migrations[v].call(parsed) + if typeof(stepped) != TYPE_DICTIONARY: + return _corrupt_fallback("迁移步骤 %d→%d 执行失败" % [v, v + 1]) + parsed = stepped + v = int(parsed.get("version", v + 1)) + _data = _normalize(parsed) + return true + + +# ---- 写盘(tmp + rename 原子写——写一半断电不留烂档)---- +# 高版本拒读态一律拒绝(契约 4 禁写);写失败返回 false。 +func write_to_disk() -> bool: + if _read_only: + push_error("[存档序列化] 写盘拒绝——档案版本高于当前(拒读禁写态)") + return false + var tmp := _path + ".tmp" + var f := FileAccess.open(tmp, FileAccess.WRITE) + if f == null: + push_error("[存档序列化] 临时文件打开失败:%s(%s)" % [tmp, error_string(FileAccess.get_open_error())]) + return false + f.store_string(JSON.stringify(_data, "\t", true)) + f.close() + if not _rename_overwrite(tmp, _path): + push_error("[存档序列化] 原子改名失败(tmp 保留待查):%s → %s" % [tmp, _path]) + return false + return true + + +# 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约: +# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移当前为空(v1 首版), +# 冒烟经本口验证链式升级。 +func register_migration(from_version: int, fn: Callable) -> void: + _migrations[from_version] = fn + + +# ---- 类型化域访问器(消费面——只经访问器操作,禁碰内部结构)---- + +func keystone_balance() -> int: + return int(_data["meta"]["keystone_balance"]) + + +func add_keystone(n: int) -> void: + _data["meta"]["keystone_balance"] = keystone_balance() + n + + +# 钥石消费唯一口:余额不足拒绝且零副作用(钥石只增不减——消费除外,元进度验收判据) +func spend_keystone(n: int) -> bool: + if keystone_balance() < n: + return false + _data["meta"]["keystone_balance"] = keystone_balance() - n + return true + + +func forge_level(attr: String) -> int: + return int(_data["meta"]["forge_levels"].get(attr, 0)) + + +func set_forge_level(attr: String, n: int) -> bool: + if not FORGE_ATTRS.has(attr): + push_error("[存档序列化] 属性锻造键出域拒绝:%s(合法键 %s)" % [attr, str(FORGE_ATTRS)]) + return false + _data["meta"]["forge_levels"][attr] = n + return true + + +func item_unlocked(item_id: String) -> bool: + return _data["meta"]["item_unlocks"].has(item_id) + + +func unlock_item(item_id: String) -> void: + _data["meta"]["item_unlocks"][item_id] = true + + +func clear_count(level_index: int) -> int: + return int(_data["meta"]["level_clear_counts"].get(str(level_index), 0)) + + +func boss_first_kill_marked(level_index: int) -> bool: + return _data["meta"]["boss_first_kill"].has(str(level_index)) + + +func recent_summaries() -> Array: + return _data["meta"]["recent_summaries"] + + +# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构): +# 钥石入账 + 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入(cap 10,最新在前)。 +# 本批不含 Boss 金额(首杀全量/重复杀小额归元进度批——届时增第二参 boss_drops,默认 [] 零行为兼容)。 +func apply_adventure_summary(summary: Dictionary) -> void: + add_keystone(int(summary.get("keystone_total", 0))) + var cleared := int(summary.get("levels_cleared", 0)) + for li in range(1, cleared + 1): + var key := str(li) + _data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1 + _data["meta"]["boss_first_kill"][key] = true + var arr: Array = _data["meta"]["recent_summaries"] + arr.push_front(summary.duplicate(true)) + while arr.size() > SUMMARY_CAP: + arr.pop_back() + + +# ---- 私有:损坏兜底与文件原语 ---- + +# 损坏兜底:坏档改名 .corrupt.bak(已存在则覆盖)→ 默认数据继续。不静默丢档(警告留痕)、不 crash。 +func _corrupt_fallback(reason: String) -> bool: + var bak := _path + ".corrupt.bak" + if not _rename_overwrite(_path, bak): + push_warning("[存档序列化] 坏档备份改名失败(原文件保留原地):%s——%s" % [bak, reason]) + else: + push_warning("[存档序列化] 检测到损坏档案,已备份为 %s——%s(用默认新档继续)" % [bak, reason]) + _data = _default_data() + return true + + +# 迁移后/低版本档字段补齐(改版不弃档:缺新域按默认补齐,既有值保留) +func _normalize(data: Dictionary) -> Dictionary: + var merged := _default_data() + for key in data: + merged[key] = data[key] + var meta: Dictionary = merged.get("meta", {}) + var def_meta: Dictionary = _default_data()["meta"] + for key in def_meta: + if not meta.has(key): + meta[key] = def_meta[key] + var forge: Dictionary = meta.get("forge_levels", {}) + for sk in def_meta["forge_levels"]: + if not forge.has(sk): + forge[sk] = def_meta["forge_levels"][sk] + meta["forge_levels"] = forge + merged["meta"] = meta + return merged + + +# 覆盖式改名(Windows 原生 rename 目标已存在时会失败——先删后改,显式覆盖语义) +func _rename_overwrite(from: String, to: String) -> bool: + var dir := DirAccess.open(from.get_base_dir()) + if dir == null: + return false + if dir.file_exists(to.get_file()) and dir.remove(to.get_file()) != OK: + return false + return dir.rename(from.get_file(), to.get_file()) == OK diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd.uid new file mode 100644 index 00000000..13ed8b75 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/core/save_manager.gd.uid @@ -0,0 +1 @@ +uid://ctnurntr6p37h diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index b1162fe7..eec345eb 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -98,6 +98,7 @@ func _run_core_smoke(db, rng) -> void: _smoke_shop_room() _smoke_shop_graph() _smoke_nine_types_gp_stream() + _smoke_save() # ---- 战斗屏棋盘布局·格边长公式回归(2026-09-19 真人试玩 bug⑥:L2 12×12 板下缘被裁约 1/6 格高—— @@ -2860,3 +2861,124 @@ func _smoke_battle_log_view() -> void: var before: int = view.parsed_text().length() view.append_row(rest[row_type]) check(view.parsed_text().length() > before, "行型 %s 格式化落地(解析文本增长)" % String(row_type)) + + +# ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/ +# 损坏兜底/迁移链/域界干净/摘要 cap)。测试文件统一 user://smoke_save_*.json 隔离,段首清场+ +# 段尾清理——绝不碰 user://save.json(红线)。摘要集成断言(run_adventure 驱动,B1 冒险结构批产物) +# 待其落 master 后补段。 +func _smoke_save() -> void: + print("--- SaveManager 存档读写/迁移/兜底 ---") + var sm_script = load("res://core/save_manager.gd") + var paths := ["user://smoke_save_roundtrip.json", "user://smoke_save_fresh.json", + "user://smoke_save_spend.json", "user://smoke_save_highver.json", + "user://smoke_save_corrupt.json", "user://smoke_save_migrate.json", + "user://smoke_save_domain.json", "user://smoke_save_cap.json"] + var stale: Array = [] + for p in paths: + stale.append(p) + stale.append(p + ".tmp") + stale.append(p + ".corrupt.bak") + for p in stale: + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) + + # ① 往返:全访问器写非默认值 → write → 新实例 load → 逐域逐值相等 + var w = sm_script.new(paths[0]) + w.add_keystone(7) + check(w.set_forge_level("atk", 2) and w.set_forge_level("max_hp", 1) and w.set_forge_level("armor_class", 1), + "属性锻造三档写入") + w.unlock_item("probe_compass") + w.apply_adventure_summary({"result": "failed", "cleared": false, "levels_cleared": 2, + "stopped_at": 3, "keystone_total": 3, "master_seed": 99}) + check(w.keystone_balance() == 10 and w.clear_count(1) == 1 and w.clear_count(2) == 1 and w.clear_count(3) == 0, + "apply 原语入账(钥石 7+3、通关次数 1/1/0)") + check(w.boss_first_kill_marked(1) and w.boss_first_kill_marked(2) and not w.boss_first_kill_marked(3), + "首杀标记随通关关卡置位(1/2 有、3 无)") + check(w.write_to_disk(), "写盘成功") + var r = sm_script.new(paths[0]) + check(r.load_from_disk(), "读回成功") + check(r.keystone_balance() == 10, "钥石余额往返相等(10)") + check(r.forge_level("atk") == 2 and r.forge_level("max_hp") == 1 and r.forge_level("armor_class") == 1, + "锻造三档往返相等") + check(r.item_unlocked("probe_compass") and not r.item_unlocked("rope"), "道具解锁往返相等") + check(r.clear_count(1) == 1 and r.clear_count(2) == 1 and r.clear_count(3) == 0, "通关次数表往返相等") + check(r.boss_first_kill_marked(1) and r.boss_first_kill_marked(2) and not r.boss_first_kill_marked(3), + "首杀标记往返相等") + var s0: Dictionary = r.recent_summaries()[0] + check(r.recent_summaries().size() == 1 and int(s0.get("master_seed", -1)) == 99 and int(s0.get("keystone_total", -1)) == 3, + "冒险摘要往返相等(1 条,seed 99)") + + # ② 新档合法:不存在路径 load → true + 全零默认值 + var f2 = sm_script.new(paths[1]) + check(f2.load_from_disk() and f2.keystone_balance() == 0 and f2.forge_level("atk") == 0 + and not f2.item_unlocked("anything") and f2.clear_count(1) == 0 and f2.recent_summaries().is_empty(), + "新档合法态(load true + 全零默认)") + + # ③ spend 拒绝:余额不足 false 且零副作用(钥石只增不减——消费除外) + var f3 = sm_script.new(paths[2]) + f3.add_keystone(5) + check(not f3.spend_keystone(8) and f3.keystone_balance() == 5, "余额 5 花 8 拒绝且余额不动") + check(f3.spend_keystone(3) and f3.keystone_balance() == 2, "余额内消费成功(5→2)") + + # ④ attr 出域拒绝(push_error,值不变) + check(not f3.set_forge_level("luck", 1) and f3.forge_level("luck") == 0 and f3.forge_level("atk") == 0, + "锻造键出域拒绝(值不变)") + + # ⑤ 高版本拒读:load false、原文件字节不动、后续写盘拒绝且文件仍字节不动(契约 4) + var hv_text := "{\"version\": 99, \"meta\": {\"keystone_balance\": 1}}" + var fhv := FileAccess.open(paths[3], FileAccess.WRITE) + fhv.store_string(hv_text) + fhv.close() + var f5 = sm_script.new(paths[3]) + check(not f5.load_from_disk(), "高版本拒读(load false)") + check(FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text, "拒读后原文件字节不动") + check(not f5.write_to_disk() and FileAccess.open(paths[3], FileAccess.READ).get_as_text() == hv_text, + "拒读态写盘拒绝且文件仍字节不动") + + # ⑥ 损坏兜底:非 JSON 字节 → 默认数据 + .corrupt.bak(不静默丢档) + var junk := "not json {{{" + var fj := FileAccess.open(paths[4], FileAccess.WRITE) + fj.store_string(junk) + fj.close() + var f6 = sm_script.new(paths[4]) + check(f6.load_from_disk() and f6.keystone_balance() == 0 and f6.recent_summaries().is_empty(), + "损坏档兜底(默认数据继续)") + check(FileAccess.file_exists(paths[4] + ".corrupt.bak"), "坏档备份 .corrupt.bak 存在") + check(FileAccess.open(paths[4] + ".corrupt.bak", FileAccess.READ).get_as_text() == junk, "备份内容 = 原坏档字节") + + # ⑦ 迁移链:register_migration 测试钩 → 手工 v0 旧结构档 → 链式升级 v1 + var mig := func(d: Dictionary) -> Dictionary: + return {"version": 1, "meta": {"keystone_balance": int(d.get("keystone_old", 0))}} + var f7 = sm_script.new(paths[5]) + f7.register_migration(0, mig) + var fm := FileAccess.open(paths[5], FileAccess.WRITE) + fm.store_string(JSON.stringify({"version": 0, "keystone_old": 12})) + fm.close() + check(f7.load_from_disk() and f7.keystone_balance() == 12, "v0 旧档经迁移链入账(keystone_old 12 → 余额 12)") + check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 1, + "迁移后写盘 version == 1") + + # ⑧ 域界干净:盘上文本不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2) + var disk_text := FileAccess.open(paths[0], FileAccess.READ).get_as_text() + var banned := ["\"ap\"", "\"rap\"", "\"gp\"", "\"inventory\"", "\"hero_hp\""] + var leaked := [] + for b in banned: + if disk_text.contains(b): + leaked.append(b) + check(leaked.is_empty(), "域界干净——盘上无局内资源键(发现 %s)" % str(leaked)) + + # ⑩ 摘要 cap:12 份 → 保留 10、最新在前 + var f10 = sm_script.new(paths[7]) + for i in range(12): + f10.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 0, + "stopped_at": 0, "keystone_total": 0, "master_seed": i}) + var arr: Array = f10.recent_summaries() + check(arr.size() == 10, "摘要 cap 10(12 份 → 10 条)") + check(int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2, + "最新在前(头 seed 11 / 尾 seed 2)") + + # 段尾清理(测试文件绝不残留) + for p in stale: + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) From 569d6347ab4ece64bf1c0a1ee44951ef284184a4 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 04:44:41 +0800 Subject: [PATCH 2/9] =?UTF-8?q?docs(save):=20=E5=AD=98=E6=A1=A3=E5=BA=8F?= =?UTF-8?q?=E5=88=97=E5=8C=96=20GDD=20=E5=8D=87=20v1.0=20=E5=AE=9A?= =?UTF-8?q?=E7=A8=BF+registry=20save=5Ffile=20=E7=99=BB=E8=AE=B0+06=20?= =?UTF-8?q?=E6=9D=A1=E7=9B=AE=E8=A7=A3=E5=86=B3=E7=A7=BB=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 存档序列化.md v1.0:格式/数据模型/读写时机/版本迁移/损坏兜底五项裁决成文,待设计六问全部处置 - 02-系统清单 状态列同步已定稿;元进度.md 存档义务指针改指 registry save_file(悬空引用清偿) - registry save_file 新条目(domains 逐域含内容 owner)+ schema_version 落地注记 - 06「存档契约条目的登记方式」解决移除(登记口径=registry 逐域登记);roadmap 时间线 +1 --- design/gdd/02-系统清单.md | 2 +- design/gdd/06-待办清单.md | 10 -- design/gdd/systems/冒险循环/元进度.md | 2 +- design/gdd/systems/表现与支撑/存档序列化.md | 128 +++++++++++++------- design/registry/entities.yaml | 17 ++- production/roadmap.md | 1 + 6 files changed, 102 insertions(+), 58 deletions(-) diff --git a/design/gdd/02-系统清单.md b/design/gdd/02-系统清单.md index 20ad7640..9942003a 100644 --- a/design/gdd/02-系统清单.md +++ b/design/gdd/02-系统清单.md @@ -52,7 +52,7 @@ |---|---|---| | `HUD框架.md` | 界面布局与呈现层契约 | 已定稿·自旧体系迁移 | | `数据配置.md` | 数值表与单一事实源管线 | 已定稿·自旧体系迁移 | -| `存档序列化.md` | 存档契约与版本迁移 | 范围草案·待设计 | +| `存档序列化.md` | 存档契约与版本迁移 | 已定稿 | | `音频.md` | 音乐音效方向与实现 | 范围草案·待设计 | | `教学引导.md` | 新手教学与提示 | 范围草案·待设计 | | `设置与本地化.md` | 设置项与多语言 | 范围草案·待设计 | diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 18375fdb..1b1044dd 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -337,16 +337,6 @@ **归属**:冒险结构定稿批(随批建账) **状态**:待办(仅剩冒险表半边) -### 存档序列化 - -#### 存档契约条目的登记方式 - -**现状**:内容归生产方、承载归存档序列化的方向已定,但"存档里每个域由谁登记、登记在哪"没有成文——归档曾把存档结构整体记在元进度名下,现行元进度文档又把读写与迁移指给存档序列化;域级契约的持有方式随存档序列化设计批与元进度批联合裁决 - -**归属**:存档序列化与元进度联合裁决 - -**状态**:待办 - ### 教学引导 diff --git a/design/gdd/systems/冒险循环/元进度.md b/design/gdd/systems/冒险循环/元进度.md index a9578c50..08460f84 100644 --- a/design/gdd/systems/冒险循环/元进度.md +++ b/design/gdd/systems/冒险循环/元进度.md @@ -66,7 +66,7 @@ **投值语言锁定(2026-09-18 数值语言拍板)**:三柜台属性强化一律 **flat 整数加减**("每级 +1 基础攻击"式,而非"+50% 攻击"式)——避开百分比词条被数值成长稀释的先例陷阱、守住数字可读性锚;具体每级数值仍归元进度设计批投值。 -三条柜台纪律:**定价曲线一律几何递增**(每级费用按公比上浮——原则已拍板,起步价与公比数字归设计批配读数),与产出逻辑配套:深层关钥石产出多、后级强化贵,逼玩家往深处走。**道具工坊首版只挂通关次数渠道**是排期裁决(定价体系未建时的零定价依赖落地形态),不是终态。**存档义务随柜台产生**:钥石余额、永久强化等级表、每关通关次数表、Boss 首杀标记位——域级契约登记方式见[存档序列化](../表现与支撑/存档序列化.md)「待办」的联合裁决条目。 +三条柜台纪律:**定价曲线一律几何递增**(每级费用按公比上浮——原则已拍板,起步价与公比数字归设计批配读数),与产出逻辑配套:深层关钥石产出多、后级强化贵,逼玩家往深处走。**道具工坊首版只挂通关次数渠道**是排期裁决(定价体系未建时的零定价依赖落地形态),不是终态。**存档义务随柜台产生**:钥石余额、永久强化等级表、每关通关次数表、Boss 首杀标记位——四件已逐域登记在 registry `save_file` 条目(内容 owner = 元进度,承载 owner = [存档序列化](../表现与支撑/存档序列化.md))。 护甲等级进首版属性柜台是 2026-09-18 拍板(用户选 A),其代价条款见下方换算率节的强条目登记。 diff --git a/design/gdd/systems/表现与支撑/存档序列化.md b/design/gdd/systems/表现与支撑/存档序列化.md index 0e253ef3..15421ca8 100644 --- a/design/gdd/systems/表现与支撑/存档序列化.md +++ b/design/gdd/systems/表现与支撑/存档序列化.md @@ -1,14 +1,14 @@ # 存档序列化 > 一句话:存档契约与版本迁移 -> 状态:范围草案(待设计) +> 状态:已定稿(2026-09-21 存档序列化批落地) > 旧版参考:../../../archive/gdd-2026-09-14/systems-index.md(旧体系未单独成文,散见于归档索引) ## 设计目的 本系统管的是"关掉游戏之后,玩家的东西还在不在"。序列化(本文含义:把游戏运行中的数据写成可存盘的档案,下次启动再原样读回来)与版本迁移(本文含义:游戏更新后,旧版本写下的存档要能被新版本继续读懂,必要时把旧结构改写成新结构)合起来,就是"存档契约"这门生意的全部。 -这件事是[核心循环](../../01-核心循环.md)里"存档在变厚"那条故事线的底层承载:钥石攒进存档、倒下也不丢——写在纸面上就是本系统的职责。本文是范围草案:只收拢现行材料能确证的范围边界与已知约束,存档格式、字段、读写时机与迁移方案一概未定,见文末「待设计问题清单」。 +这件事是[核心循环](../../01-核心循环.md)里"存档在变厚"那条故事线的底层承载:钥石攒进存档、倒下也不丢——写在纸面上就是本系统的职责。本系统在切片里的落地面是 `core/save_manager.gd`(SaveManager,Core 层零 UI、零玩法依赖),本文描述的就是这份契约:存什么、何时读写、版本怎么走、坏了怎么办。 ## 系统范围 @@ -18,20 +18,23 @@ | 负责 | 说明 | |---|---| -| 存档契约 | 存档的域划分与读写约定——域单位已由客户登记(见核心规则),字段与格式未设计 | -| 版本迁移 | 游戏更新后旧存档的兼容与改写策略——只立项,未设计 | -| 读写时机 | 何时写盘、何时读回——只立项,未设计 | +| 存档契约 | 单档 JSON 存档的域划分与读写约定(域清单见核心规则;registry `save_file` 条目逐域登记) | +| 版本迁移 | 版本字段 + 读档时链式迁移 + 高版本拒读禁写 | +| 读写时机 | 启动读盘;正式冒险收束写盘;原子写盘原语 | | 元进度的持久化承载 | 归档登记的职责口径:"元进度持久化框架"——钥石与解锁进度由本系统落盘 | +| 最近冒险摘要的持久化 | 冒险摘要五件事副本入档(cap 10),重开对照跨进程成立 | 不负责: | 不负责 | 归谁 | |---|---| | 存进去的内容口径(钥石怎么得、能换什么) | [元进度](../冒险循环/元进度.md)——本系统只承载,不定义 | -| 冒险摘要记哪几件事 | [冒险结构](../冒险循环/冒险结构.md)——本系统只消费其快照域标识 | +| 冒险摘要记哪几件事 | [冒险结构](../冒险循环/冒险结构.md)——本系统只消费其摘要五件事 | +| 冒险中续玩快照(主种子 + 随机流计数器的局中恢复) | [冒险结构](../冒险循环/冒险结构.md)——P1 后置(06 已登记),本批不涉及 | +| Boss 击杀的钥石金额计算(首杀全量 / 重复杀小额) | [元进度](../冒险循环/元进度.md)——本系统只记域内事实,不算账 | | 数值配方本体(所有玩家一致的表) | [数据配置](数据配置.md)——表是配方,存档是玩家进度,两者分离(见核心规则) | -| 局内数值的持久化 | 无——局内资源全部单局清零,现行没有局内数值需要入档(见核心规则的反面清单) | -| 系统级设置(音量/画面等) | 不入档——设置与存档两分(2026-09-19 用户拍板):设置独立存储、一安装一份,存档只存该存档的生涯数据;设置域归[设置与本地化](设置与本地化.md)所有 | +| 局内数值的持久化 | 无——局内资源全部单局清零,没有局内数值需要入档(见核心规则的反面清单) | +| 系统级设置(音量/画面等) | 不入档——设置与存档两分(2026-09-19 用户拍板):设置独立存储、一安装一份,存档只存生涯数据;设置域归[设置与本地化](设置与本地化.md)所有 | ## 在核心循环中的位置 @@ -41,81 +44,116 @@ 存档序列化不占核心循环里的一段,它贴在"死亡重开"出口的外侧:局内胜负归零时,它保证玩家攒下的东西不跟着归零。核心循环把"存档在变厚"讲成玩家重开的理由之一,本系统就是那条理由的底层承载。 -## 核心规则(已确证的范围边界与已知约束) - -本系统尚未设计,本节只收现行材料能确证的边界与约束;格式、字段、时机等全部问题见文末「待设计问题清单」。 +## 核心规则 ### 基础层身份:无上游依赖的承载方 -归档依赖分层为本文登记的身份是:基础层系统、无上游依赖、里程碑 Alpha,登记职责一句话是"元进度持久化框架"。这个身份有两条现行推论。其一,本系统不依赖任何玩法系统就能工作——它是被动的承载方,将来任何系统要把数据活过单局,都向本系统登记持久化归属,而不是本系统去追认每个系统。其二,登记职责虽从元进度起家,现行文档树上点名本系统的客户已经不止一户(见下一节)。 +SaveManager 不依赖任何玩法系统就能工作:它是被动的承载方,任何系统要把数据活过单局,都向本系统登记持久化归属,而不是本系统去追认每个系统。这条身份可验证:SaveManager 不引入任何玩法脚本、不碰数值表(ConfigDB)与随机服务(RngService),可以独立实例化、独立读写验证(冒烟独立段承载)。 -### 承接已登记的两户客户 +### 存什么:表与档分家,局内资源不入档 -现行文档里明确把数据托付给本系统的有两家,构成本系统目前确定的存档内容输入: +第一条边界:数据表与存档分家。表是配方,所有玩家一致;存档是玩家进度,每人不同。存档只记进度事实(得了什么、解锁了什么),不把数值配方写进存档——数值改版后,旧存档仍按加载时的数据表解释。这条边界从"数值单一事实源在数据表"的纪律直接推出,细节以[数据配置](数据配置.md)为准。 -| 客户 | 存什么 | 权威口径 | +第二条边界是反面清单:局内资源不入存档。行动点、储备行动点、道具库存、生命账本、GP(金币)全部单局清零——各归各的系统管,没有局内数值需要持久化。若未来某局内资源要开放跨局携带(如储备行动点跨战斗携带),须修订其池规则并向本系统登记持久化归属,禁止静默超域。 + +存档是单文件 JSON(`user://save.json`),字典键一律字符串。数据模型: + +```json +{ + "version": 1, + "meta": { + "keystone_balance": 0, + "forge_levels": {"max_hp": 0, "atk": 0, "armor_class": 0}, + "item_unlocks": {}, + "level_clear_counts": {"1": 0, "2": 0, "3": 0}, + "boss_first_kill": {}, + "recent_summaries": [] + } +} +``` + +| 域 | 存什么 | 内容 owner | |---|---|---| -| [元进度](../冒险循环/元进度.md) | 钥石(击杀关底 Boss 取走的永久资源)与解锁进度 | 钥石口径的权威在[游戏概念](../../00-游戏概念.md)与[元进度](../冒险循环/元进度.md),本文引用不展开 | -| [冒险结构](../冒险循环/冒险结构.md) | 快照域(域单位 = 一次冒险:主种子 + 各随机流计数器);冒险摘要(含本局主种子)供重开对照与复盘 | 域单位与摘要口径见[冒险结构](../冒险循环/冒险结构.md)的依赖表,本文照单承载 | +| `keystone_balance` | 钥石余额——只增不减(消费除外) | [元进度](../冒险循环/元进度.md) | +| `forge_levels` | 属性锻造三档等级(生命上限/基础攻击/护甲等级) | [元进度](../冒险循环/元进度.md) | +| `item_unlocks` | 已解锁道具集合(道具工坊消费) | [元进度](../冒险循环/元进度.md) | +| `level_clear_counts` | 每关通关次数(键 = 关卡序号字符串) | [元进度](../冒险循环/元进度.md) | +| `boss_first_kill` | Boss 首杀标记位(键 = 关卡序号字符串) | [元进度](../冒险循环/元进度.md) | +| `recent_summaries` | 最近冒险摘要副本,最新在前,最多 10 条 | [冒险结构](../冒险循环/冒险结构.md) | -表里的快照(本文含义:把某一刻的完整局面记录下来,供之后复盘与回放)由冒险结构定域,本文只管把它存取可靠。两户都是"生产方定内容、本系统管承载"的分工:元进度把钥石永不丢失立为它自己的验收判据,本系统作为承载方,将来的读写设计以不破坏这条判据为硬约束——档案写坏、读不回来,等于把玩家的钥石弄丢。 +消费面纪律:其他系统只经 SaveManager 的类型化访问器(读余额/加钥石/花钥石、读写锻造等级等)与域更新原语(`apply_adventure_summary`)操作存档,不碰内部数据结构。 -### 存什么:进度与配方分家,局内资源不入档 +### 读写时机 -第一条边界:数据表与存档分家。表是配方,所有玩家一致;存档是玩家进度,每人不同。由此推出一条已知约束:存档只记进度事实(得了什么、解锁了什么),不得把数值配方内置进存档——数值改版后,旧存档仍能按加载时的数据表解释。这条边界从"数值单一事实源在数据表"的现行纪律直接推出,细节以[数据配置](数据配置.md)定稿口径为准(该文档并行写作中)。 +``` +启动 ──▶ 读盘(load_from_disk:新档/损坏兜底/迁移链/高版本拒读,见下两节) +正式冒险收束(adventure_finished)──▶ apply_adventure_summary 入账 ──▶ 原子写盘 +营地钥石消费(元进度实现批接线)──▶ 每笔变更后即时写盘 +``` -第二条边界是反面清单:局内资源不入存档。行动点、储备行动点、道具库存、生命账本全部单局清零——各归各的系统管,现行没有任何局内数值需要持久化。这条边界已有登记在案的先例:储备行动点(玩家停手时把剩余行动点存入的池子,现行每场战斗清零)若未来开放跨战斗携带,按行动经济的归档登记口径,须修订其池规则并向本系统登记持久化归属,禁止静默超域;该开关的现行状态见[行动经济](../排雷循环/行动经济.md)的后置清单与[不做清单](../../05-不做清单.md)。 +写盘时机纪律:**凡钥石发生变动的时刻都要落盘**——正式冒险收束写一次,营地内每笔消费后即时写(元进度实现批接线)。这是"钥石不丢"判据的最强保证:任何时刻拔电源,玩家最多丢当前这一局的局内进程,不丢档。 + +写盘是原子操作:先写临时文件(`<路径>.tmp`),写完改名覆盖正式档——写一半断电不留烂档。 + +单局测试入口不写档:关卡直入口(L2/L3)与调试两层路径不接写盘——测试模式不污染生涯数据;只有正式冒险(AdventureRunner)收束才入账。 + +### 版本与迁移 + +存档带版本字段(`version`,当前 = 1,与 registry `schema_version` 的 MVP=1 同源)。三条走向: + +| 情形 | 处置 | +|---|---| +| 版本相同 | 正常加载;旧档缺的新域按默认值补齐(既有值不动)——数据表加字段不弃档 | +| 版本更高(未来版本的档) | **拒读且禁写**:不加载、后续写盘一律拒绝、内存以默认新档运行——保护未来版本的档不被当前版本覆写 | +| 版本更低(旧档) | 读档时逐级迁移:从旧版本起逐个调用注册的迁移函数升到当前版本;任一步失败按损坏兜底处理 | + +迁移函数经 `register_migration(旧版本号, 函数)` 注册:函数把数据从「旧版本」改写成「旧版本 + 1」。当前生产迁移链为空(第一版存档没有前身),骨架先行;首个改结构批随批注册第一个迁移函数。 + +### 损坏兜底 + +档案读不开(文件损坏、JSON 解析失败、结构出域、缺版本字段)时:原文件改名为 `<路径>.corrupt.bak`(已有同名备份则覆盖)留下现场,游戏用默认新档继续运行,并留下警告日志。两条底线:不静默丢档(坏档永远可人工找回)、不崩溃(坏档不挡玩)。 ## 依赖与输出 -本系统没有上游——这是归档依赖分层的登记口径,也是基础层身份的另一半:它先于一切玩法系统存在,供所有人存取。已登记的下游与协作: +本系统没有上游——基础层身份的另一半:它先于一切玩法系统存在,供所有人存取。已登记的下游与协作: | 方向 | 系统 / 文档 | 接口要点 | |---|---|---| -| 下游 | [元进度](../冒险循环/元进度.md) | 钥石与解锁进度的持久化承载方;对方以"钥石不丢"为验收,本系统以不破坏它为约束 | -| 下游 | [冒险结构](../冒险循环/冒险结构.md) | 快照契约域单位 = 冒险(主种子 + 随机流计数器);冒险摘要含主种子,供复盘 | -| 协作 | [数据配置](数据配置.md) | 表与档分家:数值口径永远以加载时的表为准;边界细节以其定稿口径为准 | +| 下游 | [元进度](../冒险循环/元进度.md) | 钥石余额、锻造等级、通关次数、首杀标记的持久化承载方;对方以"钥石不丢"为验收,本系统以不破坏它为约束 | +| 下游 | [冒险结构](../冒险循环/冒险结构.md) | 冒险摘要五件事副本入档(cap 10);快照与复盘的持久化承接 P1 后置 | +| 协作 | [数据配置](数据配置.md) | 表与档分家:数值口径永远以加载时的表为准 | ## P0 最小闭环 -MVP 不要求本系统的闭环:归档把本系统排在 Alpha 里程碑,与元进度同批(见[不做清单](../../05-不做清单.md):先把单局循环做扎实)。MVP 期没有存档,单局循环完整成立。Alpha 期最小闭环的方向跟着元进度的最小集走:钥石与解锁进度能写入存档、下次启动原样读回、数值表更新后旧档仍能读。快照与复盘域是否进首批交付,随元进度批排期裁决(见待办)。 +MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)把单局循环排前)。本批已落地的最小集:存档域首批内容(元进度四件 + 最近冒险摘要)能写入、读回、跨进程存活;版本与损坏策略就位;正式冒险收束自动入账。元进度消费面(花钥石、买强化、营地界面)归元进度实现批——承载已就绪,内容方接线即可用。 ## P1 / P2 后置 | 优先级 | 内容 | 后置原因 | |---|---|---| -| P1 | 快照与复盘的持久化承接 | 域单位已由冒险结构登记为冒险;是否随 Alpha 首批落地见待办 | -| P2 | 多存档槽位 | 单档还是多档未定,见待设计问题清单 | -| P2 | 损坏处理与兜底策略 | 只立项未设计,见待设计问题清单 | +| P1 | 快照与复盘的持久化承接(主种子 + 随机流计数器续玩恢复) | 域单位已由[冒险结构](../冒险循环/冒险结构.md)登记为冒险;06 已登记后置 | +| P2 | 多存档槽位 | 单档 MVP(2026-09-21 裁决);多档等有真实需求再立项 | ## 调参表 -暂无本系统自有的参数。存档格式、写盘时机、迁移策略都未设计,没有可调的现行值;将来若有(例如写盘频率一类),随设计定稿再登记。 +本系统没有玩法调参数值。两个结构常量登记在 registry `save_file` 条目:存档版本号(=1,与 registry `schema_version` 同源)、最近摘要保留条数(10)。二者是结构契约不是平衡值,改动属于版本迁移事件,不走调参批。 ## 验收标准 -待设计。方向性判据(供未来定稿参考): - -- 钥石不因存档丢失:本系统的写入、读回与迁移,不得破坏[元进度](../冒险循环/元进度.md)验收标准中的钥石不丢判据。 -- 改版不弃档:数据表数值更新后,既有存档仍能被读懂并继续游戏,不要求玩家重开。 -- 零玩法依赖可验证:本系统的读写与迁移,能在不运行任何玩法系统的前提下独立验证。 -- 域界干净:单局清零的资源(行动点、储备行动点、道具库存等)不出现在存档内容清单里。 +- 往返一致:全部域写入非默认值 → 写盘 → 新实例读回,逐域逐值相等(冒烟承载)。 +- 钥石不丢:写入、读回与迁移不得破坏[元进度](../冒险循环/元进度.md)的钥石只增不减判据;余额不足的消费被拒绝且无副作用(冒烟承载)。 +- 高版本拒读禁写:高版本档加载被拒、原文件字节不动、后续写盘被拒且文件仍字节不动(冒烟承载)。 +- 改版不弃档:旧版本档经迁移链读回;低版本档缺新域按默认补齐,既有值保留(冒烟承载)。 +- 域界干净:盘上文本不含局内资源键(行动点/储备行动点/道具库存/生命账本/GP)(冒烟承载)。 +- 零玩法依赖:SaveManager 不引入任何玩法脚本、不碰数值表与随机服务,可独立实例化验证(冒烟独立段承载)。 ## 待办 本系统待办条目已统一收编至 [06-待办清单](../../06-待办清单.md),本节不再逐条登记。 -## 待设计问题清单 - -1. 存档内容清单:除两户已登记的输入外,还有哪些系统的哪些数据需要入档?逐项登记持久化归属。 -2. 存档时机:何时写盘(每次结算、每枚钥石到手、退出时),写盘失败怎么办? -3. 版本迁移策略:迁移发生在哪一步(读档时、启动时),迁移失败怎么处理? -4. 存档槽位与多存档:单档还是多档,槽位与元进度账本如何对应? -5. 损坏处理:档案读不开时的玩家侧表现与数据兜底,是否保留备份。 -6. 冒险摘要的持久化归属:摘要在进程内存活即可,还是必须写入存档(重开对照跨进程是否成立)? - ## 版本历史 | 版本 | 日期 | 改动 | |---|---|---| | v0.1 | 2026-09-15 | 自归档线索整合为范围草案(旧体系未单独成文) | +| v1.0 | 2026-09-21 | **存档序列化批(SaveManager 落地)**:格式裁决 JSON 单档 `user://save.json`;数据模型逐域成文(元进度四件 + 最近冒险摘要 cap 10);读写时机(启动读盘/冒险收束写盘/营地即时写/测试入口不写)+ tmp+rename 原子写;版本策略(高版本拒读禁写/低版本链式迁移/缺域补齐);损坏兜底(.corrupt.bak 备份 + 默认新档);待设计问题清单六条全部裁决成文(1/2/3/5/6 入核心规则,4 = 单档 MVP 多档留 P2)。自范围草案升定稿 | diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 4dcfc856..85534ee5 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -64,10 +64,25 @@ entities: type: constant owner: design/gdd/systems/表现与支撑/数据配置.md attributes: - value: 实现期定(MVP=1);全部占位 schema 均携带版本字段 + value: 实现期定(MVP=1);全部占位 schema 均携带版本字段(已随存档序列化批落地 2026-09-21——存档侧 SAVE_FORMAT_VERSION=1 与本条同源) high_version_policy: 拒读(比较逻辑 owner=存档序列化) referenced_by: [存档序列化] + - name: save_file + type: schema_data + owner: design/gdd/systems/表现与支撑/存档序列化.md + attributes: + path: user://save.json(单档 MVP——多存档槽位 P2 保留) + format: JSON 单档(人可读可调试;字典键一律 String) + version: SAVE_FORMAT_VERSION=1(registry schema_version 同源;高版本拒读 + 禁写盘 + 内存默认新档运行;低版本读档时链式迁移 register_migration,任一步失败按损坏兜底) + domains: meta 域六件——keystone_balance(内容 owner=元进度)/ forge_levels 三档 max_hp·atk·armor_class(元进度)/ item_unlocks(元进度·道具工坊消费)/ level_clear_counts(元进度)/ boss_first_kill(元进度)/ recent_summaries cap 10 冒险摘要五件事副本(内容 owner=冒险结构) + write_timing: 启动读盘;正式冒险收束(adventure_finished → apply_adventure_summary)写盘;营地每笔钥石变更后即时写(元进度实现批接线);单关测试入口与调试两层不写档 + atomic_write: tmp + rename 原子写(写一半断电不留烂档——钥石不丢判据承载) + corruption_policy: 坏档改名 .corrupt.bak(已存在覆盖)+ 默认新档继续 + 警告留痕(不静默丢档不 crash) + domain_boundary: 行动点/RAP/道具库存/生命账本/GP 不入档(局内资源单局清零;冒烟断言承载) + impl: core/save_manager.gd(Core 层零 UI 零玩法依赖;消费面只经类型化访问器与 apply_adventure_summary 原语) + referenced_by: [元进度, 冒险结构] + - name: action_economy type: constant_set owner: design/gdd/systems/排雷循环/行动经济.md diff --git a/production/roadmap.md b/production/roadmap.md index 4c928326..d093acfd 100644 --- a/production/roadmap.md +++ b/production/roadmap.md @@ -16,6 +16,7 @@ | 2026-09-15 | 命名清偿批;格面资源批(五段结算/极性路由/可选类删除);攻击类型与奖励抽象分离;护甲等级登记批;伤害量值区间批 | | 2026-09-16 | 行动点压速批 8→6;护甲等级掷点批落地(smoke 233 全绿);AP6+AC 验收判读口径 v1.0;06-待办清单建账改版;系统更名批;敌人技能池提案对答启动 | | 2026-09-17 | 敌人技能池提案定稿(1c63830);技能池实施批落地(smoke 263 全绿,待收编);魔法道具切片期空内容拍板;关卡节点地图定稿批解锁;跨会话状态持久化批(快照节 + 本文件 + 守卫文案升级) | +| 2026-09-21 | 存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档,smoke 463 全绿);GP+商店房实施批;第三关内容批;八型批等 2026-09-20 多批(详见 active.md 台账 96-116) | ## 路线图(主线路线与门) From 7ddfa4b3667176c5f64d68809a61c61286143cd9 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 04:45:51 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat(core):=20=E5=86=92=E9=99=A9=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=AE=9E=E7=8E=B0=E6=89=B9=E2=80=94=E2=80=94Adventure?= =?UTF-8?q?Runner+LevelRunner=E8=B4=A6=E6=9C=AC=E5=A4=96=E7=BD=AE+Adventur?= =?UTF-8?q?eTable=E5=BB=BA=E8=A1=A8=EF=BC=8C=E4=B8=89=E5=85=B3=E6=AD=A3?= =?UTF-8?q?=E5=BC=8F=E5=BA=8F=E5=88=97=E7=94=9F=E6=95=88=EF=BC=88=E5=86=92?= =?UTF-8?q?=E7=83=9F482=E5=85=A8=E7=BB=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dungeon-sweeper-vertical-slice/README.md | 14 +- .../core/adventure_runner.gd | 224 ++++++++++++++++ .../core/adventure_runner.gd.uid | 1 + .../core/config_db.gd | 3 + .../core/level_runner.gd | 93 ++++++- .../data/schemas/adventure_table.gd | 23 ++ .../data/schemas/adventure_table.gd.uid | 1 + .../data/tables/adventure.tres | 14 + .../presentation/battle_narrator.gd | 20 ++ .../presentation/main.gd | 87 ++++++- .../presentation/ui_assembler.gd | 7 +- .../smoke/level_autoplay.gd | 118 ++++++++- .../smoke/smoke_all.gd | 242 ++++++++++++++++++ 13 files changed, 826 insertions(+), 21 deletions(-) create mode 100644 prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd create mode 100644 prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd.uid create mode 100644 prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd create mode 100644 prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd.uid create mode 100644 prototypes/dungeon-sweeper-vertical-slice/data/tables/adventure.tres diff --git a/prototypes/dungeon-sweeper-vertical-slice/README.md b/prototypes/dungeon-sweeper-vertical-slice/README.md index 91c8bb33..36e517ad 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/README.md +++ b/prototypes/dungeon-sweeper-vertical-slice/README.md @@ -12,7 +12,7 @@ 1. 用 Godot 4.7.2 打开本目录(含 `project.godot` 的文件夹) 2. 按 F5 运行(主场景 `presentation/main.tscn`) -3. **主入口:`▶ 第一关 · 地精洞窟`**——线性节点链:战→战→赏(随机主动道具)→战→战→疗(血量恢复点)→王(Boss 遗迹守卫) +3. **主入口:`▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)`**——一条命走完三关(冒险结构实现批 2026-09-21:生命/GP/加算区跨关同账、非末关通关发大奖励〔上限提升+血回满+钥石〕、任意战斗倒下冒险终、结算屏出冒险摘要五件事);首关节点链:战→战→赏→战→战→疗→王。`▶ 测试直入口 · 第二关 / 第三关` 保留单关试玩模式 4. `测试用 · 角色数值调整板`:攻击/生命百分比加成(模拟天赋树解锁)+ 批量模拟(输出通关率/承伤期望/败北节点分布) headless 冒烟(55 项断言,含关卡线性推进/满配通关/裸装败北/回放确定性,约 30 秒): @@ -169,7 +169,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s ## L2 试玩采集清单(分支节点图批 2026-09-17——关卡 2「幽暗地域」试玩观测项) -入口:首页"▶ 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)"(测试模式直入口,种子框同用;与关卡 1 行为一致:通关/败北回首页)。 +入口:首页"▶ 测试直入口 · 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)"(测试模式直入口,种子框同用;正式序列走「开始冒险」主入口)。 - **选路行为**:战斗胜利/兑付确认后弹出选路覆盖层(整图 + 候选按钮)——玩家是否理解"零自动推进、每跳都要选";单候选节点(恢复点→编号 6)是否被读成"卡住"。 - **岔路使用率**:四处真实岔路的走向分布(入口 0→[1,2];战斗 1→[3,4];奖励 2→[4,5];深层 4→[6,7])——奖励点路线 vs 纯战斗路线的选择比例;整图渲染是否被实际用来规划(对比只点按钮的玩家)。 @@ -209,7 +209,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s 5. **难度台阶可感知性**:D2 伏击兽/D4 Boss 相比 D0 蛆虫的变强是否有具体面孔(新敌种=新图像线索)。 6. **奖励供给体感**:可选类道具(刀/治疗包)获取频率与囤积感受(供给控制器批后每关 ≈4.5–6.5 件)。 -试玩方式:F5 → 「▶ 第一关 · 地精洞窟」基线打一局 → 调整板 +100% 攻击再打一局(对照"数值提升→通关期望改善"的元进度体感)。 +试玩方式:F5 → 「▶ 开始冒险」基线打一局(一条命三关连探——冒险结构实现批 2026-09-21 起,第一关读数即冒险首关) → 调整板 +100% 攻击再打一局(对照"数值提升→通关期望改善"的元进度体感)。 ## 2026-09-12 试玩批次 1 → v3 数值批(F7 流程实践) @@ -522,3 +522,11 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s - 结构面:SkillTable type 枚举 +debuff;池纪律 +每池妨害 ≤1;施放类 side 守卫(buff→enemy / debuff→hero,`validate_skill_pool_links` 增 status_table 显式入参——装载序修正);意图徽标 +妨害系 ☠(紫)。 - 验证:冒烟 306 项 0 失败(+新探针 `_smoke_debuff_rider`:毒爆行逐拍精确复算 / rider 键封闭 / 蛛网束缚落主角端到端 / 六项加载校验拒绝)+ 四探针 PASS;800 局模拟试玩(两关 × 20 种子 × 双画像)——L2 基线通关 90%→80%、死因全部集中 Boss(设计方向生效),L1 零行为变更回归一致(65%/85%)。 - 顺手收口:smoke_all `variant_pool(1, ["dungeon"])` → `"dungeon"`(并行 board_type 批签名 Array→String 的漏网同步——此前该断言被 SCRIPT ERROR 静默吞掉);analyze_playtest.py 反事实重算 +rider 识别。 + +## 2026-09-21 冒险结构实现批(三关正式序列生效——AdventureRunner + LevelRunner 账本外置 + AdventureTable 建表) + +**主入口升级**:「▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)」——一条命走完三关的完整 Roguelike 冒险;`▶ 测试直入口 · 第二关/第三关` 保留单关试玩模式,L1 单关直入口随批取消(冒险首关即 L1)。 + +**冒险级语义**(设计权威 = 冒险结构.md v0.6):主种子恰一次注入(冒险内不重播种,同种子同操作序列逐位可复现);生命账本跨关同账(战后回写、恢复点按比例封顶、大奖励回满);GP 钱包跨关延续、冒险收束清零;统一冒险加算区(商店购买与大奖励同账);非末关通关发大奖励(上限提升 +10 读 adventure.tres、血回满、钥石 +10),末关不发上限提升、钥石照记;任意战斗倒下 = 冒险失败;结算屏出冒险摘要五件事(打穿没有/通关几关/止步第几关/钥石合计/主种子)。 + +**验证**:冒烟 437→482 项 0 失败三闸全过(冒险表 +4 / API 闸 +8 / 冒险验收 +33:表覆盖义务、满配通关、种子复现、大奖励恰 2 次与覆盖值传播、败北线两种子扫描、重复宣布拒绝、GP 跨关延续、加算区同账、恢复数学、放弃两态);MC 回归零行为变更(run_level 旧路径同种子逐位一致)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd b/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd new file mode 100644 index 00000000..f24f72d3 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd @@ -0,0 +1,224 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完—— +# 主种子恰一次注入、生命/GP/加算区跨关同账、大奖励非末关恰兑付一次、三结局摘要五件事成立(P0) +# Date: 2026-09-21(冒险结构实现批——设计权威 design/gdd/systems/冒险循环/冒险结构.md) +# 冒险结构编排器(Core 层零 UI 依赖——headless 可跑完整冒险):一局的关卡序列、生命账本、GP 冒险 +# 账本、统一冒险加算区、关卡大奖励、三结局结算与主种子接线的唯一所有者。 +# +# 接口契约四件套落点(冒险结构.md「与关卡节点地图的边界」——本类是正式侧承载): +# ① 开始关卡 = start_level_external(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接); +# ② 战斗结果上报 = runner 经共享账本属性直写(LevelRunner._on_battle_finished 回写 hero_hp 到 +# _ledger——零直调战斗域,战后生命终值随账本回流本类); +# ③ 治疗封顶回报 = apply_rest(恢复点节点兑付时 LevelRunner 经 _rest_handler 回调——按比例回血并 +# 封顶的执行归本类); +# ④ 关卡结束 = _on_level_finished(LevelRunner.level_finished 到达——结果 + 生命终值 + 节点位置)。 +# +# 接口漂移清偿登记(06 清单①②③⑥⑦——LevelRunner 侧同批登记):①种子域归本类 +# (start_adventure 调 rng.new_adventure 全冒险恰一次;LevelRunner 冒险模式不重播种); +# ②生命账本归本类(共享 _ledger);③恢复兑付执行归本类(_rest_handler);⑥收束先清库后宣布—— +# 现行无关卡域库存(格面资源批 2026-09-15 后为空集),战斗结束 retire_mapping + despawn_all 先于 +# battle_finished 即事实清库,不另设空接口;⑦失败结算归本类(LevelRunner 只上报结果)。 +# +# 收束次序登记(大奖励兑付时点——冒险结构.md「关卡大奖励」):关卡节点地图先清库、后宣布关卡 +# 结束(次序不可换);大奖励在宣布之后、下一关开始之前兑付——本类 _on_level_finished 兑付、 +# continue_to_next_level 开始下一关,两步间为呈现层大奖励屏(冒烟/自动路径立即确认)。 +# 结算受理只算一次(契约):state != "running" 的关卡结束宣布与放弃请求一律拒绝并留诊断, +# 账本与摘要零变化。 +# 纪律:ADR-0001 S6——连 runner 信号一律 _forward_* 方法 Callable(禁 lambda 捕获 self 成环); +# Core 层禁 UI 引用;数值零字面量——level_clear_hp_bonus / keystone_per_level 只读 db.adventure, +# 基础上限只读 db.hero。 +class_name AdventureRunner +extends RefCounted + +signal level_started(level_index: int, display_name: String) # 每关开始(含冒险首关)——呈现层重取 runner()、刷新图条与层名行 +signal big_reward_paid(reward: Dictionary) # 大奖励兑付(非末关通关恰一次)——{from_level, hp_bonus, new_max_hp, keystone_gained, keystone_total};呈现层大奖励屏/冒烟立即确认 +signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子 +# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)---- +signal node_entered(node: Dictionary, info: Dictionary) +signal choices_presented(candidates: Array) +signal step_row(row: Dictionary) +signal round_started(round_index: int) +signal board_changed() +signal wallet_changed(event: String, gp: int, extra: Dictionary) + +var db +var rng +var state := "idle" # idle / running / settling(冒险结构.md 三态) +var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享) +var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(商店购买与大奖励同一本账) +var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入) +var level_index := 0 # 当前关卡序号(1 起) +var levels_total := 0 # 序列总关数(读 level 表行数) +var keystone := 0 # 钥石入账累计(摘要第三件事的账面) +var master_seed := 0 # 本局主种子(摘要第五件事) +var summary := {} # 最近一次结算的冒险摘要(idle 后保留——重开才新生成) +var _runner: LevelRunner # 当前关卡行进器(每关新建——关卡域状态天然隔离) +var _pending_next_level := false # 大奖励已兑付、等 continue_to_next_level 确认 +var _override := [0, 0] # TunePanel 百分比通道 [atk_pct, hp_pct](每关新 runner 都要应用) + + +func _init(db_ref, rng_ref) -> void: + db = db_ref + rng = rng_ref + + +# ---- 冒险生命周期 ---- + +# 冒险开始(主种子注入恰一次——全冒险唯一 rng.new_adventure 调用点;重开 = 全新一轮: +# 账本/加算区/已购档/钥石/摘要全重置,生命置满基础上限,从第 1 关起步) +func start_adventure(seed_val: int) -> bool: + if state != "idle": + push_error("[冒险结构] start_adventure 拒绝——state=%s(重开须待上一局结算完毕)" % state) + return false + master_seed = seed_val + rng.new_adventure(seed_val) + ledger = {"hero_hp": 0, "gp": 0} + bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} + purchased = {} + keystone = 0 + summary = {} + levels_total = db.level.rows.size() + level_index = 1 + _pending_next_level = false + ledger["hero_hp"] = int(db.hero.max_hp) # 生命账本置满(基础上限读英雄表——加算区刚归零) + state = "running" + return _start_current_level() + + +# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度) +func set_hero_override(atk_pct: int, hp_pct: int) -> void: + _override = [atk_pct, hp_pct] + if _runner != null: + _runner.set_hero_override(atk_pct, hp_pct) + + +# 当前关卡行进器读面(呈现层/冒烟用——每关新建实例,调用方须在 level_started 后重取) +func runner() -> LevelRunner: + return _runner + + +# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶;与 LevelRunner.rest_heal_amount +# 同式,同一公式两个落点是临时态,注释指认,统一归并归后续批) +func apply_rest(pct: int, hp: int, max_hp: int) -> int: + return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) + + +# 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;呈现层大奖励屏 / 冒烟立即确认) +func continue_to_next_level() -> bool: + if not _pending_next_level: + push_error("[冒险结构] continue_to_next_level 拒绝——无待确认的关卡推进") + return false + _pending_next_level = false + level_index += 1 + return _start_current_level() + + +# 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。 +# MVP 无界面入口,语义先行预留;结算受理只算一次——非 running 态拒绝) +func abandon() -> bool: + if state != "running": + push_error("[冒险结构] abandon 拒绝——state=%s(非进行中不可放弃)" % state) + return false + if _runner != null and _runner.current_battle() != null: + push_error("[冒险结构] abandon 拒绝——战斗进行中(战斗域单事务在飞)") + return false + _settle("abandoned") + return true + + +# ---- 关卡推进(私有——序列编排单一出口)---- + +func _start_current_level() -> bool: + _runner = LevelRunner.new(db, rng) + # core↔core 接线只许方法 Callable(ADR-0001 S6——lambda 捕获 self 会延长 RefCounted 生命周期乃至成环) + _runner.node_entered.connect(_forward_node_entered) + _runner.choices_presented.connect(_forward_choices_presented) + _runner.step_row.connect(_forward_step_row) + _runner.round_started.connect(_forward_round_started) + _runner.board_changed.connect(_forward_board_changed) + _runner.wallet_changed.connect(_forward_wallet_changed) + _runner.level_finished.connect(_on_level_finished) + _runner.set_hero_override(int(_override[0]), int(_override[1])) + # 接口①开始关卡:生命经共享账本交接(账本当前值即开局生命——冒险开始置满 / 大奖励回满已写好) + var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased, + Callable(self, "apply_rest")) + if ok: + level_started.emit(level_index, String(_runner.level_row.get("display_name", ""))) + return ok + + +# 接口④关卡结束(结果 + 生命终值 + 节点位置——胜负皆报;结算受理只算一次) +func _on_level_finished(payload: Dictionary) -> void: + if state != "running": + push_error("[冒险结构] 重复关卡结束宣布拒绝——state=%s(账本与摘要零变化)" % state) + return + var result := String(payload.get("result", "")) + if result == "defeat": + ledger["hero_hp"] = 0 # 倒下记 0(生命账本战后回写口径) + _settle("failed") + return + if result != "victory": + push_error("[冒险结构] 关卡结束结果出域: %s" % result) + return + if level_index < levels_total: + # 大奖励(非末关通关恰一次):上限提升写加算区 → 血回满 = 新上限 → 钥石入账(顺序即契约) + var hp_bonus: int = int(db.adventure.level_clear_hp_bonus) + bonus["max_hp"] = int(bonus.get("max_hp", 0)) + hp_bonus + ledger["hero_hp"] = int(db.hero.max_hp) + int(bonus["max_hp"]) + keystone += int(db.adventure.keystone_per_level) + # 置位先于 emit(信号同步派发——处理器内立即确认时 _pending_next_level 必须已就位) + _pending_next_level = true + big_reward_paid.emit({ + "from_level": level_index, + "hp_bonus": hp_bonus, + "new_max_hp": int(db.hero.max_hp) + int(bonus["max_hp"]), + "keystone_gained": int(db.adventure.keystone_per_level), + "keystone_total": keystone, + }) + return + # 末关通关:钥石照记、不发上限提升(没有下一战可受益)——冒险完成结算 + keystone += int(db.adventure.keystone_per_level) + _settle("completed") + + +func _settle(result: String) -> void: + # 冒险摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石入账合计 / 本局主种子 + summary = { + "result": result, + "cleared": result == "completed", + "levels_cleared": levels_total if result == "completed" else level_index - 1, + "stopped_at": 0 if result == "completed" else level_index, + "keystone_total": keystone, + "master_seed": master_seed, + } + if _runner != null: + _runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现) + state = "settling" + adventure_finished.emit(summary) + state = "idle" + + +# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明)---- + +func _forward_node_entered(node: Dictionary, info: Dictionary) -> void: + node_entered.emit(node, info) + + +func _forward_choices_presented(candidates: Array) -> void: + choices_presented.emit(candidates) + + +func _forward_step_row(row: Dictionary) -> void: + step_row.emit(row) + + +func _forward_round_started(ri: int) -> void: + round_started.emit(ri) + + +func _forward_board_changed() -> void: + board_changed.emit() + + +func _forward_wallet_changed(event: String, gp_val: int, extra: Dictionary) -> void: + wallet_changed.emit(event, gp_val, extra) diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd.uid new file mode 100644 index 00000000..c9ec8c8f --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/core/adventure_runner.gd.uid @@ -0,0 +1 @@ +uid://lem5anpkcd0p diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd index 98f00e4e..4c92f674 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/config_db.gd @@ -21,6 +21,7 @@ const TABLE_MANIFEST := { "level": "level.tres", "difficulty": "difficulty.tres", "shop": "shop.tres", # GP+商店房实施批 2026-09-21(表与 schema 落籍归并行数据面——本批只接线加载与消费) + "adventure": "adventure.tres", # 冒险结构实现批 2026-09-21(AdventureTable 建表——大奖励与钥石行值单点) } var action_economy: ActionEconomyTable @@ -37,6 +38,7 @@ var difficulty: DifficultyTable # GP+商店房实施批(2026-09-21):ShopTable 暂持无类型注记——schema class_name 落籍归并行数据面, # 落籍后可收紧为 `var shop: ShopTable`(消费面一律走下方 shop_* 访问器,禁直读行集) var shop = null +var adventure: AdventureTable var is_ready := false var _diagnostics: Array = [] # {severity, category, message} @@ -119,6 +121,7 @@ func load_tables(root: String, collect_only: bool) -> bool: level = loaded.get("level") difficulty = loaded.get("difficulty") shop = loaded.get("shop") + adventure = loaded.get("adventure") is_ready = not has_errors return not has_errors diff --git a/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd b/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd index cfe8b156..59217cb2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/core/level_runner.gd @@ -17,6 +17,18 @@ # 加算区 bonus{atk,max_hp,armor_class}(商店购买与关卡大奖励共用账本——战斗开始参数 = 基础表值 + # 加算区,TunePanel 百分比在加算之后乘)+ 商店房节点兑付(开覆盖层/购买出账/离开回选路确认—— # 复用 continue_after_node 兑付确认纪律,零自动推进)。 +# 冒险结构实现批(2026-09-21)——接口漂移清偿登记(06 清单「切片行进代码与定稿的接口漂移」①②③⑥⑦): +# ① 种子域归冒险结构:start_level 旧路径(单关测试模式)保留自调 rng.new_adventure;新路径 +# start_level_external(冒险模式)不重播种——主种子注入恰一次归 AdventureRunner.start_adventure。 +# ② 生命账本归冒险结构:hero_hp/gp 改属性落 _ledger 字典——冒险模式经 start_level_external 注入 +# AdventureRunner 共享账本(Dictionary 引用共享),单关模式 = 本实例私有账本;战斗结果上报语义 = +# runner 经共享账本属性直写(接口②——战后生命终值已在本类 _on_battle_finished 回写账本)。 +# ③ 恢复数学抽成单一路径 rest_heal_amount——冒险模式经 _rest_handler 回调(AdventureRunner.apply_rest, +# 接口③治疗封顶回报执行体归冒险结构);单关模式默认内置。公式逐位一致,零行为变更。 +# ⑥ 收束先清库后宣布——现行无关卡域库存(格面资源批 2026-09-15 可选类删除后为空集),战斗结束 +# retire_mapping + despawn_all 先于 battle_finished 发出即事实清库,不另设空接口(假契约)。 +# ⑦ 倒下的失败结算归冒险结构:本类只发 level_finished(每战结果上报语义),失败结算由 +# AdventureRunner._on_level_finished 执行;GP 冒险收束清零同步归冒险结构(本类冒险模式不关钱包)。 class_name LevelRunner extends RefCounted @@ -32,9 +44,20 @@ var db var rng var level_row: Dictionary = {} var node_index := -1 -var hero_hp := 0 -var gp := 0 # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) -var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励 hp 上限提升写同一本账(大奖励在切片序列未生效,契约位:大奖励 hp 上限提升写本加算区,随正式序列批接线) +# ---- 账本载体(冒险结构实现批——漂移②清偿)---- +# 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本); +# 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。 +var _ledger: Dictionary = {"hero_hp": 0, "gp": 0} +var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③) +var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点) +# hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger) +var hero_hp: int: + get: return int(_ledger["hero_hp"]) + set(v): _ledger["hero_hp"] = v +var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) + get: return int(_ledger["gp"]) + set(v): _ledger["gp"] = v +var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励写同一本账(冒险模式 = AdventureRunner 共享字典;大奖励接线归 AdventureRunner._on_level_finished) var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零) # 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口 var _settlement = null # 当前战斗编排器(关卡结束置 null) @@ -87,6 +110,11 @@ func restore_hero_base() -> void: # ---- 关卡生命周期 ---- func start_level(level_index: int, seed_val: int) -> bool: + # 单关测试模式(旧路径——既有语义一字不改):私有账本、内置恢复数学、自调 new_adventure(漂移① + # 单关=冒险的历史简化在测试模式保留;正式种子域归 AdventureRunner.start_adventure) + _external = false + _rest_handler = Callable(self, "rest_heal_amount") + _ledger = {"hero_hp": 0, "gp": 0} var row := _find_level_row(level_index) if row.is_empty(): push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index) @@ -116,6 +144,42 @@ func start_level(level_index: int, seed_val: int) -> bool: return _enter_node(entry) +# 冒险结构接口①:开始关卡(载荷 = 关卡序号 + 当前生命 + 生效上限——生命经共享账本交接; +# 种子域归冒险结构:本路径不重播种;账本归冒险结构:本路径不重置 gp/bonus/_shop_purchased, +# 也不重置 hero_hp——账本当前值即开局生命,AdventureRunner 已在 ledger 写好) +# 流程与 start_level 同款(_find_level_row → 状态初始化 → _graph_mode 探测 → _enter_node(entry)), +# 差异仅在账本/加算区/已购档/恢复执行体的注入与零重置。 +func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary, + purchased_ref: Dictionary, rest_handler: Callable) -> bool: + var row := _find_level_row(level_index) + if row.is_empty(): + push_error("[关卡节点地图] level_index %d 在 LevelTable 无行" % level_index) + return false + _external = true + _ledger = ledger + bonus = bonus_ref + _shop_purchased = purchased_ref + _rest_handler = rest_handler + level_row = row + node_index = -1 + level_result = "" + _awaiting_choice = false + _candidates = [] + visited = [] + _graph_mode = false + for node in nodes(): + if node.has("next"): + _graph_mode = true + break + _running = true + var entry := _entry_node() + if entry.is_empty(): + push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)") + _running = false + return false + return _enter_node(entry) + + func _find_level_row(level_index: int) -> Dictionary: for row in db.level.rows: if int(row.level_index) == level_index: @@ -281,10 +345,12 @@ func _on_battle_finished(payload: Dictionary) -> void: hero_hp = maxi(1, int(payload.get("hero_hp", 0))) # despawn 前捕获值(载荷自包含) _gain_wallet(int(payload.get("gp_dropped", 0))) # 战斗完成入账(clamp ≥0——破财可把 escrow 打负,钱包入账时钳制) if String(node.get("type", "")) == "boss" or result == "defeat": - # Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(Roguelike——冒险即终,重开归 冒险结构/元进度) + # Boss 胜利 = 关卡通关;任意战斗败北 = 关卡失败(漂移⑦清偿——每战结果上报冒险结构, + # 失败结算归 AdventureRunner;本类只发 level_finished) level_result = "victory" if result == "victory" else "defeat" var hp_left := hero_hp if result == "victory" else 0 - _close_wallet() # 冒险收束(通关/倒下)→ GP 清零(局内货币生命周期终点) + if not _external: + close_wallet() # 单关模式:关卡收束即冒险收束(GP 清零);冒险模式归 AdventureRunner._settle _running = false _settlement = null level_finished.emit({"result": level_result, "hp": hp_left, "node_index": node_index, @@ -295,11 +361,17 @@ func _on_battle_finished(payload: Dictionary) -> void: _present_choices() +# 恢复数学单一路径(漂移③清偿——同一公式两个落点:本体内置默认 + AdventureRunner.apply_rest, +# 统一归并归后续批):floor(上限 × 比例 ÷ 100) 后封顶缺口;冒险结构 GDD 公式逐位一致 +func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int: + return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) + + func _apply_rest() -> int: var pct: int = int(level_row.get("rest_heal_pct", 0)) - var mhp := effective_max_hp() - var heal := int(floor(float(mhp) * float(pct) / 100.0)) - var actual: int = mini(heal, mhp - hero_hp) + # 接口③治疗封顶回报:单关模式 = 内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest + #(执行归冒险结构——回报比例、由其按封顶公式回写共享账本后再由本类叠加) + var actual: int = int(_rest_handler.call(pct, hero_hp, effective_max_hp())) hero_hp += actual return actual @@ -355,8 +427,9 @@ func _gain_wallet(amount: int) -> void: wallet_changed.emit("gain", gp, {"amount": amount}) -# 冒险收束清零(通关/倒下——局内货币生命周期终点,不入存档不入摘要) -func _close_wallet() -> void: +# 冒险收束清零(通关/倒下/放弃——局内货币生命周期终点,不入存档不入摘要;public——冒险模式由 +# AdventureRunner._settle 在冒险收束时调用,单关模式 = 关卡收束即冒险收束) +func close_wallet() -> void: gp = 0 wallet_changed.emit("clear", 0, {}) diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd new file mode 100644 index 00000000..7d71b3b5 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd @@ -0,0 +1,23 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完,通关兑付的大奖励 +# 与钥石合计在冒险摘要里成立(P0) +# Date: 2026-09-21(冒险结构实现批——AdventureTable 建表,registry adventure_table 条目 2026-09-15 登记) +# 数据配置 AdventureTable schema(冒险结构 关卡大奖励行值;单行全局,Rule 4 豁免——HeroTable/SettlementTable 先例)。 +# 两字段 knob(调参表归 冒险结构;数值所有权归 难度曲线与Boss 曲线调参批,全部 provisional): +# level_clear_hp_bonus = 关卡大奖励 HP 上限提升(写统一冒险加算区 max_hp 轴——与商店购买同一本账, +# 经 begin_battle 覆盖值通道对后续每战生效;值锚 = 难度曲线与Boss 区间平移换算率,上调须重扫通关率带)。 +# keystone_per_level = 每通关一关钥石入账值(末关通关照记;消费方 元进度——本表只记账不做消费)。 +class_name AdventureTable +extends Resource + +@export var level_clear_hp_bonus: int = 10 # [0,999] knob——关卡大奖励 HP 上限提升 +@export var keystone_per_level: int = 10 # [0,999] knob——每通关一关钥石入账 + + +func validate() -> Array: + var errs: Array = [] + if level_clear_hp_bonus < 0 or level_clear_hp_bonus > 999: + errs.append({"category": "domain", "message": "level_clear_hp_bonus 越域 [0,999]: %d" % level_clear_hp_bonus}) + if keystone_per_level < 0 or keystone_per_level > 999: + errs.append({"category": "domain", "message": "keystone_per_level 越域 [0,999]: %d" % keystone_per_level}) + return errs diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd.uid b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd.uid new file mode 100644 index 00000000..8c9d86ff --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/data/schemas/adventure_table.gd.uid @@ -0,0 +1 @@ +uid://cmg04glgweakh diff --git a/prototypes/dungeon-sweeper-vertical-slice/data/tables/adventure.tres b/prototypes/dungeon-sweeper-vertical-slice/data/tables/adventure.tres new file mode 100644 index 00000000..9b5e5e64 --- /dev/null +++ b/prototypes/dungeon-sweeper-vertical-slice/data/tables/adventure.tres @@ -0,0 +1,14 @@ +[gd_resource type="Resource" script_class="AdventureTable" load_steps=2 format=3] + +[ext_resource type="Script" path="res://data/schemas/adventure_table.gd" id="1"] + +[resource] +script = ExtResource("1") +level_clear_hp_bonus = 10 +keystone_per_level = 10 + +; 冒险结构实现批(2026-09-21 建表——registry adventure_table 条目同批注记更新): +; 关卡大奖励三件套的数值半边(上限提升)+ 钥石每关入账值;单行全局 Rule 4 豁免(HeroTable/SettlementTable 先例)。 +; 大奖励 = 非末关通关恰一次:上限提升(写统一冒险加算区 max_hp 轴)+ 血回满(= 新上限)+ 钥石入账; +; 末关通关不发上限提升、钥石照记(设计意图:奖励只朝下一战生效)。钥石消费方 = 元进度(本表只记账)。 +; 两值全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss)。 diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd index 2e05c3e3..c5ee3dc2 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/battle_narrator.gd @@ -82,3 +82,23 @@ static func boss_name(runner, db) -> String: var row: Dictionary = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) return String(row.get("display_name", "")) return "遗迹守卫" + + +# 大奖励屏文案(冒险结构实现批——兑付三件:上限提升/血回满/钥石入账;读数 = big_reward_paid 载荷快照) +static func big_reward_text(reward: Dictionary) -> String: + return "第 %d 层通过!大奖励:生命上限 +%d(新上限 %d),生命回满,钥石 +%d(累计 %d)" % [ + int(reward.get("from_level", 0)), int(reward.get("hp_bonus", 0)), + int(reward.get("new_max_hp", 0)), int(reward.get("keystone_gained", 0)), + int(reward.get("keystone_total", 0))] + + +# 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子) +static func adventure_summary_text(summary: Dictionary) -> String: + var verdict := "冒险失败 · 未打穿" + if bool(summary.get("cleared", false)): + verdict = "冒险完成 · 已打穿" + elif String(summary.get("result", "")) == "abandoned": + verdict = "冒险放弃 · 未打穿" + return "%s\n通关 %d 关 · 止步第 %d 层 · 钥石合计 %d · 主种子 %d" % [ + verdict, int(summary.get("levels_cleared", 0)), int(summary.get("stopped_at", 0)), + int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))] diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 1e8a0b76..67baab64 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -29,7 +29,9 @@ const DebugSessionScript := preload("res://presentation/debug_session.gd") var db var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) -var runner = null # 关卡节点地图 图版推进器(完整关卡模式) +var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner()) +var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) +var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _level_active := false var _seed_edit: LineEdit @@ -200,6 +202,9 @@ func _input_locked() -> bool: func _start_full_level(level_index: int = 1) -> void: _start_overlay.visible = false + if level_index == 1: + _start_adventure() # 冒险首关即 L1——正式序列入口(冒险结构实现批) + return # E4:加成经 runner.set_hero_override → encounter 参数入战斗(零表写——无双写叠加隐患) runner = LevelRunner.new(db, get_node("/root/RngService")) runner.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) @@ -211,6 +216,7 @@ func _start_full_level(level_index: int = 1) -> void: runner.level_finished.connect(_on_level_finished) runner.wallet_changed.connect(_on_wallet_changed) _level_active = true + _adventure_mode = false _reset_run_scope() log_view.clear() var seed_val := _seed_value() @@ -223,6 +229,40 @@ func _start_full_level(level_index: int = 1) -> void: tune_panel.atk_pct() > 0 or tune_panel.hp_pct() > 0)) +# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; +# 信号连接只在组合根——ADR-0001) +func _start_adventure() -> void: + adventure = AdventureRunner.new(db, get_node("/root/RngService")) + _adventure_mode = true + adventure.step_row.connect(_on_step_row) + adventure.round_started.connect(_on_round_started) + adventure.board_changed.connect(_on_board_changed) + adventure.node_entered.connect(_on_runner_node) + adventure.choices_presented.connect(_on_choices_presented) + adventure.wallet_changed.connect(_on_wallet_changed) + adventure.big_reward_paid.connect(_on_big_reward) + adventure.adventure_finished.connect(_on_adventure_finished) + adventure.level_started.connect(_on_adventure_level_started) + _level_active = true + _reset_run_scope() + log_view.clear() + var seed_val := _seed_value() + logger.start_session(seed_val) + adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) + if not adventure.start_adventure(seed_val): + _fatal_screen("冒险启动失败——见输出日志") + return + + +# 每关开始(冒险模式):重取 runner 实例 + 图条刷新 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达) +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)) + + func _reset_run_scope() -> void: # 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号) _hints_shown = {} @@ -231,6 +271,9 @@ func _reset_run_scope() -> void: func _on_runner_node(node: Dictionary, info: Dictionary) -> void: + # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 + if _adventure_mode and adventure != null: + runner = adventure.runner() var t := String(node.get("type", "")) if t == "battle" or t == "boss": settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) @@ -280,8 +323,41 @@ func _on_level_finished(payload: Dictionary) -> void: _refresh_presentations() +# ---- 冒险模式终局面(大奖励屏 / 冒险结算屏——冒险结构实现批)---- + +# 大奖励兑付(非末关通关恰一次):复用 _inter_overlay(label + 确认按钮,零新增交互范式); +# 文案装配归 Narrator +func _on_big_reward(reward: Dictionary) -> void: + _inter_label.text = Narrator.big_reward_text(reward) + _inter_overlay.visible = true + _refresh_presentations() + + +# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事进文案 +func _on_adventure_finished(summary: Dictionary) -> void: + _level_active = false + var result: String = String(summary.get("result", "")) + settlement = null + route_overlay.visible = false # 终局兜底(选路口非 Boss 节点才有——防御性收层) + _sync_settlement_visuals() # → 全关 + info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④) + stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) + if result == "completed": + _end_overlay.open_victory(Narrator.adventure_summary_text(summary), + "返回冒险", board_view.get_global_rect()) + else: + _end_overlay.open_defeat(Narrator.adventure_summary_text(summary), + "返回", board_view.get_global_rect()) + logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) + if runner != null: + runner.restore_hero_base() + strip_view.refresh(runner) + _open_modal(_end_overlay) + _refresh_presentations() + + func _on_battle_end_continue() -> void: - # 终局出口:返回开始界面(切片无下一关数据面——NodeOverlay/关卡结算屏归相邻域 spec) + # 终局出口:返回开始界面(冒险摘要留档试玩日志——钥石持久化归存档/元进度批) _close_modal(_end_overlay) _end_overlay.close() _debug.reset() @@ -289,6 +365,8 @@ func _on_battle_end_continue() -> void: runner.restore_hero_base() runner = null settlement = null + adventure = null + _adventure_mode = false tune_panel.reset_sliders() strip_view.refresh(null) _start_overlay.visible = true @@ -328,6 +406,11 @@ func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void: func _on_inter_next_pressed() -> void: _inter_overlay.visible = false + if _adventure_mode: + # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) + if adventure != null and not adventure.continue_to_next_level(): + _fatal_screen("关卡推进失败——见输出日志") + return if not _debug.next_layer(): _fatal_screen("战斗启动失败——见输出日志") diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd index 709a02dd..287581b0 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/ui_assembler.gd @@ -120,11 +120,12 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary: seed_edit.custom_minimum_size = Vector2(120, 0) seed_row.add_child(seed_edit) - var lvbtn := UiKit.button("▶ 第一关 · 地精洞窟(战×2 → 奖励点 → 战×2 → 恢复 → Boss)") + # 冒险结构实现批(2026-09-21):lvbtn = 正式「开始冒险」入口(三关序列);L2/L3 保留测试直入口 + var lvbtn := UiKit.button("▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)") box.add_child(lvbtn) - var lv2btn := UiKit.button("▶ 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)") + var lv2btn := UiKit.button("▶ 测试直入口 · 第二关 · 幽暗地域(分支节点图试玩——岔路口选下一跳)") box.add_child(lv2btn) - var lv3btn := UiKit.button("▶ 第三关 · 灰矮人矿城(测试直入口——基线红档属预期,配合调整板加成试玩)") + var lv3btn := UiKit.button("▶ 测试直入口 · 第三关 · 灰矮人矿城(基线红档属预期,配合调整板加成试玩)") box.add_child(lv3btn) var tunebtn := UiKit.button("测试用 · 角色数值调整板(模拟天赋树属性提升)") box.add_child(tunebtn) diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd index 2f452b24..6240c816 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/level_autoplay.gd @@ -11,6 +11,10 @@ # 2026-09-17 分支节点图批:选路适配(零自动推进)——兑付确认/战斗胜利后 runner 进选路态, # 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大—— # 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。 +# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、 +# 大奖励立即确认 continue_to_next_level、adventure_finished 置 result/summary;外层每关重取 +# runner 实例经 _drive_level 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与 +# 返回结构零变化,行为逐位不变(MC 同种子对照闸承载)。 # 运行前提:db 已 load_tables;每个 run_level 全新 RngService(同种子 ⇒ 全程逐步可回放)。 # 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。 extends RefCounted @@ -46,6 +50,22 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: if own_session: logger.close_session() return st + _drive_level(runner, st, logger, policy, route) + if st.result == "": + st.result = "timeout" + st.timeout = true + for v in runner.visited_ids(): + st.visited_ids.append(int(v)) + runner.restore_hero_base() + st.log_path = logger.log_path() + if own_session: + logger.close_session() + return st + + +# 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle) +static func _drive_level(runner, st: Dictionary, logger, policy: String, route: String) -> void: + st["policy"] = policy var guard_battles := 20 var guard_actions := 200 while st.result == "" and guard_battles > 0 and guard_actions > 0: @@ -60,18 +80,110 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: if this_settlement == null: break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御 _autoplay_battle(runner, this_settlement, st, logger) + + +# 三关连探冒险驱动(冒险结构实现批 2026-09-21)——AdventureRunner 编排:主种子恰一次 +# (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。 +# 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events / +# visited_ids(全冒险节点行进序列——跨关累计,非末关 runner 的 visited 随实例更替,此处为唯一全量面)。 +static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, + logger = null, policy: String = "normal", route: String = "min") -> Dictionary: + var own_session := logger == null + if own_session: + logger = load("res://presentation/playtest_logger.gd").new() + logger.start_session(seed_val) + logger.ap_per_round = int(db.action_economy.ap_per_round) + logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route}) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(seed_val) + var adr = load("res://core/adventure_runner.gd").new(db, rng) + var st := { + "result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, + "rest_healed": 0, "hp_before_rest": -1, + "defeat_node_index": -1, "defeat_node_type": "", + "boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, + "log_path": "", "policy": policy, "refreshes": 0, "mine_hits": 0, + "visited_ids": [], "choices": 0, + "summary": {}, "wallet_events": [], "encounter_max_hp_seq": [], "reward_events": [], + "shop_gp_seq": [], # 商店房到达余额序(逐关 1 次——GP 跨关延续验收取数面) + } + adr.step_row.connect(_on_step_row.bind(adr, st, collect_rows)) + adr.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger)) + adr.wallet_changed.connect(_on_adventure_wallet.bind(st, logger)) + adr.big_reward_paid.connect(_on_adventure_reward.bind(st, adr)) + adr.adventure_finished.connect(_on_adventure_finished.bind(st, logger)) + adr.set_hero_override(atk_pct, hp_pct) + if not adr.start_adventure(seed_val): + st.result = "start_failed" + if own_session: + logger.close_session() + return st + var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由 big_reward 确认与 runner 重取承载) + while st.result == "" and guard_drives > 0: + guard_drives -= 1 + _drive_level(adr.runner(), st, logger, policy, route) if st.result == "": st.result = "timeout" st.timeout = true - for v in runner.visited_ids(): - st.visited_ids.append(int(v)) - runner.restore_hero_base() + if adr.runner() != null: + adr.runner().restore_hero_base() st.log_path = logger.log_path() if own_session: logger.close_session() return st +# 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取; +# encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典, +# 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕) +static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr, st: Dictionary, logger) -> void: + var r = adr.runner() + var t := String(node.get("type", "")) + st.visited_ids.append(int(r.node_id_of(node))) + if t == "battle" or t == "boss": + st.battles += 1 + st.encounter_max_hp_seq.append(int(r.effective_max_hp())) + if logger != null and r.current_battle() != null: + logger.attach_battle(r.current_battle()) + if t == "boss": + st.boss_depth = int(node.get("node_depth", -1)) + st.hp_at_boss = int(adr.ledger["hero_hp"]) + elif t == "reward": + r.continue_after_node() # 奖励点 = 内容待拍板直通节点(格面资源批 2026-09-15) + elif t == "rest": + st.rest_healed = int(info.get("healed", 0)) + st.hp_before_rest = int(info.get("hp", 0)) - st.rest_healed + r.continue_after_node() + elif t == "shop": + st.shop_gp_seq.append(int(info.get("gp", -1))) + r.continue_after_node() # 商店房零购离开(自动路径直通 Boss——购买行为归冒烟专项/真人试玩) + if logger != null: + logger.log_node(node, info, r.node_index) + + +# 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发) +static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void: + st.wallet_events.append({"event": String(event), "gp": int(gp_val), + "amount": int(extra.get("amount", 0)), "product": String(extra.get("product", ""))}) + if logger != null: + logger.log_wallet(event, gp_val, extra) + + +# 大奖励立即确认(非末关通关 → continue_to_next_level 进下一关——自动路径无大奖励屏停点) +static func _on_adventure_reward(reward: Dictionary, st: Dictionary, adr) -> void: + st.reward_events.append(reward.duplicate(true)) + if not adr.continue_to_next_level(): + st.result = "advance_failed" + + +# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛) +static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void: + st.summary = summary.duplicate(true) + st.result = String(summary.get("result", "")) + if logger != null: + logger.log_level_end(summary) + + # 路径轴策略(确定性——候选编号最小/最大;min 侧 = 岔路恒取小编号支)。 # 编号取 runner.next_candidate_ids()(编号空间解析单一出口——单链下标 / 图 node_depth) static func _route_pick(runner, route: String) -> int: diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index b1162fe7..c1df4902 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -98,6 +98,248 @@ func _run_core_smoke(db, rng) -> void: _smoke_shop_room() _smoke_shop_graph() _smoke_nine_types_gp_stream() + _smoke_adventure_table() + _smoke_adventure_api() + _smoke_adventure() + + +# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)---- + +# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸) +func _smoke_adventure_table() -> void: + print("--- 冒险表(AdventureTable 建表——大奖励/钥石行值单点)---") + check(_db.adventure != null, "db.adventure 已加载(adventure.tres 入 manifest)") + if _db.adventure == null: + return + check(_db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(两字段域检在案)") + var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus) + var ks: int = int(_db.adventure.keystone_per_level) + check(hp_bonus >= 0 and hp_bonus <= 999, "level_clear_hp_bonus ∈ [0,999](%d)" % hp_bonus) + check(ks >= 0 and ks <= 999, "keystone_per_level ∈ [0,999](%d)" % ks) + + +# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 放弃两态) +func _smoke_adventure_api() -> void: + print("--- 冒险结构 API 闸(重复开始拒绝 · 放弃两态受理)---") + var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(42)) + adr.set_hero_override(400, 200) # 首战速胜档(放弃的非战斗受理要走到选路态) + check(adr.start_adventure(42), "冒险启动受理(主种子注入恰一次——state=running)") + check(not adr.start_adventure(43), "进行中重复 start_adventure 拒绝(重开须待结算完毕)") + check(adr.runner().current_battle() != null, "首战在飞(接口①开始关卡直达战斗节点)") + check(not adr.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)") + # 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理 + var autoplay = load("res://smoke/level_autoplay.gd") + var battle = adr.runner().current_battle() + var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"} + var guard := 30 + while adr.runner().current_battle() == battle and guard > 0: + guard -= 1 + autoplay._autoplay_battle(adr.runner(), battle, st, null) + check(adr.runner().current_battle() == null and adr.runner().awaiting_choice(), + "首战胜后选路态(非战斗期——current_battle 置空)") + check(adr.abandon(), "非战斗期 abandon 受理(选路态——同失败型结算)") + var sum: Dictionary = adr.summary + check(String(sum.get("result", "")) == "abandoned" and not bool(sum.get("cleared", true)), + "放弃结算:result=abandoned · cleared=false(已获面不追回——钥石累计 %d)" % int(sum.get("keystone_total", -1))) + check(not adr.abandon(), "结算后重复 abandon 拒绝(结算受理只算一次——state=idle)") + + +# ③ 冒险验收断言(映射 冒险结构.md 验收标准——表覆盖/满配通关/种子复现/大奖励/败北线/ +# 重复拒绝/GP 跨关延续/加算区同账/恢复数学/放弃) +func _smoke_adventure() -> void: + print("--- 冒险结构验收(三关序列 · 大奖励 · 摘要五件事 · 账本封闭)---") + var autoplay = load("res://smoke/level_autoplay.gd") + # ① 表与覆盖义务(序列关数 = 行集关数;行集内每个战斗/Boss 节点跨表精确行——覆盖义务断言) + check(int(_db.level.rows.size()) == 3, "序列关数 = 行集关数(三关正式序列生效)") + var bg_keys := {} + for row in _db.board_gen.rows: + bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + var en_keys := {} + for row in _db.enemy.rows: + en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + var miss := 0 + for row in _db.level.rows: + for n in row.nodes: + var nt := String(n.get("type", "")) + if nt != "battle" and nt != "boss": + continue + var key := "%d_%d" % [int(row.level_index), int(n.get("node_depth", -1))] + if not bg_keys.has(key) or not en_keys.has(key): + miss += 1 + check(miss == 0, "行集内每个战斗/Boss 节点在 board_gen + enemy 有精确行(缺失 %d)" % miss) + # ② 满配通关 + ④ 大奖励兑付(恰 2 次 · 读数=表值 · 回满=新上限 · 覆盖值通道传播 L2/L3 首战上限) + var st: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + var sum: Dictionary = st["summary"] + check(String(st["result"]) == "completed" and bool(sum.get("cleared", false)), + "满配通关(42 / atk+200 / hp+100——cleared=true)") + check(int(sum.get("levels_cleared", -1)) == 3, "通关几关 = 3(levels_cleared)") + check(int(sum.get("keystone_total", -1)) == 3 * int(_db.adventure.keystone_per_level), + "钥石合计 = 3 × keystone_per_level(%d)" % int(sum.get("keystone_total", -1))) + check(int(sum.get("master_seed", -1)) == 42, "摘要携带主种子(42)") + var rewards: Array = st["reward_events"] + check(rewards.size() == 2, "大奖励恰兑付 2 次(L1/L2 后——末关不发上限提升)") + var hp_bonus: int = int(_db.adventure.level_clear_hp_bonus) + var reward_ok: bool = rewards.size() == 2 + if reward_ok: + for i in rewards.size(): + var r: Dictionary = rewards[i] + if int(r.get("hp_bonus", -1)) != hp_bonus or int(r.get("from_level", -1)) != i + 1 \ + or int(r.get("new_max_hp", -1)) != int(_db.hero.max_hp) + hp_bonus * (i + 1) \ + or int(r.get("keystone_gained", -1)) != int(_db.adventure.keystone_per_level): + reward_ok = false + check(reward_ok, "大奖励读数:hp_bonus=表值 · 新上限累加 · 回满=新上限 · 钥石逐关注账") + var seq: Array = st["encounter_max_hp_seq"] + var k := 2.0 # hp+100 ⇒ TunePanel 百分比后乘 ×2(加算先加、百分比后乘——生效次序口径) + var exp_base := maxi(1, int(round(float(int(_db.hero.max_hp)) * k))) + var exp_l2 := maxi(1, int(round(float(int(_db.hero.max_hp) + hp_bonus) * k))) + var exp_l3 := maxi(1, int(round(float(int(_db.hero.max_hp) + 2 * hp_bonus) * k))) + var idx_l2 := seq.find(exp_l2) + var idx_l3 := seq.find(exp_l3) + check(seq.size() > 0 and int(seq[0]) == exp_base, + "L1 首战上限入参 = 基础表值(%d——加算区冒险开始全 0)" % exp_base) + check(idx_l2 > 0 and idx_l3 > idx_l2, + "L2/L3 首战上限入参 = 基础+加算(%d→%d——大奖励经覆盖值通道传播)" % [exp_l2, exp_l3]) + # ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列逐位一致) + var st2: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq + and st2["visited_ids"] == st["visited_ids"], + "同种子两局逐位可复现(summary/上限序列/行进序列——主种子单点派生)") + # ⑤ 败北线(零配 L1 败北 + 半配 L2 败北——种子扫描,仿商店房 range(42,92) 先例) + var lose := {} + for s in range(42, 92): + var stl: Dictionary = autoplay.run_adventure(_db, s, 0, 0) + if String(stl["result"]) == "failed" and int(stl["summary"].get("stopped_at", -1)) == 1: + lose = {"s": s, "st": stl} + break + check(not lose.is_empty(), "零配 L1 败北种子可达(seed=%s)" % str(lose.get("s", -1))) + if not lose.is_empty(): + var sl: Dictionary = lose["st"]["summary"] + check(not bool(sl["cleared"]) and int(sl["levels_cleared"]) == 0 \ + and int(sl["keystone_total"]) == 0 and int(sl["stopped_at"]) == 1, + "L1 败北摘要:未打穿 · 通关 0 关 · 钥石 0 · 止步第 1 层") + var half := {} + for s in range(42, 142): + var sth: Dictionary = autoplay.run_adventure(_db, s, 50, 0) + if String(sth["result"]) == "failed" and int(sth["summary"].get("stopped_at", -1)) == 2: + half = {"s": s, "st": sth} + break + check(not half.is_empty(), "半配(atk+50)L2 败北种子可达(seed=%s)" % str(half.get("s", -1))) + if not half.is_empty(): + var sh: Dictionary = half["st"]["summary"] + check(not bool(sh["cleared"]) and int(sh["levels_cleared"]) == 1 \ + and int(sh["keystone_total"]) == int(_db.adventure.keystone_per_level) \ + and int(sh["stopped_at"]) == 2, + "L2 败北摘要:L1 已过(钥石 1×表值)· 止步第 2 层") + # ⑥ 重复宣布拒绝(settle 后再调 _on_level_finished——摘要零变化) + var adr6 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(7)) + adr6.set_hero_override(400, 200) + check(adr6.start_adventure(7), "重复拒绝场景:冒险启动(seed 7)") + var b6 = adr6.runner().current_battle() + var st6 := _adv_st() + var g6 := 30 + while adr6.runner().current_battle() == b6 and g6 > 0: + g6 -= 1 + autoplay._autoplay_battle(adr6.runner(), b6, st6, null) + check(adr6.abandon(), "重复拒绝场景就位:选路态放弃 → 已结算") + var sum6_before: Dictionary = adr6.summary.duplicate(true) + adr6._on_level_finished({"result": "victory", "hp": 10, "node_index": 1, "node_id": 1, "node_type": "battle"}) + check(adr6.summary == sum6_before, "重复关卡结束宣布拒绝(账本与摘要零变化——契约 7)") + # ⑦ GP 跨关延续(L1 收束无 clear · 每战入账恰一次 · L2 商店房余额 > 0 · 冒险收束 clear 恰一次为末事件) + var wallet: Array = st["wallet_events"] + var clears := 0 + var gains := 0 + for ev in wallet: + if String(ev["event"]) == "clear": + clears += 1 + elif String(ev["event"]) == "gain": + gains += 1 + check(gains == int(st["battles"]), "每战完成入账恰一次(gain %d = battles %d)" % [gains, int(st["battles"])]) + check(clears == 1 and wallet.size() > 0 and String(wallet[wallet.size() - 1]["event"]) == "clear", + "钱包 clear 恰一次且为末事件(关卡收束不清——GP 冒险收束清零归冒险结构)") + var shop_seq: Array = st["shop_gp_seq"] + check(shop_seq.size() == 2 and int(shop_seq[0]) > 0, + "L2 商店房到达余额 > 0(%d——L1 积累跨关延续;两关商店房必经各 1)" % (shop_seq[0] if shop_seq.size() > 0 else -1)) + # ⑧ 加算区同账(手工场景:L2 商店购买 → 大奖励直调——同一本账累加 + 回满新上限) + var same := {} + for s in range(42, 92): + var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s)) + a8.set_hero_override(400, 200) + if not a8.start_adventure(s): + continue + if _adv_drive_to_shop(a8, _adv_st(), autoplay): + same = {"a": a8, "s": s} + break + check(not same.is_empty(), "同账场景种子可达(seed=%s——L2 商店房就位)" % str(same.get("s", -1))) + if not same.is_empty(): + var a8 = same["a"] + var r8 = a8.runner() + var gp0: int = int(r8.gp) + check(gp0 >= 4, "进店余额 ≥ hp_up L2 首价 4(%d)" % gp0) + check(r8.buy_product("hp_up"), "购买 hp_up 受理(出账 + 加算区)") + check(int(a8.bonus.get("max_hp", -1)) == int(_db.adventure.level_clear_hp_bonus) + 3, + "统一冒险加算区含商店值(max_hp = %d[L1 大奖励] + 3[商店]——同一本账前半)" % int(_db.adventure.level_clear_hp_bonus)) + a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index, + "node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟大奖励(victory 载荷直调——L2 非末关) + check(int(a8.bonus.get("max_hp", -1)) == 2 * int(_db.adventure.level_clear_hp_bonus) + 3, + "大奖励与商店购买同账累加(max_hp = %d + 3 + %d)" % [int(_db.adventure.level_clear_hp_bonus), int(_db.adventure.level_clear_hp_bonus)]) + check(int(a8.ledger["hero_hp"]) == int(_db.hero.max_hp) + int(a8.bonus["max_hp"]), + "血回满 = 新上限(%d/%d)" % [int(a8.ledger["hero_hp"]), int(_db.hero.max_hp) + int(a8.bonus["max_hp"])]) + # ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致) + var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(99)) + check(int(a9.apply_rest(30, 70, 100)) == 30 and int(a9.apply_rest(30, 95, 100)) == 5 \ + and int(a9.apply_rest(30, 100, 100)) == 0, + "恢复点数学:floor(100×30%)=30 · 封顶缺口 5 · 满血回 0 合法") + var lr9 = load("res://core/level_runner.gd").new(_db, _fresh_rng(99)) + check(int(lr9.rest_heal_amount(30, 70, 100)) == 30 and int(lr9.rest_heal_amount(30, 95, 100)) == 5, + "同一公式两个落点逐位一致(LevelRunner.rest_heal_amount 内置默认)") + # ⑩ 放弃(战斗中拒绝 · 非战斗期受理同失败型结算) + var a10 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(15)) + a10.set_hero_override(400, 200) + check(a10.start_adventure(15), "放弃场景:冒险启动(seed 15——首战在飞)") + check(not a10.abandon(), "战斗中 abandon 拒绝(战斗域单事务在飞)") + var b10 = a10.runner().current_battle() + var st10 := _adv_st() + var g10 := 30 + while a10.runner().current_battle() == b10 and g10 > 0: + g10 -= 1 + autoplay._autoplay_battle(a10.runner(), b10, st10, null) + check(a10.runner().current_battle() == null and a10.abandon(), "非战斗期(选路态)abandon 受理") + var s10: Dictionary = a10.summary + check(String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)), + "放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)") + + +# 手工场景驱动:L1 全程 + L2 推进到商店房(_drive_level 复用 + 兑付确认/选路/大奖励确认手工编排) +func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool: + var guard := 24 + while guard > 0 and String(st["result"]) == "": + guard -= 1 + autoplay._drive_level(a.runner(), st, null, "normal", "min") + if String(st["result"]) != "": + return false + if a._pending_next_level: + a.continue_to_next_level() + continue + var r = a.runner() + if String(r.current_node().get("type", "")) == "shop": + return true + if r.awaiting_choice(): + var ids: Array = r.next_candidate_ids() + ids.sort() + r.pick_next_node(int(ids[0])) + continue + var t := String(r.current_node().get("type", "")) + if t == "reward" or t == "rest": + r.continue_after_node() + return false + + +# 手工场景状态袋(_drive_level/_autoplay_battle 消费的最小字段族——run_adventure st 的同构子集) +func _adv_st() -> Dictionary: + return {"result": "", "hp_left": 0, "battles": 0, "rounds": 0, "hero_dmg": 0, + "rest_healed": 0, "hp_before_rest": -1, "defeat_node_index": -1, "defeat_node_type": "", + "boss_depth": -1, "hp_at_boss": -1, "rows": [], "timeout": false, "log_path": "", + "policy": "normal", "refreshes": 0, "mine_hits": 0, "visited_ids": [], "choices": 0} # ---- 战斗屏棋盘布局·格边长公式回归(2026-09-19 真人试玩 bug⑥:L2 12×12 板下缘被裁约 1/6 格高—— From 5709cd33184768cd6fd15ca5107e467979195538 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 04:45:52 +0800 Subject: [PATCH 4/9] =?UTF-8?q?docs(design):=20=E5=86=92=E9=99=A9=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=AE=9E=E7=8E=B0=E6=89=B9=E6=96=87=E6=A1=A3=E6=89=B9?= =?UTF-8?q?=E2=80=94=E2=80=94registry=E4=B8=89=E5=A4=84/03=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E8=A1=8C/06=E4=B8=89=E6=9D=A1=E7=9B=AE=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=E7=A7=BB=E9=99=A4/=E5=86=92=E9=99=A9=E7=BB=93?= =?UTF-8?q?=E6=9E=84v0.6/roadmap=E6=97=B6=E9=97=B4=E7=BA=BF/README?= =?UTF-8?q?=E8=AF=95=E7=8E=A9=E6=8C=87=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/03-内容规模.md | 11 +++++----- design/gdd/06-待办清单.md | 28 ------------------------- design/gdd/systems/冒险循环/冒险结构.md | 1 + design/registry/entities.yaml | 7 ++++--- production/roadmap.md | 6 ++++-- 5 files changed, 15 insertions(+), 38 deletions(-) diff --git a/design/gdd/03-内容规模.md b/design/gdd/03-内容规模.md index ee749a43..1e86c3b2 100644 --- a/design/gdd/03-内容规模.md +++ b/design/gdd/03-内容规模.md @@ -6,13 +6,13 @@ ## 关卡 -**三关已落地**,共用一条分支节点链模板(第二层幽暗地域随分支节点图批落地,第三层灰矮人矿城随第三关内容批落地;正式序列推进的收尾待办见[待办清单](06-待办清单.md)): +**三关已落地**,共用一条分支节点链模板(第二层幽暗地域随分支节点图批落地,第三层灰矮人矿城随第三关内容批落地;三关正式序列已随冒险结构实现批生效——首页「开始冒险」连探 L1→L2→L3): | 层 | 层名 | 结构 | 状态 | |---|---|---|---| -| 第一层 | 地精洞窟 | 分支节点图(战斗 / 随机道具 / 恢复点 / Boss 等节点型) | 已落地 | -| 第二层 | 幽暗地域 | 同一分支节点图模板复用,第二关深度轴行值填充 | 已落地(正式序列推进待收尾) | -| 第三层 | 灰矮人矿城 | 同一分支节点图模板复用,第三关深度轴行值填充 | 已落地(测试直入口;正式序列推进待收尾) | +| 第一层 | 地精洞窟 | 分支节点图(战斗 / 随机道具 / 恢复点 / Boss 等节点型) | 已落地(正式序列首关) | +| 第二层 | 幽暗地域 | 同一分支节点图模板复用,第二关深度轴行值填充 | 已落地(正式序列已生效) | +| 第三层 | 灰矮人矿城 | 同一分支节点图模板复用,第三关深度轴行值填充 | 已落地(正式序列已生效;测试直入口保留) | 现行节点行集与行值读 `data/tables/level.tres`(registry 为准)。再扩关卡走扩展批流程:先填深度轴行值、再接正式序列推进,不新造节点链结构。 @@ -56,7 +56,7 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。 垂直切片阶段明确砍掉或推迟的内容,写在这里防止回潮: -- 更多关卡(N>3)——第三关已按扩展批流程落地(行集 + 测试直入口);正式序列推进仍待 Alpha +- 更多关卡(N>3)——第三关已按扩展批流程落地(行集 + 测试直入口);三关正式序列已生效(冒险结构实现批 2026-09-21) - 精英节点与词缀系统——只预留了字段位,不定义语义 - 地形变异扩展内容——首批四类型已落地,反制道具与全局作用域排 Alpha(2026-09-20 用户拍板) - 道具/增益之间的组合协同(遗物式联动) @@ -78,3 +78,4 @@ MVP 不加新状态;扩容走 status_effects.tres 加行 + registry 登记。 | v0.5 | 2026-09-20 | 层名二改批:「裂颚洞窟」→「地精洞窟」(用户拍板——不绑定 DND 专名部族、规避 IP 指涉,直取地精巢穴意象;v0.4 出典随之作废仅留档)。全仓 8 处消费面随批同步 | | v0.6 | 2026-09-20 | **第三关内容批(灰矮人矿城——与切片代码 / registry / 难度曲线与Boss / 棋盘生成 / 地形变异 同批)**:层名表加第三层(测试直入口已落地、正式序列推进待收尾——正式三关序列推进仍登记 Alpha 验收条件);棋盘尺寸条款由两档改按关配置(第三关 13×13 / Boss 14×14 随批铺行);棋盘节地形变异句随批补第三关一对(落石/矿尘);暂不做「更多关卡」行更新为已按扩展批流程落地。行值读 `data/tables/level.tres` / `board_gen.tres` / `variation.tres`(registry 为准) | | v0.7 | 2026-09-20 | **GP+商店房批(与切片代码 / registry / 关卡节点地图 / 冒险结构 / 战斗结算 / 地图战斗资源 / 难度曲线与Boss 同批)**:格面资源池"八型 × 正负两向(十六格)"→"九型 × 正负两向(十八格)"(+gp 金币);新增「商店」小节一行——shop.tres 读表纪律(商品三行 / 掉落参数 / 价格参数,全部 provisional 归曲线调参批);一句话定位句补"一套商店配置" | +| v0.8 | 2026-09-21 | **冒险结构实现批(与切片代码 / registry / 冒险结构 同批)**:层名表 L2/L3 状态行改「正式序列已生效」——首页「开始冒险」三关连探落地(AdventureRunner 编排;测试直入口保留);「更多关卡」行同步;正文零历史叙述纪律下仅状态口径更新,结构框架零改动 | diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 18375fdb..e5f0e733 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -205,25 +205,6 @@ **状态**:已拍板登记(2026-09-18),待 Alpha 内容批执行 -#### 切片行进代码与定稿的接口漂移 - -**现状**:切片行进器(LevelRunner)为单关 provisional 实现,与关卡节点地图 v1.0 定稿存在接口漂移,逐条登记:① `start_level` 载荷含种子并自开随机流(`rng.new_adventure`)——定稿:种子注入与随机流域归冒险结构,开始关卡载荷 = 关卡序号 + 当前生命 + 生效上限;② 主角生命在行进器本地持有——定稿:生命账本归冒险结构;③ 恢复兑付为行进器自扣(`_apply_rest`)——定稿:发治疗封顶回报、由冒险结构执行回血封顶;④ 战斗胜利自动推进(`_enter_next_node` 直调)——定稿:全节点确认 + 兑付后确认、零自动推进;⑤ `continue_after_node` 仅对奖励/恢复生效——定稿:泛化为全节点行进确认与选路动作;⑥ 收束点未编排"先调战斗结算关卡收束清库"步——定稿:先清库后宣布为强制次序;⑦ 战斗败北直接终关卡——定稿:每战结果上报冒险结构,倒下的失败结算归冒险结构;⑧ 节点图结构(节点 + 连线)与图合法性校验未实现——单链数组为特例可先行,图版随内容批。 - -**归属**:切片图版实现批(随第二关内容批 / 关卡节点地图 P1「分支节点图内容与实现批」) - -**状态**:部分清偿(2026-09-17 分支节点图批)——④⑤⑧ 三条已清偿:战斗胜利改进选路态(`choices_presented`,零自动推进);`continue_after_node` 泛化为兑付确认(确认 ≠ 推进,选路动作归 `pick_next_node`);节点图结构 + 图合法性校验已落地(节点带 `next` 出边表,编号 = `node_depth`,编号唯一 / 入口唯一 / Boss 恒唯一终点 / 前进方向无环 / 双向可达六检,违规加载致命,L1 单链经同一套校验)。①②③⑥⑦ 属正式侧冒险结构接口,保持登记。 - - - - -#### 两关序列未生效 - -**现状**:第二关深度轴行值填充批落地前,两关序列未生效,冒险以单关运行(合法态;扩展批已排 P1)。生效前提(第二关行值/图版填充)已于 2026-09-17 分支节点图批满足。注记(2026-09-17 分支节点图批):切片首页已加"关卡 2 试玩"直入口(测试模式 `start_level(2)`,与序列推进无关)——行值/图版前提已满足后,正式序列推进(关卡 1 通关接续关卡 2)仍未实现,本条保持登记。 - -**归属**:难度曲线与Boss(随第二关内容批);Alpha 验收条件登记 - -**状态**:待办(2026-09-19 用户拍板:垂直切片期可接受现状——测试直入口进关 2;**列入 Alpha 版本验收条件**,正式序列推进与切片行进器接口漂移 ①②③⑥⑦ 的对齐随 Alpha / 正式侧冒险结构批执行) - #### 大奖励数值随难度曲线联动 **现状**:上限提升与恢复比例同通关率带、换算率锚的联动复验,随难度曲线与Boss的调参批执行 @@ -328,15 +309,6 @@ **状态**:待办(档位已定案,条款修订随表现层升级批) -### 数据配置 - -#### 冒险表未建表(原「关卡表与冒险表未入表清单」关卡表半边已收口) - -**现状**:关卡表半边已收口(2026-09-18 清理批核销:level.tres + LevelTable schema + config_db 加载行齐备,切片加载清单在册);冒险表 `AdventureTable` 仍未建——registry 已登记字段(keystone_per_level,单行全局,Rule 4 豁免)且注记「未建表零代码影响」,切片无消费方(元进度未实现),建账归冒险结构定稿批 - -**归属**:冒险结构定稿批(随批建账) - -**状态**:待办(仅剩冒险表半边) ### 存档序列化 #### 存档契约条目的登记方式 diff --git a/design/gdd/systems/冒险循环/冒险结构.md b/design/gdd/systems/冒险循环/冒险结构.md index 4f4cf1f6..0ec383d8 100644 --- a/design/gdd/systems/冒险循环/冒险结构.md +++ b/design/gdd/systems/冒险循环/冒险结构.md @@ -284,3 +284,4 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 | v0.3 | 2026-09-17 | 代币→钥石统一拍板(用户裁决):大奖励第三件「代币记账 +10」改「钥石入账 +10」——与元进度钥石同一资源(渠道 = 关卡大奖励入账与 Boss 掉落),「代币」措辞退役;调参表字段名、验收标准、依赖表同步。数值与机制不变 | | v0.4 | 2026-09-17 | 数值纪律收口批:正文与调参表 knob 现行值改指路存根(事实源 data/tables/*.tres 与 registry),3 处失同步修正(示例轨迹改纯符号〔旧 50/50 生命轴示例作废〕、大奖励 +10/+10 改读 adventure_table 行值并删换算率数字锚、调参表 30/10/10 三格改读 level.tres / adventure_table) | | v0.5 | 2026-09-20 | **GP+商店房批(用户拍板——与关卡节点地图 v1.1 / 战斗结算 v0.23 / 地图战斗资源 v0.15 / 难度曲线与Boss v0.18 / registry / 切片同批)**:① 新增「GP 冒险账本」节——钱包三收口(战斗完成统一入账 / 商店购买出账 / 冒险收束清零)+ 战斗内缓冲隔离语义(格面兑付入缓冲、战斗完成统一到账、战斗域零钱包状态);② 统一冒险加算区成文——商店购买与关卡大奖励上限提升同一本账(三轴 {攻击, 生命上限, 护甲等级},经覆盖值通道生效;购血 = 上限与当前生命同步 +Δ);③ GP 掉落掷定消费 gp_drop 封闭随机流(主种子派生)登记进主种子节与调参表;④ GP 不入存档、不入冒险摘要;范围表 / 大奖励表 / 依赖表 / 验收标准同步。数值全 provisional-pending-曲线调参批,行值读 shop.tres | +| v0.6 | 2026-09-21 | **冒险结构实现批落地登记(切片 AdventureRunner + LevelRunner 账本外置 + AdventureTable 建表——与切片代码 / registry / 03 / 06 / roadmap 同批)**:本批只记实现批状态,正文规则口径零改动。落地面 = 三关正式序列生效(首页「开始冒险」)、接口契约四件套正式侧承载(开始关卡 start_level_external / 战斗结果上报共享账本直写 / 治疗封顶回报 apply_rest / 关卡结束 level_finished)、生命账本与 GP 冒险账本上交冒险结构层、大奖励非末关恰兑付一次(末关钥石照记)、冒险摘要五件事产出、主种子恰一次注入(registry rng_service new_adventure 调用权同批登记)、结算受理只算一次;06 待办「两关序列未生效」「切片行进代码与定稿的接口漂移」「冒险表未建表」三条随批解决移除。**规划书冲突按代码为准登记两处**:①大奖励 `_pending_next_level` 置位先于 emit(信号同步派发——处理器内立即确认需标记已就位);②放弃受理补 state=running 守卫(契约 7 结算受理只算一次的 abandoned 侧对齐) | diff --git a/design/registry/entities.yaml b/design/registry/entities.yaml index 4dcfc856..d9bb2b1a 100644 --- a/design/registry/entities.yaml +++ b/design/registry/entities.yaml @@ -23,6 +23,7 @@ entities: streams: [board_gen, reward_roll, encounter, armor_roll, behavior_roll, variant_place, gp_drop] stream_names: 当前封闭清单(增流需改代码 + P1 支柱评审;combat_var 已于 2026-09-07 full 评审删除——玩法随机不预置基础设施;encounter 增流 2026-09-12 用户拍板——遭遇随机化批,scope_event_binding = 每次战斗实例化恰一 scope;**armor_roll 增流 2026-09-16 用户拍板**——护甲等级掷点批,回合内随机首次入流:scope_event_binding = 每次掷点恰一 scope〔逐次掷逐次取号、可逐位回放〕,消费面 = 战斗结算 吃掷点分量判定;**behavior_roll 增流 2026-09-16 用户拍板**——敌人技能池批(开轮摇号):回合内随机、时点前置到每轮开轮〔结算步骤流之外〕,scope_event_binding = 每敌每摇号恰一 scope,**派生向量 = spawn_index × 1000000 + 单敌摇号序**〔单敌出招轨迹可独立复放、敌互不穿插;取号门 = open_scope(stream, scope) 显式 scope 取号——计数器仍单调 +1 供观测〕,消费面 = 战斗结算 开轮摇号换算层 + 技能摇号器 SkillLottery;**variant_place 增流 2026-09-20 用户拍板**——地形变异首批类型定稿批:scope_event_binding = 每次装板恰一 scope〔常规换图/轮内刷新图各取号一次〕,消费面 = 战斗结算 布置段 _place_variants——变异类型选取 + 连通片生长取数;**gp_drop 增流 2026-09-20 用户拍板**——GP+商店房批:scope_event_binding = 每场战斗结束的掉落掷定恰一 scope,消费面 = 战斗结算 战斗结束分支——按战斗节点身份查商店表掷定 GP 掉落,掷定 + 关级加值 + 战斗内缓冲合成本战 gp_dropped〔shop_roll 流随"无三选一摇号"裁决作废、从未入册〕) stream_facade: open_stream(stream) 取号(计数器+1→派生→返回已播种实例);derive_at(stream, scope) 纯函数供测试/预览(不推计数器) + new_adventure: 冒险开始重播种入口——调用权 = AdventureRunner.start_adventure 唯一正式调用方(切片单关测试路径 LevelRunner.start_level 除外;冒险结构实现批 2026-09-21——主种子全冒险恰一次注入,冒险内不重置不逐关播种) scope_event_binding: board_gen=每次生成新扫雷图(棋盘生成);reward_roll=每次 board install(格奖励批 + 清图奖励批)(地图战斗资源——2026-09-09 接口漂移改写,原"每次奖励结算 roll 批次";地图战斗资源 Rule 2/AC-1 为契约源);encounter=每次战斗实例化恰一 scope(战斗结算 begin_battle 实例化期、扫雷开始前——unit_count 区间均匀骰,挖掘决策域之外 P1 合规);armor_roll=每次掷点恰一 scope(战斗结算 战斗段吃掷点分量逐次判定——1d20,护甲等级掷点批 2026-09-16);variant_place=每次装板恰一 scope(战斗结算 布置段——地形变异首批类型定稿批 2026-09-20);gp_drop=每场战斗结束的掉落掷定恰一 scope(战斗结算 战斗结束分支——GP+商店房批 2026-09-20) unseeded_access: 致命中止 snapshot_contract: 续玩/复盘快照 = master_seed + 各流 scope 计数器(消费方 存档序列化) @@ -539,17 +540,17 @@ entities: nodes: 节点图结构——节点数组 + 连线(node_kind ∈ {battle, reward, rest, boss, shop},切片字段名 type;+shop 随 2026-09-20 GP+商店房批——商店房兑付 = 打开商店覆盖层、离开后确认回选路,不携带战斗行〔零敌人、零棋盘配置行〕;每节点领关卡内唯一**节点编号**,battle/boss 数据键 node_depth 沿用〔v1.0 语义升级:深度 → 节点编号〕;连线 = 出边节点编号表,只朝前进方向、无回头路);单链特例 = 隐式后继(数组序即链,切片现行形态零漂移) rowset_rule: 图合法性校验(2026-09-17 关卡节点地图 v1.0)——节点编号关卡内唯一、入口唯一、Boss 恒唯一终点(编号最大,Dmax 语义平移)、连线无环、所有节点沿连线可达 Boss,违规加载期致命;实现批(2026-09-17 分支节点图批):校验落 LevelTable.validate(单链形态经同一套算法——编号解析单链 = 数组下标 / 图 = node_depth),L1 单链与 L2 图双行集加载全绿断言;商店房编排(每关恰 1、Boss 前一位必经位)由冒烟断言承载、非加载校验——图合法性六检零新增(GP+商店房批 2026-09-20,关卡节点地图 v1.1) cross_table_validation: battle/boss 节点 (L,D) 在 board_gen/enemy 两表存在精确行(数据配置 跨表校验⑧复用 + 冒险结构 AC-7 冒烟断言——序列关数 ⊆ 内容覆盖,越界关卡 = 内容生产缺口) - mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口试玩 ≠ 两关序列生效,正式序列推进仍登记待办);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口 ≠ 序列生效——正式三关序列推进仍登记 Alpha 验收条件);2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载** + mvp_content: L1 全行(切片转正——7 节点链〔单链特例,合法图形态〕、rest_heal_pct=30);L2 全行已落地(2026-09-17 分支节点图批——首张分支图:9 节点〔入口 0 → 中间三层 2/3/2 → Boss 8〕、真实岔路 4 处、奖励点+恢复点各 1、战斗节点 6 + Boss;行值 provisional 归 难度曲线与Boss v0.7;首页直入口试玩模式保留);**L3 全行已落地(2026-09-20 第三关内容批——「第三层 · 灰矮人矿城」:九节点图复用 L2 同模板〔6 战 + Boss + 奖励 + 恢复、真实岔路 4 处〕,行值全 provisional 归 难度曲线与Boss;测试直入口保留)**;2026-09-20 GP+商店房批 rewiring:L2/L3 图版 Boss 重编号 8→9、各新增 (8, shop, next[9]) 商店房节点(Boss 前一位必经位),L1 零改动,图合法性六检零新增——必经位由冒烟断言承载;**2026-09-21 冒险结构实现批:三关正式序列生效**(首页「开始冒险」= L1→L2→L3 连探,AdventureRunner 编排——大奖励/生命账本/GP 冒险账本/主种子归冒险结构层,冒烟断言在案)** referenced_by: [冒险结构, 关卡节点地图, 难度曲线与Boss, 棋盘生成, 数据配置] - name: adventure_table type: schema_data owner: design/gdd/systems/冒险循环/冒险结构.md attributes: - table: AdventureTable(数据配置 承载,单行全局;冒险结构 回填 owner;Rule 4 豁免——单行全局常量表,HeroTable/SettlementTable 先例;建表走 数据配置 接口漂移规则) + table: AdventureTable(数据配置 承载,单行全局;冒险结构 回填 owner;Rule 4 豁免——单行全局常量表,HeroTable/SettlementTable 先例;建表走 数据配置 接口漂移规则。**已建表**——data/schemas/adventure_table.gd + data/tables/adventure.tres(两字段 10/10)+ ConfigDB 加载行"adventure",冒烟断言在案(冒险结构实现批 2026-09-21)) fields: level_clear_hp_bonus: int [0,999](knob 默认 10——关卡大奖励 HP 上限提升,冒险内生效经 begin_battle 覆盖值通道传入后续每战;值锚 = 难度曲线与Boss 区间平移换算率 +10hp ≈ +1 轮,上调须 难度曲线与Boss 重扫 L2 通关率带) - keystone_per_level: int [0,999](knob 默认 10——每通关一关的钥石入账值;消费方 元进度;2026-09-17 代币→钥石统一拍板改键〔原 token_per_level〕——AdventureTable 未建表,零代码影响) + keystone_per_level: int [0,999](knob 默认 10——每通关一关的钥石入账值;消费方 元进度;2026-09-17 代币→钥石统一拍板改键〔原 token_per_level〕;AdventureTable 已建表——冒险结构实现批 2026-09-21,零代码影响注记随之失效) mvp_content: 两字段默认值即 MVP 行(10 / 10,provisional) extension_reserve: 大奖励形态扩列位(三选一 / 攻击型 / 通关加成——冒险结构 OQ2,随元进度设计) referenced_by: [冒险结构, 元进度, 数据配置] diff --git a/production/roadmap.md b/production/roadmap.md index 4c928326..7919678c 100644 --- a/production/roadmap.md +++ b/production/roadmap.md @@ -16,14 +16,16 @@ | 2026-09-15 | 命名清偿批;格面资源批(五段结算/极性路由/可选类删除);攻击类型与奖励抽象分离;护甲等级登记批;伤害量值区间批 | | 2026-09-16 | 行动点压速批 8→6;护甲等级掷点批落地(smoke 233 全绿);AP6+AC 验收判读口径 v1.0;06-待办清单建账改版;系统更名批;敌人技能池提案对答启动 | | 2026-09-17 | 敌人技能池提案定稿(1c63830);技能池实施批落地(smoke 263 全绿,待收编);魔法道具切片期空内容拍板;关卡节点地图定稿批解锁;跨会话状态持久化批(快照节 + 本文件 + 守卫文案升级) | +| 2026-09-20 | 第三关内容批(灰矮人矿城行集 + 测试直入口,smoke 375 全绿);GP+商店房实施批(九型格面+随机掉落+Boss前商店房+统一加算区,smoke 437 全绿);八型批呈现层漏网修复(真人试玩卡断——行协议呈现层同步闸 11 断言) | +| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿)——存档批(存冒险状态)与元进度批(钥石消费)挂点就位 | ## 路线图(主线路线与门) 1. **技能池实施批(已落地,smoke 263 全绿;待一笔收编提交)**:批 1 核心机制 + 批 2 呈现替代 + 批 3 文档与 registry 已完成;收编提交后本站闭合。设计权威 = `docs/proposal-skill-pool.md`。 2. **验收批**(门 = 技能池批收编 + 工作区净 + smoke 全绿):按 `docs/acceptance-ap6-ac.md` v1.0 跑三数据源(蒙特卡洛双档 / 真人 3–5 局 / 日志分析器),无道具基线;产出判读结论与选档建议。 3. **难度曲线阶段二调参批**(门 = 验收读数):四级阶梯落档 + 三杠杆扫描(血量锚走廊 / 群战上探 / 注入衰减)+ 挖掘风险不等式与清图支配性两对账重跑 + AC 行值投值。 -4. **第二关内容批**(门 = 调参行值):深度轴行值 D1–D4 填充 + 新内容投放(技能池定稿后 = 既有池新技能);填齐后两关序列生效。 -5. **并行 / 后置轨道**:关卡节点地图 GDD 定稿批(已解锁,与在飞批零文件冲突);魔法道具内容批(排切片收尾后);真人通关率标定(挂 playtest 批);元进度定稿(排 Alpha 前收口——资源关系已裁统一钥石〔2026-09-17〕,剩解锁清单/定价与钥石产出细则)。 +4. **深度轴行值填充批(原第二关内容批的行值半边;门 = 调参行值)**:L2/L3 行集已落地、三关正式序列已生效(2026-09-21 冒险结构实现批);剩深度轴行值 D1–D4 投值与新内容投放,归难度曲线阶段二调参批同窗。 +5. **并行 / 后置轨道**:存档序列化批(挂点 = 冒险状态与摘要——冒险结构实现批已就位);元进度批(钥石消费方挂点 = 冒险摘要钥石合计——已就位);魔法道具内容批(排切片收尾后);真人通关率标定(挂 playtest 批,指引见切片 README)。 ## 维护纪律(收口批五步) From 8b92ed8d088751c324ba69f1b3a493b63189f59c Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 04:48:44 +0800 Subject: [PATCH 5/9] =?UTF-8?q?chore(session):=20=E5=86=92=E9=99=A9?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E5=AE=9E=E7=8E=B0=E6=89=B9=E5=BF=AB=E7=85=A7?= =?UTF-8?q?+=E5=8F=B0=E8=B4=A6=20117=EF=BC=9B=E8=A1=A5=E7=99=BB=E5=B9=B6?= =?UTF-8?q?=E8=A1=8C=E6=89=B9=20112-114=20=E4=BC=9A=E8=AF=9D=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- production/session-state/active.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/production/session-state/active.md b/production/session-state/active.md index 78622def..2e8a55f2 100644 --- a/production/session-state/active.md +++ b/production/session-state/active.md @@ -12,8 +12,12 @@ ## 当前状态快照(收口批先更新本节,再追加台账;≤10 行) +- **更新于**:2026-09-21(**冒险结构实现批——三关正式序列生效**,worktree adventure-structure-20260921,台账 117):按用户规划书(.zcode/plans/adventure-structure-batch-plan-2026-09-21.md)T0–T9 执行——切片从三个单关 demo 升级为一局完整三关冒险:AdventureTable 建表(adventure.tres 10/10)+AdventureRunner 新建(主种子恰一次/生命账本/GP 冒险账本/统一加算区/大奖励非末关恰兑付/三结局摘要五件事)+LevelRunner 账本外置(hero_hp/gp 属性化共享账本,接口漂移①②③⑥⑦清偿)+首页「开始冒险」三关连探(大奖励屏+冒险结算屏);06 三条目解决移除(两关序列未生效/接口漂移/冒险表未建表)。代拍板七项按规划书 §7 采行待确认(详见台账),规划书冲突按代码为准三处登记。冒烟 437→482 项 0 失败三闸全过+四探针 PASS;MC 回归逐位一致(旧单关路径零行为变更证明);文档三工具与基线一致 - **更新于**:2026-09-20(**GP+商店房实施批(L3-A 玩家成长批)**,worktree gp-shop-20260920,台账 116):用户六问裁决落地——①GP 进格面第九型(金币袋 weight 2 power 3 低概率惊喜+破财字典负向行,正向合计 76);②掉落=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留)+关级加值(L2 +1/L3 +2),gp_drop 新封闭流(黄金向量已登);③记账=战斗完成统一到账,格面兑付走战斗内缓冲 escrow 与钱包隔离;④商店房 Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9);⑤统一冒险加算区(商店购买与大奖励同账,购血上限当前同步);⑥三商品全陈列无刷新无三选一,价格=基价+关级加成+步长×已购档;数值/分层验收整体后置归曲线调参批(骨架先行)。冒烟 437 项 0 失败+三探针 PASS;GDD 八文档(关卡节点地图 v1.1 五类枚举/冒险结构 v0.5 GP 账本/战斗结算 v0.23/地图战斗资源 v0.15 九型/曲线 v0.18 杠杆登记/元进度 v1.2/03 v0.7/05 红线例外成文)+06 GP 条目收口+「关卡结构随机生成」方向观察登记 - **更新于**:2026-09-20(**第三关内容批 + GP 定稿提案批**,worktree l3-content-20260920,台账 115〔113/114 号让给并行在飞会话两批,顺延让号〕):L3-B 落地——第三层「灰矮人矿城」行集(enemy 七行 duergar/hook_horror/duergar_king + board 13×13/Boss 14×14 词表值 deep_underdark + 九节点图 + 落石/矿尘 + 大地脉动=sapped 首发)+ 关卡 3 测试直入口 + 冒烟 375 项全绿 + registry 五条目 + GDD 五文档(03 v0.6/曲线 v0.17/棋盘生成 v0.4/地形变异 v0.4/06 GP 条目立项提前);板形验算 13/14×14 推理率 0.94–0.96 全达标零降档;MC 读数:零成长基线普通 16%/完美 20%(N=300,死亡 75–80% 集 Boss——预案「必红」预判被棋盘武器库部分证伪,成长批必要性不变)+ 成长代理档 40%/80%;**GP+商店房定稿提案已交付**(proposal-gp-shop-room-2026-09-20.md,10 拍板点:格面池零修订/商店房必经位/三属性 flat 档 攻+1=4 血+3=2 甲+1=4),待用户逐条拍板后进波次 2 实施批 +- **更新于**:2026-09-20(**ComfyUI 出图提示词与规格卡批**,worktree art-prompts-20260920,台账 114):`design/art/comfyui-prompts.md` v1.0 成文——美术清单 ComfyUI 通道执行侧:主角三态+六敌种逐资产提示词卡(风格条款转写 art-bible:亮描边轮廓通道进正负双侧、紫赤 300–345° 禁入带写入全部卡片);兄弟精改荐脚本换色通道;候选入库→裁决→后处理→转正五步流程+验收速查;两份清单各补指路行;纯文档零代码,doc 门禁与基线一致(合并 31d2710) +- **更新于**:2026-09-20(**八型批呈现层漏网修复批〔真人试玩卡断〕**,worktree battlelog-attachfix-20260920,台账 113):八型批伏击退役拆除伤害行 attach 字段后战斗日志视图拆行分支漏网——敌方首笔攻击行格式化即 Invalid access、结算段中途夭折=界面卡断;修复=基线+伏击与三分量两退役拆行分支拆除+smoke 新增行格式化呈现层同步闸 11 断言(行协议 9 类自此有呈现层回归网);冒烟 355→366 项 0 失败三闸全过+四探针全过;纯呈现层零行值零机制改动(合并 931bba7) +- **更新于**:2026-09-20(**地穴全图卷并入 Alpha 魔法道具整体重设计批**,worktree magic-fold-20260920,台账 112):用户拍板「垂直切片阶段不保留魔法道具相关内容」——06「地穴全图卷效果重定」删除并入 Alpha 魔法道具整体重设计(口径同台账 98 显影尘批,清单 44→42 条);顺带清除「零伤害/无威胁组合的跨字段护栏」死条目(台账 100 已裁决删除,八型批合并 3468066 冲突解决误取分支侧复活,本批回归修复);悬空指针三处清偿(魔法道具.md 存根行/03-内容规模 待定句/registry catacomb_scroll 行内措辞);纯文档零代码冒烟不适用,doc 门禁与 master 基线一致 - **更新于**:2026-09-20(**第一层定名「地精洞窟」二改批**,worktree rename-l1-goblin-20260920,台账 111):用户拍板「不用这么 DND,就叫地精洞窟」——终名不绑定 DND 专名部族、规避 IP 指涉;「裂颚」出典作废仅留档,8 处消费面与提案层名行/沿革注记随批同步(03-内容规模 补 v0.5);纯字面变更零行值零机制改动,冒烟 355 项 0 失败 - **更新于**:2026-09-20(**第一层改名「裂颚洞窟」批**,独立 worktree 分支 rename-l1-20260920,台账 110〔登记时撞并行会话 109 号,顺延让号〕):用户拍板「苔藓回廊与地精不契合」作为过期词语全仓退役——新名致敬 DND 5e《凡戴尔的失落矿坑》地精部族「裂颚」(Cragmaw)洞窟巢穴,洞窟纵深连通幽暗地域、与第三关预案「灰矮人矿城」构成三层纵深叙事线;八处面同步(level.tres display_name+表尾批注 / ui_assembler 首页按钮 / 切片 README×2 / 00-游戏概念 / 03-内容规模 v0.4 / registry level_table / 第三关提案层名行+蕈林理由重写+两处旧读数顺手校正);纯字面变更零行值零机制改动,冒烟 355 项 0 失败 - **更新于**:2026-09-20(**地图战斗资源正文历史叙述迁出批**,worktree doc-polish-20260920,台账 109):变更记录归版本历史的纪律成文(正文零历史叙述),极性矩阵节两处旧体系对照句迁出;doc_term_audit 疑似 3→2 @@ -288,5 +292,12 @@ 109. **地图战斗资源正文历史叙述迁出批(2026-09-20,本会话——用户对 doc_term_audit 剩余 3 疑似逐条裁决:①battle ambusher 留档表可接受〔废案存档不影响使用〕、②极性矩阵正文的历史对照句应迁出——变更记录应如 ADR 记于版本历史而非系统描述正文、③06 雾格失义随待办完成自然消解;worktree doc-polish-20260920,合并 3abc2a1,纯文档 1 文件 2 行)**:①极性矩阵节「取代旧体系的四套固定兑付」历史对照句删除(信息已在版本历史 v0.4 全量留档);②节尾「退化特例/撤旗退役变更叙述」删除,改一句纪律成文:**矩阵只描述现行规则,被取代的旧体系与历次结构变更一律记版本历史,不在正文复述**;③验证=gdd_lint 与 master 基线 diff 只减不增(-1 条 R3)、doc_term_audit 疑似现行 3→2(余 2 处=用户判定留档:battle ambusher 留档表、06 雾格失义陈述)。**纪律首例**——本批确立「正文零历史叙述」文档纪律,后续各系统文档的历史对照句按同口径迁移(未排期,遇批顺带) 110. **第一层改名「裂颚洞窟」批(2026-09-20,本会话——用户拍板「苔藓回廊跟地精不太契合……直接把苔藓回廊作为过期词语删除替换掉」,新名授权本会话选定;worktree rename-l1-20260920)**:全仓「苔藓回廊」现行用字清零(8 处消费面全替换,仅 5 处退役审计注记有意保留——版本历史/registry 注记/表尾批注/提案沿革,符合文档纪律)——①切片 `level.tres` display_name →「第一层 · 裂颚洞窟」+表尾改名批注;②切片首页按钮 `ui_assembler.gd`;③切片 README 试玩指引×2;④`00-游戏概念` 首关层名行;⑤`03-内容规模` 层名表+版本历史 v0.4;⑥registry `level_table` display_name 行。**命名出典**:新名致敬 DND 5e《凡戴尔的失落矿坑》(Lost Mine of Phandelver)地精部族「裂颚」(Cragmaw)洞窟巢穴——地精盘踞的浅层洞窟、通道向下延伸天然连通幽暗地域,且 LMOP 反主即卓尔「黑蜘蛛」,与 L2 蛛化卓尔 Boss 同谱;三层纵深叙事线贯通:裂颚洞窟(地精部族)→ 幽暗地域(蛛群·卓尔裔)→ 灰矮人矿城(第三关预案方向,未拍板)。第三关提案随批同步:层名行 + 蕈林备选理由重写(原理由绑「苔藓」已失义)+ 层名沿革注记 + 两处旧读数顺手校正(L2 完美 Boss 承伤改 51.6% 重投基线口径〔原 55.8% 系台账 96 地形批读数、已被台账 105 重投翻页〕;通关率带改分带后 L1 [35,55]/L2 [25,45])。仅字面变更:display_name 无代码/探针消费方(grep 证实),节点结构/行值/机制零改动。**验证**:冒烟 355 项 0 失败(tools/run_smoke.py 三闸全过;全新 worktree 首跑撞 class_name 缓存坑,对切片目录 `--editor --quit` 刷缓存后过——十二坑清单第 1 条再验证);全仓 grep 苔藓仅剩审计注记。 111. **第一层定名「地精洞窟」二改批(2026-09-20,本会话——用户拍板「不用这么 DND,就叫地精洞窟就行了,不用明确说明是某个部族,毕竟我们没有 DND 的相应版权」;worktree rename-l1-goblin-20260920,纯字面零机制)**:台账 110 的「裂颚洞窟」(DND 5e《凡戴尔的失落矿坑》Cragmaw 致敬名)全仓退役改定「地精洞窟」——理由 = 终名不绑定 DND 专名部族、规避 IP 指涉。8 处消费面同步(level.tres display_name+表尾二改批注 / ui_assembler 首页按钮 / 切片 README×2 / 00-游戏概念 / 03-内容规模 层名表+版本历史 v0.5〔v0.4 出典作废留档〕 / registry level_table / 第三关提案层名行+沿革注记改写);「裂颚」字样仅存于版本历史/批注/沿革等审计记录(有意保留)。洞窟纵深连通幽暗地域的三层纵深叙事线取向不变(地精洞窟→幽暗地域→灰矮人矿城)。**验证**:冒烟 355 项 0 失败三闸全过(worktree 建起即对切片目录预刷类名缓存——十二坑清单第 9 条路径坑的规避姿势);grep 裂颚非审计命中 0。 + +112. **地穴全图卷并入 Alpha 魔法道具整体重设计批(2026-09-20,本会话——用户拍板「统一放在另一个'魔法道具重新设计'的待办项里面,垂直切片阶段不保留魔法道具相关的内容」;worktree magic-fold-20260920,合并 fbb1488,纯文档零代码)**:**裁决面**——06 清单「地穴全图卷效果重定」(原两方案待拍板:纯全图预读/另配信息半边)按用户指令删除,去向=并入 **Alpha 阶段魔法道具整体重设计**(解决即移除口径;清单 44→42 条)——至此垂直切片期魔法道具族待办全部出清(显影尘台账 98 先例、探宝罗盘 2026-09-19 已排 Alpha)。**顺带回归修复**——「零伤害/无威胁组合的跨字段护栏」死条目清除:该条已随台账 100 伤害下限批解决移除(6a9f590),八型批合并 3468066 冲突解决误取分支侧复活(父一 master 0 处 × 父二 ambush 分支 1 处 → 合并结果 1 处,git 考古实证),本批重落地已裁决删除态。**悬空指认清偿三处**:①魔法道具.md 五维度表信息维度存根行——地穴全图卷半边并入显影尘同句改指 Alpha 重设计;②03-内容规模.md 待定句同口径合并;③registry aux_items catacomb_scroll 行内「显影半边待重定」改「效果待重定·随魔法道具整体重设计归 Alpha」(表头 effect_kinds/mvp_content 两处 Alpha 指针台账 98 已覆盖,不动)。**边界**——00-游戏概念/元进度道具工坊的地穴全图卷罗列系登记现状陈述非待办指针,未动;探宝罗盘条目已排 Alpha 维持原状。**验证**——doc_link_audit --strict 与 gdd_lint 均与 master 基线一致(exit 1 系既有白名单欠账非本批引入);冒烟不适用。 + +113. **八型批呈现层漏网修复批(2026-09-20,本会话——用户真人试玩遇界面流程卡断并提交 Godot 错误堆栈 `BattleLogView._format_row: Invalid access to property or key 'attach'`;worktree battlelog-attachfix-20260920,合并 931bba7)**:**根因**——八型批伏击退役(台账 101)拆除伤害行 attach 字段后,战斗日志视图两处退役拆行分支漏网未同步(战斗结算 §行协议「行协议变更必须同步呈现层的行格式化」义务违约):①基线+伏击拆行分支见 baseline 在场即读 row.attach——敌方行动伤害行(编排器 payload 带 baseline 从不带 attach)每场首笔敌方还手即 Invalid access,错误发生在结算战斗段中途→段流程夭折=界面整体卡断;②普通+穿刺+魔法三分量拆行分支随 2026-09-19 充能退役批同样过期(现行单分量 payload 只带实际攻击类型一键),主角营地换型穿刺/魔法攻击时同型崩溃(潜伏雷,用户尚未踩到)。**修复**——两拆行分支拆除,行上归因读数只剩掷点档位随行(2026-09-16 掷点批口径不变);playtest_logger payload 字段注记 attach→rider 同步;纯呈现层零行值零机制改动,行协议文档无改动(文档侧八型批已同步、漏网仅在代码)。**测试基建**——smoke 新增「战斗日志视图行格式化呈现层同步闸」段 11 断言:现行形态敌方/主角伤害行逐型喂真实视图+退役文案不再出现+未生效聚合 ×2+其余六行型逐型解析文本增长(格式化中途夭折=零增长,脚本错误静默吞断言盲区转可见 FAIL)——行协议 9 类自此有呈现层回归网;此为「逻辑层冒烟抓不到 UI bug」的首个仓内对策,UI 驱动夹具(真场景+合成输入+截图+超时护栏)仍待立项。**验证**——冒烟 355→366 项 0 失败三闸全过;四场景探针全过(ui_probe/tune PASS、click 问题=0、layout 全量转储 exit 0)。**附答**——用户另问第一关"?"格=藤蔓缠绕(信息型地形变异数字遮蔽,台账 96 设计行为非 bug,已答)。 +114. **ComfyUI 出图提示词与规格卡批(2026-09-20,本会话——用户请托「根据美术待生成清单补充对应的 prompt 还是具体的规格」,asset-spec 流程产出;worktree art-prompts-20260920,合并 31d2710,纯文档零代码)**:新建 `design/art/comfyui-prompts.md` v1.0——美术清单 ComfyUI 通道(角色级资产)的执行侧提示词卡:①主角三态(待机/攻击/受击)+六敌种(地精/熊地精普通精英两版/食人魔酋长/巨蜘蛛/兄弟精/蛛化卓尔精灵 Boss)逐资产正向提示词+通用正负底座,风格条款全转写 art-bible——§3.1 亮色描边+落影底盘轮廓通道(正向声明+负向禁黑描边)、§4.2 语义色禁区(紫赤 HSV 300°–345° 禁入带写入除 Boss 外全部卡片负向;Boss 卡反向主动取色+吸色验收专属注记);②通用规格表:1x 画布工作假设(主角 32×48 站姿样张档/普通怪 44×36 宽≥高/精英 44×44/Boss 64×72,标注一屏样张批冻结)、生成底色定纯浅灰 #F2F2F2(弃绿幕/品红幕理由=品红落禁入带、抠边留违规色相污染)、每资产 2–3 版候选+同 seed 同形象纪律;③兄弟精改荐**脚本换色通道**(换色不换形=原则三角色侧应用,ComfyUI 直出降备选,验收=与熊地精终稿剪影叠加逐像素一致);④五步流程=出图→候选入库 design/art/candidates/<资产>/→用户裁决→后处理(去底/最近邻降采样/色板量化/四联测试条,脚本管线代跑)→转正,附验收速查五关(灰度剪影认档/轮廓通道深浅底可辨/吸色扫禁入带/2–3 头身/同形象并排);⑤两份清单各补指路行(art-assets-checklist §2 头+asset-checklist 渠道表尾)。**边界**——动画类(意图三档/演出三类/连锁坍塌)明确不在本文件(一屏样张锁方案后另立规格卡);脚本管线资产(徽章 14 枚/节点图/砖面终稿)不需要提示词(规格锚 art-bible §4.6 图标键契约)。**验证**——doc_link_audit --strict 与 gdd_lint 均与 master 基线一致(31 文件/769 链接/死链 7 处全在 design/ux/battle.md 既有欠账、gdd_lint 8 文件既有,均非本批引入);冒烟不适用;worktree 与分支已清。 115. **第三关内容批 + GP 定稿提案批(2026-09-20,本会话——用户对 proposal-level3-content-2026-09-20.md 五拍板点全按推荐通过(灰矮人矿城/GP+商店房提前捆绑/乏力首发/板面+地形杠杆/测试直入口),plan 模式三子代理并行执行;worktree l3-content-20260920)**:**L3-B 第三关内容批**——①数据表:enemy.tres 七行(duergar 灰矮人重甲 15 血/灰矮人重装手 16/小队 8、hook_horror 钩爪恐魔轻甲 5–6 血群行〔复用 bite 共享行〕、Boss duergar_king 灰矮人石君王 75 血重甲=Σ91×r_boss≈0.82 锚;两敌种首遇位节点 0/1 固定数量不加骰;armor_class 恒 0 归在案投值条目);board_gen.tres 七行(战斗 13×13/Boss 14×14、d0.25、t0.88、词表第三值 deep_underdark);level.tres 第三关九节点图(与 L2 同模板);variation.tres 落石(flat_damage 3,1→2→3 递进)+矿尘(hide_number);skills.tres earth_pulse 大地脉动(妨害型=sapped 乏力第二词汇首发,字段口径同 web_snare);②代码:board_gen_table.gd 词表 v1.1+1、关卡 3 测试直入口(ui_assembler lv3btn+main 接线)、smoke_all.gd L3 断言段(图结构/跨表 7 坐标/定点/物种集八敌种/首遇绿档成长代理档口径 atk+200·hp+100 十二种子);③registry 五条目(enemy/board_gen_table/skill/variation_table/level_table);④GDD 五文档:03 v0.6(三关已落地/棋盘按关配置口径)/难度曲线与Boss v0.17(第三关图版注记+L3 provisional-pending-growth 口径)/棋盘生成 v0.4(词表 v1.1)/地形变异 v0.4(六类型两轴×三关)/06(GP 条目立项提前);**验证**——冒烟 375 项 0 失败三闸全过+三 UI 探针 PASS;板形验算 13×13 推理率 0.950–0.959/14×14 0.941 全达标零降档(降档梯子未触发);Boss max_gen_attempts 36→32(越 schema 域 [1,32],自决取域上限并批注登记);**MC 读数(unique)**——零成长基线 N=300:普通 16%/完美 20%,死亡 75–80% 集中 Boss(8.boss 败北 188/194 次);成长代理档 N=30:普通 40%/完美 80%;**预案「零成长必红档」预判被部分证伪**(棋盘武器库的克制输出〔魔法矢+1 克重甲/爆裂/攻击增益〕是预案生存链估算未计入的供给面),但成长批必要性判读不变(零成长读数低于 L1/L2 分带下缘、Boss 死亡占比过高、带域需成长后才可判读)——L3 行值维持 provisional-pending-growth,正式带归曲线调参批与 L3-A 同窗。**GP+商店房定稿提案交付**(design/research/proposal-gp-shop-room-2026-09-20.md,196 行 10 拍板点):格面池零修订(GP 走战斗掉落单渠道 普通 2/Boss 5 固定值)、节点图四类→五类加商店房(每关恰 1 中段必经位 L2 行集首落地)、三属性 flat 同族档(攻+1 首价 4/血上限+3 首价 2/护甲+1 首价 4,重复购买每档+2,三选一+刷新 2 GP 限 2 次,shop_roll 新随机流)、入关第三关参数账 攻 5–6/血 28–34/AC11 对照成长代理档、「抽象货币不做」红线判读不拦(GP=战斗之果非扫雷夹层)、验收 shop=off/on 双格分层——**十拍板点待用户逐条表态后进波次 2 实施批(A 表/B 代码/C 文档/D 验证四互斥文件集拆批清单已附)**。 116. **GP+商店房实施批(L3-A 玩家成长批)(2026-09-20,本会话——用户对 proposal-gp-shop-room-2026-09-20.md 六开放问题逐条裁决(关键推翻:GP 进第九型格面,格面池零修订推荐被否);plan 批四子代理接力(A 表/B 代码/C 文档/D 冒烟);worktree gp-shop-20260920)**:**裁决要点**——①GP 进九型:金币袋(gp_gain,power 3,weight 2,低概率惊喜获取、量与战斗掉落相当)+破财(gp_loss,power 2,weight 0 字典行,钱包 clamp ≥0);②掉落非固定值=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留行)+关级加值(L1 0/L2 1/L3 2)+关级价格加成(L1 0/L2 2/L3 4);③记账=完成战斗统一到账(battle_finished 载荷 +gp_dropped),战斗内格面兑付走战斗域缓冲 escrow(战斗开始清零随载荷上抛即弃)与钱包隔离;④商店房=Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9、(8,shop,next[9])、L1 零改动、必经位冒烟断言承载);⑤统一冒险加算区 {atk,max_hp,armor_class}——商店购买与关卡大奖励同账、经覆盖值通道生效、购血上限与当前生命同步 +Δ;⑥三商品全陈列**无刷新无三选一**(shop_roll 作废),定价按期望账制定(L2 商店前可得 ≈35–40 GP vs 三件首购 16/全购 26–34,取舍空间成立,账进 shop.tres 批注);⑦**数值与分层验收整体后置**(用户拍板"骨架先行、数值是肌肉":shop=off/on 双格 MC、带平移、换算率全归曲线调参批与既有三条目同窗);⑧「关卡内结构随机生成(哈迪斯/弹珠式)」登记 06 方向观察条目未立项。**落地面**——新表 shop.tres+ShopTable schema(四行族+域校验)、reward 九型批(CELL_TYPES+gp、字典十八格、正向合计 76)、level_table 词表五类+L2/L3 rewiring(陈旧 (·,8) Boss 行清理)、代码四件(rng_service gp_drop 流〔黄金向量 m=42 scope0=6226476397888000011/scope1=5031558177015478025 已登 registry〕、battle_orchestrator escrow+gp_dropped、level_runner 钱包三收口+商店兑付+加算区、config_db shop 加载+gp 效应豁免)、呈现五件(ShopOverlay 商店覆盖层/顶栏金币常驻/格面徽标 gp 金色键〔避开紫赤禁入带〕/选路图商店房词表/battle_narrator 台词)、playtest_logger(battle_end.gp_dropped+wallet 事件行)、GDD 八文档(关卡节点地图 v1.1 五类枚举+编排规则/冒险结构 v0.5 GP 冒险账本+统一加算区/战斗结算 v0.23 载荷+缓冲/地图战斗资源 v0.15 九型批/难度曲线 v0.18 杠杆登记+分层 defer 成文/元进度 v1.2 双币分工/03 v0.7/05 红线显式例外成文)、registry 六处(reward/shop_table 新条目/level_table/载荷/随机流+黄金向量/术语三词条)、06 GP 提案条目解决移除、提案文首裁决记录块。**验证**——冒烟 437 项 0 失败三闸全过(+60 项断言:GP 到账区间/escrow 隔离+破财 clamp/商店三收口+价格递增+余额拒/图不变式含 DFS 全路径必经穿店/九型字典+黄金向量);ui_probe/click_probe(商店层适配)/tune_probe 三探针 PASS。 + +117. **冒险结构实现批——三关正式序列生效(2026-09-21,本会话——按用户规划书 .zcode/plans/adventure-structure-batch-plan-2026-09-21.md T0–T9 顺序执行,规划书 §7 代拍板默认值七项直接采行;worktree adventure-structure-20260921,代码批 7ddfa4b+文档批 5709cd3)**:**批次定位**——切片从「三个可分别试玩的单关 demo」升级为「一局完整的三关 Roguelike 冒险」:冒险级状态(生命账本/GP 钱包/统一加算区/主种子)所有权自 LevelRunner 上交 AdventureRunner(core/adventure_runner.gd 新建,Core 层零 UI)。**落地面**——①AdventureTable 建表:adventure_table.gd(两字段域检 [0,999])+adventure.tres(10/10 provisional)+config_db 接线+冒烟表闸 +4;②LevelRunner 账本外置(06 接口漂移①②③⑥⑦清偿):hero_hp/gp 改属性落 _ledger 字典(冒险模式=共享账本,调用点零改动)、恢复数学抽成单一路径 rest_heal_amount(冒险模式经 _rest_handler=AdventureRunner.apply_rest——接口③)、start_level_external 冒险模式入口(零重播种零账本重置)、close_wallet 公开化(冒险模式收束清零归 AdventureRunner._settle;漂移⑥=不发明空清库接口、注释登记事实清库);③AdventureRunner:start_adventure 主种子恰一次(rng_service new_adventure 唯一正式调用方——registry 同批登记)/大奖励非末关恰兑付一次(上限提升读 db.adventure+血回满=新上限+钥石照记,末关不发提升)/三结局结算(completed/failed/abandoned)+摘要五件事/结算受理只算一次(state 守卫)/四件套接口正式承载/ADR-0001 S6 全 _forward_* 方法转发;④level_autoplay 增 run_adventure(三关连探自动驱动——_drive_level 从 run_level 抽取零行为变更)+encounter_max_hp_seq/wallet_events/reward_events/shop_gp_seq 采集面;⑤呈现层:首页 lvbtn 改「开始冒险(三关连探:地精洞窟→幽暗地域→灰矮人矿城)」、L2/L3 加「测试直入口」前缀保留、大奖励屏(复用 inter_overlay+Narrator.big_reward_text)+冒险结算屏(复用 BattleEndOverlay+Narrator.adventure_summary_text 摘要五件事);⑥冒烟新段 +45(437→482:表闸 +4/API 闸 +8/冒险验收 +33——表覆盖义务/满配通关/种子复现/大奖励恰 2 次与覆盖值传播/败北线两种子扫描〔零配 L1 败+半配 L2 败〕/重复宣布拒绝/GP 跨关延续〔gain= battles·clear 恰一次为末事件·L2 商店房余额>0〕/加算区同账〔商店+大奖励同账累加+回满新上限〕/恢复数学三读数/放弃两态);⑦文档:registry 三处(adventure_table 已建表/level_table 三关序列生效/rng_service new_adventure 调用权)、06 三条目解决移除(两关序列未生效/切片行进代码与定稿的接口漂移/冒险表未建表)、03 v0.8(L2/L3 状态行改正式序列已生效)、冒险结构 v0.6(实现批落地登记,正文零改动)、roadmap(时间线 09-20/09-21 两行+路线图第 4/5 项收口——存档批与元进度批挂点就位)、切片 README(主入口口径+修改记录条目)。**代拍板登记(规划书 §7 七项采行,待用户确认)**:①lvbtn 改开始冒险+L1 单关直入口取消(冒险首关即 L1);②大奖励屏复用 inter_overlay;③冒险结算屏复用 BattleEndOverlay;④钥石只在结算屏摘要展示(HUD 常驻归元进度批);⑤每关新建 LevelRunner;⑥不发明空清库接口;⑦放弃 API 预留+冒烟断言、无 UI 入口。**规划书冲突按代码为准三处(台账登记)**:①_pending 置位先于 emit——信号同步派发下大奖励处理器内立即 continue_to_next_level 需 _pending_next_level 已就位(T4 手动验证实证 advance_failed 后修正);②encounter_max_hp_seq 抓取点改 runner.effective_max_hp()——规划书指定 node_entered 时抓 encounter.hero_max_hp,但 encounter 字典在 begin_battle 内才赋值(node_entered 先于 begin 发出)读到空字典,effective_max_hp() = begin_battle 将收 hero_max_hp 同一表达式等价;③abandon() 补 state=running 守卫——规划书只写战斗中拒绝,补齐后结算后重复放弃亦拒绝(契约 7 结算受理只算一次的 abandoned 侧对齐)。**验证**——冒烟 482 项 0 失败三闸全过;四探针全过(ui_probe/tune/click 问题=0/layout 转储 exit 0——click 探针真实驱动了一局冒险失败终局屏);MC 零行为变更回归:T0 同款命令(20 局 L1 normal unique seed0=4200,基线格通关率 35%)重跑 10 行读数与基线逐位一致(diff 仅 _drive_level 抽取的堆栈帧行号与退出泄漏计数——非行为面);文档三工具与 master 基线一致(doc_link_audit 死链 7=7 既有欠账、链接 787→786 系本批删除一条待办指路链接、gdd_lint 8 文件既有、doc_term_audit 215 处/疑似现行 1 处均既有)。 From 96255ae19898d3e45826a894044af493bf857f22 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 05:12:57 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat(save):=20=E5=AD=98=E6=A1=A3=E6=8C=82?= =?UTF-8?q?=E7=82=B9=E6=8E=A5=E5=85=A5=E7=BB=84=E5=90=88=E6=A0=B9=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=90=AF=E5=8A=A8=E8=AF=BB=E7=9B=98+=E5=86=92?= =?UTF-8?q?=E9=99=A9=E6=94=B6=E6=9D=9F=E5=86=99=E7=9B=98+=E6=91=98?= =?UTF-8?q?=E8=A6=81=E9=9B=86=E6=88=90=E6=96=AD=E8=A8=80=E2=91=A9=E9=A1=B9?= =?UTF-8?q?=EF=BC=88=E5=86=92=E7=83=9F=20518=20=E5=85=A8=E7=BB=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.gd:_ready 建 SaveManager+load_from_disk(拒读/损坏类内兜底不阻断);_on_adventure_finished 尾部 apply_adventure_summary+write_to_disk(正式冒险收束唯一写档点);L2/L3 直入口与调试两层 走 runner/debug 路径不经本处理器——契约 7 测试入口不写档结构性成立 - smoke ⑨ 摘要集成:run_adventure(42,200,100) 真局摘要→域读数(余额=3×keystone_per_level/次数全 1/ 首杀齐/recent seed42)+二局翻倍+写盘读回保持 --- .../presentation/main.gd | 13 +++++++- .../smoke/smoke_all.gd | 32 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 67baab64..5ddceca9 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -26,11 +26,13 @@ const Narrator := preload("res://presentation/battle_narrator.gd") const ModalStack := preload("res://presentation/modal_stack.gd") const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") +const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) var db var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) var runner = null # 关卡节点地图 图版推进器(完整关卡模式;冒险模式 = 每关重取 adventure.runner()) var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) +var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构) var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) var _level_active := false @@ -76,6 +78,10 @@ func _ready() -> void: if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return + # 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动 + # (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。 + save_manager = SaveManagerScript.new() + save_manager.load_from_disk() _debug = DebugSessionScript.new(db, get_node("/root/RngService"), Callable(self, "_debug_hero_override"), Callable(self, "_seed_value")) _debug.battle_created.connect(_on_debug_battle_created) @@ -349,6 +355,11 @@ func _on_adventure_finished(summary: Dictionary) -> void: _end_overlay.open_defeat(Narrator.adventure_summary_text(summary), "返回", board_view.get_global_rect()) logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) + # 存档入账(存档序列化批):正式冒险收束唯一写档点——域更新原语 + 原子写盘。 + # 单关测试入口与调试两层不经过本处理器,天然不写档(契约:测试模式不污染生涯数据)。 + if save_manager != null: + save_manager.apply_adventure_summary(summary) + save_manager.write_to_disk() if runner != null: runner.restore_hero_base() strip_view.refresh(runner) @@ -357,7 +368,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: func _on_battle_end_continue() -> void: - # 终局出口:返回开始界面(冒险摘要留档试玩日志——钥石持久化归存档/元进度批) + # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批) _close_modal(_end_overlay) _end_overlay.close() _debug.reset() diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd index 0a49d081..1478e30e 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/smoke_all.gd @@ -3106,16 +3106,16 @@ func _smoke_battle_log_view() -> void: # ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/ -# 损坏兜底/迁移链/域界干净/摘要 cap)。测试文件统一 user://smoke_save_*.json 隔离,段首清场+ -# 段尾清理——绝不碰 user://save.json(红线)。摘要集成断言(run_adventure 驱动,B1 冒险结构批产物) -# 待其落 master 后补段。 +# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成)。测试文件统一 user://smoke_save_*.json 隔离, +# 段首清场+段尾清理——绝不碰 user://save.json(红线)。 func _smoke_save() -> void: print("--- SaveManager 存档读写/迁移/兜底 ---") var sm_script = load("res://core/save_manager.gd") var paths := ["user://smoke_save_roundtrip.json", "user://smoke_save_fresh.json", "user://smoke_save_spend.json", "user://smoke_save_highver.json", "user://smoke_save_corrupt.json", "user://smoke_save_migrate.json", - "user://smoke_save_domain.json", "user://smoke_save_cap.json"] + "user://smoke_save_domain.json", "user://smoke_save_cap.json", + "user://smoke_save_integration.json"] var stale: Array = [] for p in paths: stale.append(p) @@ -3220,6 +3220,30 @@ func _smoke_save() -> void: check(int(arr[0].get("master_seed", -1)) == 11 and int(arr[9].get("master_seed", -1)) == 2, "最新在前(头 seed 11 / 尾 seed 2)") + # ⑨ 摘要集成(冒险结构批产物驱动):run_adventure 真局摘要 → apply 原语 → 域读数 → 写盘读回保持 + var autoplay = load("res://smoke/level_autoplay.gd") + var st9: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + var f9 = sm_script.new(paths[8]) + f9.apply_adventure_summary(st9.get("summary", {})) + var kpl := int(_db.adventure.keystone_per_level) + check(String(st9.get("result", "")) == "completed", "摘要集成:冒险驱动满配通关(seed 42)") + check(f9.keystone_balance() == 3 * kpl, "集成钥石余额 = 3 × keystone_per_level(%d)" % f9.keystone_balance()) + check(f9.clear_count(1) == 1 and f9.clear_count(2) == 1 and f9.clear_count(3) == 1, "集成通关次数表全 1") + check(f9.boss_first_kill_marked(1) and f9.boss_first_kill_marked(2) and f9.boss_first_kill_marked(3), + "集成三关首杀标记齐") + check(f9.recent_summaries().size() == 1 and int(f9.recent_summaries()[0].get("master_seed", -1)) == 42, + "集成 recent 1 条且 seed 42") + var st9b: Dictionary = autoplay.run_adventure(_db, 42, 200, 100) + f9.apply_adventure_summary(st9b.get("summary", {})) + check(f9.clear_count(1) == 2 and f9.clear_count(2) == 2 and f9.clear_count(3) == 2, "第二局后通关次数全 2") + check(f9.keystone_balance() == 6 * kpl, "第二局后余额翻倍(6 × keystone_per_level)") + check(f9.recent_summaries().size() == 2, "集成 recent 长度 2") + check(f9.write_to_disk(), "集成写盘成功") + var r9 = sm_script.new(paths[8]) + check(r9.load_from_disk() and r9.keystone_balance() == 6 * kpl and r9.clear_count(3) == 2 + and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3), + "集成写盘读回全量保持") + # 段尾清理(测试文件绝不残留) for p in stale: if FileAccess.file_exists(p): From a569f1935db0cae5548cbaf607d85687a267d812 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 14:31:03 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat(ui):=20=E6=94=BE=E5=BC=83=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E7=95=8C=E9=9D=A2=E5=91=88=E7=8E=B0=E2=80=94=E2=80=94?= =?UTF-8?q?=E9=80=89=E8=B7=AF=E5=81=9C=E7=82=B9=E6=8C=82=E6=94=BE=E5=BC=83?= =?UTF-8?q?=E6=8C=89=E9=92=AE=EF=BC=88=E5=86=92=E9=99=A9=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=BC=80/=E5=8D=95=E5=85=B3=E7=9B=B4=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E5=85=B3=EF=BC=8C=E6=88=98=E6=96=97=E4=B8=AD=E6=8B=92=E7=BB=9D?= =?UTF-8?q?=E9=98=B2=E5=BE=A1=E5=88=86=E6=94=AF=EF=BC=9B=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8B=8D=E6=9D=BF=E5=8F=B0=E8=B4=A6=20119=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/main.gd | 18 ++++++++++++++++-- .../presentation/views/route_overlay.gd | 19 +++++++++++++++++++ .../smoke/ui_probe.gd | 13 +++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd index 5ddceca9..2df53ddd 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/main.gd @@ -137,6 +137,7 @@ func _build_ui() -> void: 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) shop_overlay.buy_requested.connect(_on_shop_buy) shop_overlay.leave_pressed.connect(_on_shop_leave) _end_overlay.review_pressed.connect(_on_end_review) @@ -223,6 +224,7 @@ func _start_full_level(level_index: int = 1) -> void: runner.wallet_changed.connect(_on_wallet_changed) _level_active = true _adventure_mode = false + route_overlay.set_abandon_available(false) # 单关测试直入口无冒险可放弃(放弃语义对象 = 整局冒险) _reset_run_scope() log_view.clear() var seed_val := _seed_value() @@ -250,6 +252,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 用户拍板) _reset_run_scope() log_view.clear() var seed_val := _seed_value() @@ -311,7 +314,7 @@ func _on_level_finished(payload: Dictionary) -> void: _level_active = false var result: String = String(payload.get("result", "")) settlement = null - route_overlay.visible = false # 终局兜底(选路口非 Boss 节点才有——防御性收层) + _close_modal(route_overlay) # 终局兜底(放弃入口新场景:选路层在开态——走模态栈恢复其遮挡面;未开过时 close 容错) _sync_settlement_visuals() # → 全关 info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④) stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) @@ -344,7 +347,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: _level_active = false var result: String = String(summary.get("result", "")) settlement = null - route_overlay.visible = false # 终局兜底(选路口非 Boss 节点才有——防御性收层) + _close_modal(route_overlay) # 终局兜底(放弃入口新场景:选路层在开态——走模态栈恢复其遮挡面;未开过时 close 容错) _sync_settlement_visuals() # → 全关 info_panel.set_status_cleared_feedback() # 状态槽全清可见反馈(增益状态×4④) stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) @@ -608,6 +611,17 @@ func _on_route_chosen(node_id: int) -> void: push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) +func _on_abandon_requested() -> void: + # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款; + # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, + # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 + if not _adventure_mode or adventure == null: + return + if not adventure.abandon(): + logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) + board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") + + # ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)---- func _on_shop_buy(product_id: String) -> void: diff --git a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/route_overlay.gd b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/route_overlay.gd index 6e7e5e7f..53e7f852 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/presentation/views/route_overlay.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/presentation/views/route_overlay.gd @@ -5,10 +5,14 @@ # 唯一前进通道 = 点候选按钮 → route_chosen(node_id) → 组合根转 runner.pick_next_node。 # 高内聚边界:只读 runner 成文出口(nodes()/visited_ids()/node_index/next_candidate_ids())做渲染, # 无任何写面;图形为程序化占位质量(占位纪律)。 +# 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——台账 119;推翻 冒险结构.md「MVP 无界面入口」 +# 后置条款):选路停点 = 天然非战斗期(接口契约:战斗中放弃一律拒绝),按钮常驻本层底部; +# 可见性归组合根(冒险模式开、单关测试直入口关——放弃的语义对象是"整局冒险")。 class_name RouteOverlay extends Control signal route_chosen(node_id: int) +signal abandon_requested const COL_ROOM_DONE := UiKit.GOLD_DIM # 已到——暗金 const COL_ROOM_CURRENT := UiKit.GOLD # 当前——奖励金 @@ -21,6 +25,7 @@ const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": " var _runner = null var _btn_box: VBoxContainer var _title: Label +var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——冒险模式开/单关测试直入口关) func _init() -> void: @@ -45,9 +50,23 @@ func _init() -> void: _btn_box = VBoxContainer.new() _btn_box.add_theme_constant_override("separation", 6) box.add_child(_btn_box) + abandon_btn = UiKit.button("🏳 放弃冒险(同失败结算——已获大奖励保留)") + abandon_btn.add_theme_color_override("font_color", UiKit.FLAG_RED) + abandon_btn.pressed.connect(_emit_abandon) + abandon_btn.visible = false + box.add_child(abandon_btn) visible = false +# 放弃入口可见性(组合根在冒险启动/单关直入口启动时切换——语义对象 = 整局冒险) +func set_abandon_available(v: bool) -> void: + abandon_btn.visible = v + + +func _emit_abandon() -> void: + abandon_requested.emit() + + # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染) func open_choices(cands: Array, runner) -> void: _runner = runner diff --git a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd index 991db624..8d81b54f 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd +++ b/prototypes/dungeon-sweeper-vertical-slice/smoke/ui_probe.gd @@ -13,10 +13,23 @@ func _ready() -> void: var main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame + # ---- 放弃入口(2026-09-21 用户拍板——台账 119):默认隐藏(单关测试直入口无冒险可放弃)---- + if main.route_overlay.abandon_btn == null or main.route_overlay.abandon_btn.visible: + print("[ui_probe] FAIL — 放弃按钮默认应隐藏(单关测试直入口关)") + get_tree().quit(1) + return main._start_full_level() await get_tree().process_frame await get_tree().process_frame var problems: Array = [] + # ---- 放弃入口·冒险模式可见 + 战斗中按下被拒(真实按钮路径——入口挂选路停点,战斗中拒绝为防御分支)---- + if main.route_overlay.abandon_btn == null or not main.route_overlay.abandon_btn.visible: + problems.append("冒险模式放弃按钮未开放(选路停点入口在案)") + else: + main.route_overlay.abandon_btn.pressed.emit() + await get_tree().process_frame + if main.adventure == null or String(main.adventure.state) != "running": + problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") # ---- 既有断言(回归基线)---- if main.board_view._cells.is_empty(): problems.append("棋盘格未生成(cells=0)") From 7d50e0f75553766356e79d17e102ccb3ae2eb951 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 14:31:04 +0800 Subject: [PATCH 8/9] =?UTF-8?q?docs(design):=20=E6=94=BE=E5=BC=83=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=E6=89=B9=E6=96=87=E6=A1=A3=E2=80=94=E2=80=94=E5=86=92?= =?UTF-8?q?=E9=99=A9=E7=BB=93=E6=9E=84=20v0.7=EF=BC=88=E6=AD=A3=E6=96=87?= =?UTF-8?q?=E7=8E=B0=E8=A1=8C=E5=8F=A3=E5=BE=84+P1=20=E4=B8=A4=E8=A1=8C?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=EF=BC=89/roadmap=20=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E7=BA=BF=E8=A1=A5=2009-18/09-19/README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/systems/冒险循环/冒险结构.md | 7 +++---- production/roadmap.md | 6 ++++-- prototypes/dungeon-sweeper-vertical-slice/README.md | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/design/gdd/systems/冒险循环/冒险结构.md b/design/gdd/systems/冒险循环/冒险结构.md index 0ec383d8..70160fd8 100644 --- a/design/gdd/systems/冒险循环/冒险结构.md +++ b/design/gdd/systems/冒险循环/冒险结构.md @@ -170,7 +170,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 重开 = 全新一轮冒险:注入新主种子、从第一关、满血、空手——不是"重打刚才那一关"。上一局清零的是生命账本、库存与种子;唯一保留面是冒险摘要,它给每次死亡一个可对照的坐标("这局到第二层"),重开因此是新局面而不是复读。 -主动放弃按与失败同型结算:已获得的大奖励不追回、已到手的保留,摘要标记未打穿。放弃请求只在非战斗期受理,战斗进行中一律拒绝(战斗域单事务在飞)。MVP 没有放弃的界面入口,语义先行预留,入口呈现后置。 +主动放弃按与失败同型结算:已获得的大奖励不追回、已到手的保留,摘要标记未打穿。放弃请求只在非战斗期受理,战斗进行中一律拒绝(战斗域单事务在飞)。放弃的界面入口挂在[关卡节点地图](关卡节点地图.md)的选路覆盖层上——选路停点天然是非战斗期,按钮只在冒险模式开放(单关测试直入口没有可放弃的冒险对象);战斗进行中没有选路停点,入口因此天然不可达,拒绝分支仅作防御兜底。 主种子(本文含义:冒险开始时注入的一个随机种子,本局全部随机性都派生自它)在冒险开始时注入恰一次。冒险内的雷图生成、奖励抽取、遭遇掷骰与 GP 掉落掷定(消费 gp_drop 封闭流)全部派生自它,冒险内不重置、不逐关播种——早期切片"每关开局各自播种"是没有冒险层时的历史简化,已改写为全冒险单一种子。冒险的可复现域 = 主种子 + 各随机流计数器,供复盘与快照(见[存档序列化](../表现与支撑/存档序列化.md))。重开抽到的种子与上一局相同是允许的(随机源范围小,撞种子不算错误),但生成新种子不得复用玩法里的随机数。 @@ -230,8 +230,6 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 | 优先级 | 内容 | 后置原因 | |---|---|---| -| P1 | 两关序列生效(第二关行集扩展批) | 前提 = 难度曲线与Boss的第二关深度轴行值填充批;未落地前以单关冒险运行(合法态) | -| P1 | 放弃入口的界面呈现 | MVP 无界面入口——仅语义预留(非战斗期受理、同失败型结算);呈现归 HUD框架与 UX 批 | | P2 | 大奖励形态扩展(三选一 / 攻击型 / 通关加成) | 冒险表扩列结构位已留;设计随元进度定稿 | | P2 | 冒险开始参数面消费 | 预留给未来跨局加成注入(MVP 空参数,结构同覆盖值通道);归元进度 | | P2 | 冒险长度扩展(多于两关) | MVP 两关对比愿景单局 20–40 分钟;扩关 = 内容批,节奏按真人单关时长数据定 | @@ -265,7 +263,7 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 - 收束次序:通关宣布到达时库存已被关卡节点地图先行清空;重复的关卡结束宣布被拒绝,账本与摘要零变化。 - 覆盖义务:冒烟断言——行集内每个战斗与关底节点(关、深)在敌人与棋盘生成的数值表中有精确行;序列关数 = 行集关数。 - 生效前提未兑现(第二关行缺行):以单关冒险运行(合法态),两关断言显式跳过而非失败。 -- 放弃:战斗中的放弃请求被拒绝;非战斗期放弃按失败同型结算,已获大奖励不追回。 +- 放弃:战斗中的放弃请求被拒绝;非战斗期放弃按失败同型结算,已获大奖励不追回;放弃入口仅在选路停点呈现且只在冒险模式开放(单关测试直入口无放弃按钮)。 - 种子域:冒险内零随机流自开,一切随机派生自主种子(含 GP 掉落掷定的 gp_drop 封闭流);同种子同操作序列两局逐位可复现。 - GP 账本封闭:钱包只有三个收口——战斗完成入账恰一次(掉落掷定 + 关级加值 + 战斗内缓冲合计)、商店购买出账、冒险收束清零;战斗内缓冲与钱包隔离,战斗进行中钱包零变动。 - GP 不留痕:GP 不入存档、不入冒险摘要;重开后钱包从零开始。 @@ -285,3 +283,4 @@ GP(显示名「金币」)是冒险内的一次性属性提升货币:战斗 | v0.4 | 2026-09-17 | 数值纪律收口批:正文与调参表 knob 现行值改指路存根(事实源 data/tables/*.tres 与 registry),3 处失同步修正(示例轨迹改纯符号〔旧 50/50 生命轴示例作废〕、大奖励 +10/+10 改读 adventure_table 行值并删换算率数字锚、调参表 30/10/10 三格改读 level.tres / adventure_table) | | v0.5 | 2026-09-20 | **GP+商店房批(用户拍板——与关卡节点地图 v1.1 / 战斗结算 v0.23 / 地图战斗资源 v0.15 / 难度曲线与Boss v0.18 / registry / 切片同批)**:① 新增「GP 冒险账本」节——钱包三收口(战斗完成统一入账 / 商店购买出账 / 冒险收束清零)+ 战斗内缓冲隔离语义(格面兑付入缓冲、战斗完成统一到账、战斗域零钱包状态);② 统一冒险加算区成文——商店购买与关卡大奖励上限提升同一本账(三轴 {攻击, 生命上限, 护甲等级},经覆盖值通道生效;购血 = 上限与当前生命同步 +Δ);③ GP 掉落掷定消费 gp_drop 封闭随机流(主种子派生)登记进主种子节与调参表;④ GP 不入存档、不入冒险摘要;范围表 / 大奖励表 / 依赖表 / 验收标准同步。数值全 provisional-pending-曲线调参批,行值读 shop.tres | | v0.6 | 2026-09-21 | **冒险结构实现批落地登记(切片 AdventureRunner + LevelRunner 账本外置 + AdventureTable 建表——与切片代码 / registry / 03 / 06 / roadmap 同批)**:本批只记实现批状态,正文规则口径零改动。落地面 = 三关正式序列生效(首页「开始冒险」)、接口契约四件套正式侧承载(开始关卡 start_level_external / 战斗结果上报共享账本直写 / 治疗封顶回报 apply_rest / 关卡结束 level_finished)、生命账本与 GP 冒险账本上交冒险结构层、大奖励非末关恰兑付一次(末关钥石照记)、冒险摘要五件事产出、主种子恰一次注入(registry rng_service new_adventure 调用权同批登记)、结算受理只算一次;06 待办「两关序列未生效」「切片行进代码与定稿的接口漂移」「冒险表未建表」三条随批解决移除。**规划书冲突按代码为准登记两处**:①大奖励 `_pending_next_level` 置位先于 emit(信号同步派发——处理器内立即确认需标记已就位);②放弃受理补 state=running 守卫(契约 7 结算受理只算一次的 abandoned 侧对齐) | +| v0.7 | 2026-09-21 | **放弃入口界面呈现批(用户拍板「放弃接口也要有」——台账 119,推翻本文原「MVP 无界面入口、呈现后置」条款)**:① 放弃入口挂[关卡节点地图](关卡节点地图.md)选路覆盖层(选路停点 = 天然非战斗期;战斗中拒绝转为防御兜底分支——入口在战斗期不可达);② 入口只在冒险模式开放,单关测试直入口没有可放弃的冒险对象;③ 正文改现行口径(放弃节 / 验收标准放弃条),P1 后置表「放弃入口的界面呈现」随落地移除(同表「两关序列生效」系 v0.6 已落地漏收口,顺手同口径移除);④ 验收闸 = ui_probe 三断言(默认隐藏 / 冒险模式可见 / 首战中按下被拒 state 保持 running)+ 冒烟既有放弃两态 API 断言 | diff --git a/production/roadmap.md b/production/roadmap.md index b6f1da28..85496279 100644 --- a/production/roadmap.md +++ b/production/roadmap.md @@ -16,8 +16,10 @@ | 2026-09-15 | 命名清偿批;格面资源批(五段结算/极性路由/可选类删除);攻击类型与奖励抽象分离;护甲等级登记批;伤害量值区间批 | | 2026-09-16 | 行动点压速批 8→6;护甲等级掷点批落地(smoke 233 全绿);AP6+AC 验收判读口径 v1.0;06-待办清单建账改版;系统更名批;敌人技能池提案对答启动 | | 2026-09-17 | 敌人技能池提案定稿(1c63830);技能池实施批落地(smoke 263 全绿,待收编);魔法道具切片期空内容拍板;关卡节点地图定稿批解锁;跨会话状态持久化批(快照节 + 本文件 + 守卫文案升级) | -| 2026-09-20 | 第三关内容批(灰矮人矿城行集 + 测试直入口,smoke 375 全绿);GP+商店房实施批(九型格面+随机掉落+Boss前商店房+统一加算区,smoke 437 全绿);八型批呈现层漏网修复(真人试玩卡断——行协议呈现层同步闸 11 断言) | -| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿);存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档)——元进度批(钥石消费)挂点就位 | +| 2026-09-18 | 加法核数值语言拍板落地(台账 75)+ 80 遍模拟试玩基线(台账 76);无伤通关根因批(行动点压速 8→6);英雄双轴批(甲/攻类型可配置);伤害道具批五型→七型;战斗数值盘问会话四收口;关卡节点地图 v1.0 定稿(分支节点图拍板);元进度定名永久强化+营地三柜台 | +| 2026-09-19 | 验收读数重测批落地(4708940);掷点流种子偏置根因修复 A+B 案(R3 混淆派生 + MC unique 模式);插旗不可逆批(台账 87,冒烟 331);普通画像 v2 基础定式拍板(普通=基础定式/完美=上界仪);技能池新效应词汇批(妨害技+必中附加分量,smoke 306);清图支配性对账重跑(c8e5afd);迭代闭环仪表盘落地(台账 78);worktree 铁律立规 | +| 2026-09-20 | 第三关内容批(灰矮人矿城行集 + 测试直入口,smoke 375 全绿);GP+商店房实施批(九型格面+随机掉落+Boss前商店房+统一加算区,smoke 437 全绿);八型批呈现层漏网修复(真人试玩卡断——行协议呈现层同步闸 11 断言);第一层定名「地精洞窟」 | +| 2026-09-21 | **冒险结构实现批:切片从三个单关 demo 升级为一局完整三关冒险**(AdventureRunner+LevelRunner 账本外置+AdventureTable 建表;三关正式序列生效;smoke 482 全绿);存档序列化批(SaveManager 落地:单档 JSON 存档读写/版本迁移/损坏兜底 + 元进度四件与冒险摘要入档,smoke 518 全绿);放弃入口界面呈现批(用户拍板「放弃接口也要有」——选路停点挂放弃按钮,台账 119)——元进度批(钥石消费)挂点就位 | ## 路线图(主线路线与门) diff --git a/prototypes/dungeon-sweeper-vertical-slice/README.md b/prototypes/dungeon-sweeper-vertical-slice/README.md index 36e517ad..8409475a 100644 --- a/prototypes/dungeon-sweeper-vertical-slice/README.md +++ b/prototypes/dungeon-sweeper-vertical-slice/README.md @@ -12,7 +12,7 @@ 1. 用 Godot 4.7.2 打开本目录(含 `project.godot` 的文件夹) 2. 按 F5 运行(主场景 `presentation/main.tscn`) -3. **主入口:`▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)`**——一条命走完三关(冒险结构实现批 2026-09-21:生命/GP/加算区跨关同账、非末关通关发大奖励〔上限提升+血回满+钥石〕、任意战斗倒下冒险终、结算屏出冒险摘要五件事);首关节点链:战→战→赏→战→战→疗→王。`▶ 测试直入口 · 第二关 / 第三关` 保留单关试玩模式 +3. **主入口:`▶ 开始冒险(三关连探:地精洞窟 → 幽暗地域 → 灰矮人矿城)`**——一条命走完三关(冒险结构实现批 2026-09-21:生命/GP/加算区跨关同账、非末关通关发大奖励〔上限提升+血回满+钥石〕、任意战斗倒下冒险终、结算屏出冒险摘要五件事;选路停点可「放弃冒险」(同失败结算,2026-09-21 用户拍板补入口));首关节点链:战→战→赏→战→战→疗→王。`▶ 测试直入口 · 第二关 / 第三关` 保留单关试玩模式 4. `测试用 · 角色数值调整板`:攻击/生命百分比加成(模拟天赋树解锁)+ 批量模拟(输出通关率/承伤期望/败北节点分布) headless 冒烟(55 项断言,含关卡线性推进/满配通关/裸装败北/回放确定性,约 30 秒): From ab98dbf6fb09dcee4ad80fe5e7ddcde5ad134d98 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 21 Sep 2026 14:32:52 +0800 Subject: [PATCH 9/9] =?UTF-8?q?chore(session):=20=E6=94=BE=E5=BC=83?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E6=89=B9=E5=BF=AB=E7=85=A7+=E5=8F=B0?= =?UTF-8?q?=E8=B4=A6=20119=EF=BC=88117=20=E4=B8=83=E9=A1=B9=E4=BB=A3?= =?UTF-8?q?=E6=8B=8D=E6=9D=BF=E7=A1=AE=E8=AE=A4=E9=97=AD=E7=8E=AF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- production/session-state/active.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/production/session-state/active.md b/production/session-state/active.md index 2e8a55f2..8724f737 100644 --- a/production/session-state/active.md +++ b/production/session-state/active.md @@ -12,6 +12,8 @@ ## 当前状态快照(收口批先更新本节,再追加台账;≤10 行) +- **更新于**:2026-09-21(**放弃入口界面呈现批 + 台账 117 七项代拍板确认**,worktree abandon-entry-20260921,台账 119):用户逐项裁决——①单关入口取消认可;⑦推翻改「放弃接口也要有」=选路覆盖层常驻「放弃冒险」按钮(选路停点=天然非战斗期;只在冒险模式开放,单关测试直入口关;战斗中拒绝转防御分支+toast 可见);②-⑥认可生效;顺手收口=终局兜底 route_overlay 收层改走模态栈(放弃时选路层在开态,须恢复遮挡面——close 容错未开过场景)。冒险结构.md v0.7(正文改现行口径+P1「放弃入口呈现」「两关序列生效」两行移除——后者系 117 漏收口顺手同口径);roadmap 时间线补 09-18/09-19 缺行。冒烟 518 项 0 失败三闸全过(新基线零增量断言)+四探针 PASS(ui_probe 新增放弃三断言:默认隐藏/冒险模式可见/首战中按下被拒 state 保持 running)+doc 三闸一致(死链 7=7 既有、链接 789→791 全为有效新链) +- **更新于**:2026-09-21(**存档序列化批**,worktree save-serialization-20260921,台账 118):SaveManager 落地(Core 层零 UI 零玩法依赖)——单档 JSON `user://save.json` 读写/版本迁移骨架(高版本拒读禁写/低版本链式迁移 register_migration)/损坏 .corrupt.bak 兜底/tmp+rename 原子写 + 存档域首批内容(元进度四件:钥石余额/锻造三档/通关次数/首杀标记 + 冒险摘要 cap 10)+ 类型化访问器与 apply_adventure_summary 原语;main.gd 挂点 = 启动读盘(兜底内聚不阻断)+ 冒险收束写盘(L2/L3 直入口与调试两层不写档——契约 7 结构性成立);存档序列化.md 升 **v1.0 定稿**(待设计六问全处置)+ registry save_file 逐域登记 + 06 条目解决移除;**代拍板 8 条待确认**(规划书 §7:JSON 单档/收束+营地即时写/拒读禁写/损坏备份/cap10/单档 MVP/迁移链钩/测试入口不写档;另登记计划书 registry 条目名冲突 save_format_version→实名 schema_version);开局遇 B1 未落 master 挂起待续,B1 落籍(4f7f25d)后分支吸并续完。冒烟 437→518 项 0 失败三闸全过+四探针 PASS+doc 三闸与 master 对账一致+MC 20 种子与 master 逐位一致(零玩法改动证明);花钥石消费面归元进度批(B3 已解阻塞) - **更新于**:2026-09-21(**冒险结构实现批——三关正式序列生效**,worktree adventure-structure-20260921,台账 117):按用户规划书(.zcode/plans/adventure-structure-batch-plan-2026-09-21.md)T0–T9 执行——切片从三个单关 demo 升级为一局完整三关冒险:AdventureTable 建表(adventure.tres 10/10)+AdventureRunner 新建(主种子恰一次/生命账本/GP 冒险账本/统一加算区/大奖励非末关恰兑付/三结局摘要五件事)+LevelRunner 账本外置(hero_hp/gp 属性化共享账本,接口漂移①②③⑥⑦清偿)+首页「开始冒险」三关连探(大奖励屏+冒险结算屏);06 三条目解决移除(两关序列未生效/接口漂移/冒险表未建表)。代拍板七项按规划书 §7 采行待确认(详见台账),规划书冲突按代码为准三处登记。冒烟 437→482 项 0 失败三闸全过+四探针 PASS;MC 回归逐位一致(旧单关路径零行为变更证明);文档三工具与基线一致 - **更新于**:2026-09-20(**GP+商店房实施批(L3-A 玩家成长批)**,worktree gp-shop-20260920,台账 116):用户六问裁决落地——①GP 进格面第九型(金币袋 weight 2 power 3 低概率惊喜+破财字典负向行,正向合计 76);②掉落=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留)+关级加值(L2 +1/L3 +2),gp_drop 新封闭流(黄金向量已登);③记账=战斗完成统一到账,格面兑付走战斗内缓冲 escrow 与钱包隔离;④商店房 Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9);⑤统一冒险加算区(商店购买与大奖励同账,购血上限当前同步);⑥三商品全陈列无刷新无三选一,价格=基价+关级加成+步长×已购档;数值/分层验收整体后置归曲线调参批(骨架先行)。冒烟 437 项 0 失败+三探针 PASS;GDD 八文档(关卡节点地图 v1.1 五类枚举/冒险结构 v0.5 GP 账本/战斗结算 v0.23/地图战斗资源 v0.15 九型/曲线 v0.18 杠杆登记/元进度 v1.2/03 v0.7/05 红线例外成文)+06 GP 条目收口+「关卡结构随机生成」方向观察登记 - **更新于**:2026-09-20(**第三关内容批 + GP 定稿提案批**,worktree l3-content-20260920,台账 115〔113/114 号让给并行在飞会话两批,顺延让号〕):L3-B 落地——第三层「灰矮人矿城」行集(enemy 七行 duergar/hook_horror/duergar_king + board 13×13/Boss 14×14 词表值 deep_underdark + 九节点图 + 落石/矿尘 + 大地脉动=sapped 首发)+ 关卡 3 测试直入口 + 冒烟 375 项全绿 + registry 五条目 + GDD 五文档(03 v0.6/曲线 v0.17/棋盘生成 v0.4/地形变异 v0.4/06 GP 条目立项提前);板形验算 13/14×14 推理率 0.94–0.96 全达标零降档;MC 读数:零成长基线普通 16%/完美 20%(N=300,死亡 75–80% 集 Boss——预案「必红」预判被棋盘武器库部分证伪,成长批必要性不变)+ 成长代理档 40%/80%;**GP+商店房定稿提案已交付**(proposal-gp-shop-room-2026-09-20.md,10 拍板点:格面池零修订/商店房必经位/三属性 flat 档 攻+1=4 血+3=2 甲+1=4),待用户逐条拍板后进波次 2 实施批 @@ -301,3 +303,7 @@ 116. **GP+商店房实施批(L3-A 玩家成长批)(2026-09-20,本会话——用户对 proposal-gp-shop-room-2026-09-20.md 六开放问题逐条裁决(关键推翻:GP 进第九型格面,格面池零修订推荐被否);plan 批四子代理接力(A 表/B 代码/C 文档/D 冒烟);worktree gp-shop-20260920)**:**裁决要点**——①GP 进九型:金币袋(gp_gain,power 3,weight 2,低概率惊喜获取、量与战斗掉落相当)+破财(gp_loss,power 2,weight 0 字典行,钱包 clamp ≥0);②掉落非固定值=均值±方差掷定(普通 3[2,4]/Boss 8[6,10]/精英 5[4,6] 预留行)+关级加值(L1 0/L2 1/L3 2)+关级价格加成(L1 0/L2 2/L3 4);③记账=完成战斗统一到账(battle_finished 载荷 +gp_dropped),战斗内格面兑付走战斗域缓冲 escrow(战斗开始清零随载荷上抛即弃)与钱包隔离;④商店房=Boss 前一位必经位(哈迪斯式,L2/L3 行集 Boss 重编号 8→9、(8,shop,next[9])、L1 零改动、必经位冒烟断言承载);⑤统一冒险加算区 {atk,max_hp,armor_class}——商店购买与关卡大奖励同账、经覆盖值通道生效、购血上限与当前生命同步 +Δ;⑥三商品全陈列**无刷新无三选一**(shop_roll 作废),定价按期望账制定(L2 商店前可得 ≈35–40 GP vs 三件首购 16/全购 26–34,取舍空间成立,账进 shop.tres 批注);⑦**数值与分层验收整体后置**(用户拍板"骨架先行、数值是肌肉":shop=off/on 双格 MC、带平移、换算率全归曲线调参批与既有三条目同窗);⑧「关卡内结构随机生成(哈迪斯/弹珠式)」登记 06 方向观察条目未立项。**落地面**——新表 shop.tres+ShopTable schema(四行族+域校验)、reward 九型批(CELL_TYPES+gp、字典十八格、正向合计 76)、level_table 词表五类+L2/L3 rewiring(陈旧 (·,8) Boss 行清理)、代码四件(rng_service gp_drop 流〔黄金向量 m=42 scope0=6226476397888000011/scope1=5031558177015478025 已登 registry〕、battle_orchestrator escrow+gp_dropped、level_runner 钱包三收口+商店兑付+加算区、config_db shop 加载+gp 效应豁免)、呈现五件(ShopOverlay 商店覆盖层/顶栏金币常驻/格面徽标 gp 金色键〔避开紫赤禁入带〕/选路图商店房词表/battle_narrator 台词)、playtest_logger(battle_end.gp_dropped+wallet 事件行)、GDD 八文档(关卡节点地图 v1.1 五类枚举+编排规则/冒险结构 v0.5 GP 冒险账本+统一加算区/战斗结算 v0.23 载荷+缓冲/地图战斗资源 v0.15 九型批/难度曲线 v0.18 杠杆登记+分层 defer 成文/元进度 v1.2 双币分工/03 v0.7/05 红线显式例外成文)、registry 六处(reward/shop_table 新条目/level_table/载荷/随机流+黄金向量/术语三词条)、06 GP 提案条目解决移除、提案文首裁决记录块。**验证**——冒烟 437 项 0 失败三闸全过(+60 项断言:GP 到账区间/escrow 隔离+破财 clamp/商店三收口+价格递增+余额拒/图不变式含 DFS 全路径必经穿店/九型字典+黄金向量);ui_probe/click_probe(商店层适配)/tune_probe 三探针 PASS。 117. **冒险结构实现批——三关正式序列生效(2026-09-21,本会话——按用户规划书 .zcode/plans/adventure-structure-batch-plan-2026-09-21.md T0–T9 顺序执行,规划书 §7 代拍板默认值七项直接采行;worktree adventure-structure-20260921,代码批 7ddfa4b+文档批 5709cd3)**:**批次定位**——切片从「三个可分别试玩的单关 demo」升级为「一局完整的三关 Roguelike 冒险」:冒险级状态(生命账本/GP 钱包/统一加算区/主种子)所有权自 LevelRunner 上交 AdventureRunner(core/adventure_runner.gd 新建,Core 层零 UI)。**落地面**——①AdventureTable 建表:adventure_table.gd(两字段域检 [0,999])+adventure.tres(10/10 provisional)+config_db 接线+冒烟表闸 +4;②LevelRunner 账本外置(06 接口漂移①②③⑥⑦清偿):hero_hp/gp 改属性落 _ledger 字典(冒险模式=共享账本,调用点零改动)、恢复数学抽成单一路径 rest_heal_amount(冒险模式经 _rest_handler=AdventureRunner.apply_rest——接口③)、start_level_external 冒险模式入口(零重播种零账本重置)、close_wallet 公开化(冒险模式收束清零归 AdventureRunner._settle;漂移⑥=不发明空清库接口、注释登记事实清库);③AdventureRunner:start_adventure 主种子恰一次(rng_service new_adventure 唯一正式调用方——registry 同批登记)/大奖励非末关恰兑付一次(上限提升读 db.adventure+血回满=新上限+钥石照记,末关不发提升)/三结局结算(completed/failed/abandoned)+摘要五件事/结算受理只算一次(state 守卫)/四件套接口正式承载/ADR-0001 S6 全 _forward_* 方法转发;④level_autoplay 增 run_adventure(三关连探自动驱动——_drive_level 从 run_level 抽取零行为变更)+encounter_max_hp_seq/wallet_events/reward_events/shop_gp_seq 采集面;⑤呈现层:首页 lvbtn 改「开始冒险(三关连探:地精洞窟→幽暗地域→灰矮人矿城)」、L2/L3 加「测试直入口」前缀保留、大奖励屏(复用 inter_overlay+Narrator.big_reward_text)+冒险结算屏(复用 BattleEndOverlay+Narrator.adventure_summary_text 摘要五件事);⑥冒烟新段 +45(437→482:表闸 +4/API 闸 +8/冒险验收 +33——表覆盖义务/满配通关/种子复现/大奖励恰 2 次与覆盖值传播/败北线两种子扫描〔零配 L1 败+半配 L2 败〕/重复宣布拒绝/GP 跨关延续〔gain= battles·clear 恰一次为末事件·L2 商店房余额>0〕/加算区同账〔商店+大奖励同账累加+回满新上限〕/恢复数学三读数/放弃两态);⑦文档:registry 三处(adventure_table 已建表/level_table 三关序列生效/rng_service new_adventure 调用权)、06 三条目解决移除(两关序列未生效/切片行进代码与定稿的接口漂移/冒险表未建表)、03 v0.8(L2/L3 状态行改正式序列已生效)、冒险结构 v0.6(实现批落地登记,正文零改动)、roadmap(时间线 09-20/09-21 两行+路线图第 4/5 项收口——存档批与元进度批挂点就位)、切片 README(主入口口径+修改记录条目)。**代拍板登记(规划书 §7 七项采行,待用户确认)**:①lvbtn 改开始冒险+L1 单关直入口取消(冒险首关即 L1);②大奖励屏复用 inter_overlay;③冒险结算屏复用 BattleEndOverlay;④钥石只在结算屏摘要展示(HUD 常驻归元进度批);⑤每关新建 LevelRunner;⑥不发明空清库接口;⑦放弃 API 预留+冒烟断言、无 UI 入口。**规划书冲突按代码为准三处(台账登记)**:①_pending 置位先于 emit——信号同步派发下大奖励处理器内立即 continue_to_next_level 需 _pending_next_level 已就位(T4 手动验证实证 advance_failed 后修正);②encounter_max_hp_seq 抓取点改 runner.effective_max_hp()——规划书指定 node_entered 时抓 encounter.hero_max_hp,但 encounter 字典在 begin_battle 内才赋值(node_entered 先于 begin 发出)读到空字典,effective_max_hp() = begin_battle 将收 hero_max_hp 同一表达式等价;③abandon() 补 state=running 守卫——规划书只写战斗中拒绝,补齐后结算后重复放弃亦拒绝(契约 7 结算受理只算一次的 abandoned 侧对齐)。**验证**——冒烟 482 项 0 失败三闸全过;四探针全过(ui_probe/tune/click 问题=0/layout 转储 exit 0——click 探针真实驱动了一局冒险失败终局屏);MC 零行为变更回归:T0 同款命令(20 局 L1 normal unique seed0=4200,基线格通关率 35%)重跑 10 行读数与基线逐位一致(diff 仅 _drive_level 抽取的堆栈帧行号与退出泄漏计数——非行为面);文档三工具与 master 基线一致(doc_link_audit 死链 7=7 既有欠账、链接 787→786 系本批删除一条待办指路链接、gdd_lint 8 文件既有、doc_term_audit 215 处/疑似现行 1 处均既有)。 + +118. **存档序列化批(2026-09-21,本会话——用户令按规划书 `.zcode/plans/save-serialization-batch-plan-2026-09-21.md` T0→T8 顺序执行(判据不过不进下一步)、worktree 开发、遇冲突先做其余再验证收尾;worktree save-serialization-20260921,四笔 a7f35c9 代码 / 569d634 文档 / ed75e0f 吸收冒险结构批 / 96255ae 挂点)**:**落地面**——`core/save_manager.gd`(Core 层零 UI 零玩法依赖,不碰 ConfigDB/RngService 可独立验证):契约 1-4 全落(表与档分家·域界干净·钥石不丢·高版本拒读禁写)+ tmp+rename 原子写 + register_migration 迁移链骨架(v1 链空先行)+ 类型化访问器(keystone/spend 拒花零副作用/forge 三档出域拒/item/clear_count/boss_first_kill/recent)+ apply_adventure_summary 域更新原语(Boss 金额归元进度批扩第二参零行为兼容);smoke `_smoke_save` 段 **36 项**(①全域往返②新档合法③余额拒花④锻造出域拒⑤高版本拒读+禁写盘原文件字节不动⑥损坏 .corrupt.bak 备份+默认新档⑦迁移链 v0→v1⑧盘上无局内资源键〔ap/rap/gp/inventory/hero_hp〕⑩摘要 cap10 最新在前 + ⑨摘要集成=run_adventure(42,200,100) 真局→余额=3×keystone_per_level/通关次数全 1/三关首杀齐/recent seed42→二局全 2/翻倍/recent2→写盘读回保持);main.gd 挂点=_ready 表载成功后建实例+读盘(拒读/损坏类内兜底不阻断——fatal 只留数值表)+ _on_adventure_finished 尾部 apply+write(正式冒险收束唯一写档点;L2/L3 直入口走 runner.start_level、调试两层走 debug_session,均不经本处理器=契约 7 测试入口不写档结构性成立)。**文档批**——存档序列化.md 升 v1.0 定稿(格式/数据模型逐域/读写时机/版本与迁移/损坏兜底五项裁决成文,待设计六问全处置:1/2/3/5/6 入核心规则、4=单档 MVP 多档 P2)+ 02 状态列已定稿 + registry `save_file` 新条目(domains 逐域含内容 owner=元进度/冒险结构、承载 owner=存档序列化)+ `schema_version` 落地注记 + 06「存档契约条目的登记方式」解决移除(登记口径=registry 逐域登记)+ 元进度.md 存档义务指针改指 save_file(悬空引用清偿)+ roadmap 时间线 +1。**代拍板 8 条(规划书 §7,待用户确认)**:JSON 单档 user://save.json / 写盘时机=收束+营地每笔即时(B3 接线)+原子写 / 高版本拒读+禁写+内存新档 / 损坏改名备份+默认新档+警告不静默丢档 / recent_summaries cap10 / 单档 MVP 多档 P2 / 迁移读档链式 Callable+测试注册钩 / 测试入口不写档。**冲突点登记**:①计划书「registry save_format_version」实名 **schema_version**(owner=数据配置)——以代码为准注记落该条目;②开工门「B1 已合并落 master」开局为假(冒险结构批当时未提交未合并)——按用户冲突协议先落 T0-T4 与 T2①-⑧⑩、挂起轮询,B1 落籍(4f7f25d,台账 117)后分支 `git merge master`(冲突三处全解:smoke 注册表双留+段体保留/06 双删侧各去一条/roadmap 同日行合并)续完 T2⑨+T3。**验证**——冒烟 437→**518** 项 0 失败三闸全过(+36:26+⑩项摘要集成);四探针全过(ui_probe/tune PASS·click 问题=0·layout 转储 exit 0);doc 三闸与 master 对账一致(lint/term 逐字一致、link 死链 7=7 既有欠账、新增 4 链接全活);**MC 零玩法改动证明**=20 局 L1 normal unique seed0=4200 与当前 master 重跑读数行逐位一致(diff 仅日志时间戳/耗时/退出栈行号三处元数据);**坑**(已记 memory)=tools/run_smoke.py ROOT 取脚本自身位置,worktree 跑测必须显式 `--path`(否则静默跑主区,断言数不变是指纹);主区合并前 AdventureTable 类名缓存须 `--editor --quit` 刷新(十二坑第 1 条再实证)。**边界**——花钥石/购买/定价/营地 UI 零涉及归元进度实现批(B3 已解阻塞);续玩快照(主种子+计数器)P1 留 06 后置。 + +119. **放弃入口界面呈现批 + 台账 117 七项代拍板确认(2026-09-21,本会话——用户对冒险结构实现批代拍板七项逐项表态:①首页单关入口取消=认可现状;⑦「放弃接口也要有」=推翻 冒险结构.md「MVP 无界面入口」后置条款、补界面入口;②-⑥认可生效;worktree abandon-entry-20260921,代码 a569f19+文档 7d50e0f)**:**落地面**——①RouteOverlay 常驻「🏳 放弃冒险(同失败结算——已获大奖励保留)」按钮(FLAG_RED 暗示语义;信号 abandon_requested→组合根 _on_abandon_requested:非冒险模式零操作〔单关测试直入口语义对象不存在〕、受理→adventure.abandon()(结算摘要 abandoned→adventure_finished 同步到达→终局屏「冒险放弃 · 未打穿」);拒绝分支〔战斗在飞——选路停点天然非战斗期,本分支仅防御〕→ux_reject 日志+toast「战斗进行中——无法放弃」可见);②可见性归组合根 set_abandon_available:_start_adventure(true)/单关直入口(false)/默认 false——放弃语义对象=整局冒险;③**顺手收口**——_on_level_finished/_on_adventure_finished 终局兜底 route_overlay 收层由裸 visible=false 改 _close_modal(放弃按钮新场景下选路层在开态,须恢复其遮挡面〔tune/start 等上层〕;modal_stack.close 对未开过层容错);④ui_probe 新增三断言(默认隐藏/冒险模式可见+首战中真实按钮按下被拒 state 保持 running——规范 3.1/3.2 场景级探针义务)。**文档面**——冒险结构.md v0.7(放弃节正文改现行口径〔入口挂选路覆盖层、冒险模式开放、战斗中不可达〕/验收标准放弃条补入口可见性/P1 后置表「放弃入口的界面呈现」移除〔同表「两关序列生效」系 117 漏收口、顺手同口径移除〕);roadmap 时间线补 09-18/09-19 两缺行(里程碑粒度自台账回填)+09-21 行补本批;README 运行节补放弃按钮一行。**边界**——battle 中无入口不可达=拒绝分支零 UI 触达路径(GDD「战斗域单事务在飞」语义不变);B2 存档批的冒险收束写盘挂点对 abandoned 结局同样触发(apply_adventure_summary 收 abandoned 摘要——存档契约天然覆盖)。**验证**——冒烟 518 项 0 失败三闸全过(新基线零增量断言,纯呈现层+文档);四探针全过(ui_probe 含新三断言/tune/click 问题=0/layout exit 0);doc 三闸与 master 一致(死链 7=7 既有欠账、链接 789→791 全为有效新链、lint 8 文件既有、术语 215/1 既有)。