feat(core): 冒险结构实现批——AdventureRunner+LevelRunner账本外置+AdventureTable建表,三关正式序列生效(冒烟482全绿)
This commit is contained in:
1 parent
1d9d4c4799
commit
7ddfa4b366
13 files changed
+826
-21
No files matched your search
@@ -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 旧路径同种子逐位一致)。
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://lem5anpkcd0p
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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, {})
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmg04glgweakh
|
||||
@@ -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)。
|
||||
@@ -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))]
|
||||
@@ -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("战斗启动失败——见输出日志")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 格高——
|
||||
|
||||
Reference in new issue
Block a user