iter10 T3-T9: 奖励重构实装——adventure表扩列(回血20/池六种/权重/上限/同调位过渡键)+boss_keystone重锚3-2+rng扩签reward_pick+E-S加权洗牌+三选一停点(零自动推进/可中断)+加算区四轴AP(BattleOrchestrator排队兑现)+钥石逐杀掉落(收束无二次派生)+同调位约束(停点替换两路/战斗沉没/节点拒绝)+BonusPickOverlay+Narrator文案族+save v3迁移+冒烟619/0(+23断言)+断点P5往返单测+run_all 15套全绿
This commit is contained in:
1 parent
49b263fbc8
commit
71502ceecd
18 files changed
+992
-169
No files matched your search
+259
-25
@@ -41,8 +41,10 @@ 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 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子
|
||||
signal bonus_choice_presented(payload: Dictionary) # 提升三选一停点(非末关通关恰一次;迭代 10 大奖励重构——big_reward_paid 退役)——{level_index, candidates, heal_gained, hp_before, hp_after, keystone_gained, keystone_total, gp, aux_cleared}
|
||||
signal bonus_resolved(result: Dictionary) # 三选一兑付完成(pick_bonus/替换受理后恰一次)——{picked, value, hp_before, hp_after, level_index}
|
||||
signal aux_replace_presented(items: Array, incoming: Dictionary) # 同调位满位替换子交互(pick_bonus 道具件满位时)——items=现有持有件记录、incoming=新件记录
|
||||
signal adventure_finished(summary: Dictionary) # 冒险结算(completed/failed/abandoned)——summary 五件事:打穿没有/通关几关/止步第几关/钥石合计/主种子(+additive keystone_drops 迭代 10)
|
||||
# ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明;ADR-0001 S6 经 _forward_* 方法)----
|
||||
signal node_entered(node: Dictionary, info: Dictionary)
|
||||
signal choices_presented(candidates: Array)
|
||||
@@ -55,7 +57,7 @@ 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 bonus := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 统一冒险加算区(商店购买与三选一属性件同一本账;四轴 +ap——迭代 10 奖励重构)
|
||||
var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入)
|
||||
var level_index := 0 # 当前关卡序号(1 起)
|
||||
var levels_total := 0 # 序列总关数(读 level 表行数)
|
||||
@@ -72,6 +74,12 @@ var _last_aux_cleared := 0 # additive(迭代 7 T4——最近
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面)
|
||||
var _save_manager = null # 断点承载注入(迭代 8 卡①——null = 不写断点;模拟/冒烟缺省)
|
||||
var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递)
|
||||
# ---- 三选一停点态(迭代 10 奖励重构批)----
|
||||
var _bonus_taken := {} # 件获取次数计数(attack/max_hp/armor/ap——次数上限判定不可由加算区累加值反推,牌库剔除态依赖)
|
||||
var _bonus_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点
|
||||
var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付)
|
||||
var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
|
||||
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void:
|
||||
@@ -92,10 +100,11 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
master_seed = seed_val
|
||||
rng.new_adventure(seed_val)
|
||||
ledger = {"hero_hp": 0, "gp": 0}
|
||||
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
bonus = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0}
|
||||
purchased = {}
|
||||
keystone = 0
|
||||
summary = {}
|
||||
_reset_bonus_state()
|
||||
levels_total = db.level.rows.size()
|
||||
level_index = 1
|
||||
_pending_next_level = false
|
||||
@@ -132,21 +141,64 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
var bp_bonus: Dictionary = bp.get("bonus", {}) as Dictionary
|
||||
bonus = {"atk": maxi(0, int(bp_bonus.get("atk", 0))),
|
||||
"max_hp": maxi(0, int(bp_bonus.get("max_hp", 0))),
|
||||
"armor_class": maxi(0, int(bp_bonus.get("armor_class", 0)))}
|
||||
"armor_class": maxi(0, int(bp_bonus.get("armor_class", 0))),
|
||||
"ap": maxi(0, int(bp_bonus.get("ap", 0)))}
|
||||
purchased = (bp.get("shop_purchased", {}) as Dictionary).duplicate(true)
|
||||
keystone = int(bp.get("keystone", 0))
|
||||
summary = {}
|
||||
levels_total = db.level.rows.size()
|
||||
level_index = int(bp.get("level_index", 1))
|
||||
_pending_next_level = false
|
||||
_reset_bonus_state()
|
||||
var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary
|
||||
for id in _bonus_taken.keys():
|
||||
_bonus_taken[id] = maxi(0, int(bp_taken.get(id, 0)))
|
||||
for v in (bp.get("bonus_aux_carry", []) as Array):
|
||||
_bonus_aux_carry.append(String(v))
|
||||
_keystone_drops = (bp.get("keystone_drops", []) as Array).duplicate(true)
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
_first_script = false # 续玩模板已随快照强制(已抽模板零重抽)——脚本模式不重放
|
||||
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {}
|
||||
# 三选一停点续玩分支(迭代 10——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付)
|
||||
var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary
|
||||
var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary
|
||||
if not bp_pending.is_empty():
|
||||
_pending_next_level = true
|
||||
_bonus_pending = {"candidates": (bp_pending.get("candidates", []) as Array).duplicate(true)}
|
||||
var payload: Dictionary = (bp_pending.get("payload", {}) as Dictionary).duplicate(true)
|
||||
if bp_pending.has("incoming"):
|
||||
_bonus_pending["chosen"] = "aux_item"
|
||||
var incoming: Array = (bp_pending.get("incoming", []) as Array).duplicate(true)
|
||||
_bonus_pending["incoming"] = incoming
|
||||
state = "running"
|
||||
bonus_choice_presented.emit(payload)
|
||||
# 替换在途续发(呈现层直落替换子态——持有件记录按恢复后 carry 重建)
|
||||
aux_replace_presented.emit(_carry_records(),
|
||||
{"index": -1, "item_id": String(incoming[0]) if not incoming.is_empty() else "", "count": incoming.size()})
|
||||
return true
|
||||
state = "running"
|
||||
bonus_choice_presented.emit(payload)
|
||||
return true
|
||||
if not bp_result.is_empty():
|
||||
_pending_next_level = true
|
||||
_bonus_result = bp_result.duplicate(true)
|
||||
state = "running"
|
||||
bonus_resolved.emit(_bonus_result.duplicate(true))
|
||||
return true
|
||||
_resume_bp = bp.duplicate(true)
|
||||
state = "running"
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# 三选一停点态归零(冒险开始/结算收束共用)
|
||||
func _reset_bonus_state() -> void:
|
||||
_bonus_taken = {"attack": 0, "max_hp": 0, "armor": 0, "ap": 0}
|
||||
_bonus_pending = {}
|
||||
_bonus_result = {}
|
||||
_bonus_aux_carry = []
|
||||
_keystone_drops = []
|
||||
|
||||
|
||||
# TunePanel 百分比通道(测试面板覆盖——每关新 runner 都要应用;正式侧归元进度)
|
||||
func set_hero_override(atk_pct: int, hp_pct: int) -> void:
|
||||
_override = [atk_pct, hp_pct]
|
||||
@@ -170,16 +222,174 @@ 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
|
||||
if not _bonus_pending.is_empty():
|
||||
push_error("[冒险结构] continue_to_next_level 拒绝——三选一停点未兑付(零自动推进契约)")
|
||||
return false
|
||||
_pending_next_level = false
|
||||
_bonus_result = {}
|
||||
level_index += 1
|
||||
return _start_current_level()
|
||||
|
||||
|
||||
# ---- 提升三选一(迭代 10 奖励重构批——选择停点零自动推进、重复兑付被拒;兑付恰一件、未选项零生效)----
|
||||
|
||||
# 玩家点选兑付(呈现层/冒烟/自动模拟确认入口):id ∈ 停点候选。属性件写加算区+计数、AP 件写第四轴、
|
||||
# GP 件走钱包「奖励入账」入口、道具件受同调位约束(满位转替换子交互)。
|
||||
func pick_bonus(id: String) -> bool:
|
||||
if state != "running" or _bonus_pending.is_empty():
|
||||
push_error("[冒险结构] pick_bonus 拒绝——无待选停点(state=%s)" % state)
|
||||
return false
|
||||
var candidates: Array = _bonus_pending.get("candidates", []) as Array
|
||||
if not candidates.has(id):
|
||||
push_error("[冒险结构] pick_bonus 拒绝——件不在候选: %s" % id)
|
||||
return false
|
||||
var value: int = int(db.adventure.bonus_value(id))
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
match id:
|
||||
"attack":
|
||||
bonus["atk"] = int(bonus.get("atk", 0)) + value
|
||||
_bonus_taken["attack"] = int(_bonus_taken.get("attack", 0)) + 1
|
||||
"max_hp":
|
||||
bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value
|
||||
_bonus_taken["max_hp"] = int(_bonus_taken.get("max_hp", 0)) + 1
|
||||
"armor":
|
||||
bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value
|
||||
_bonus_taken["armor"] = int(_bonus_taken.get("armor", 0)) + 1
|
||||
"ap":
|
||||
bonus["ap"] = int(bonus.get("ap", 0)) + value
|
||||
_bonus_taken["ap"] = int(_bonus_taken.get("ap", 0)) + 1
|
||||
"gp":
|
||||
if _runner != null:
|
||||
_runner.grant_bonus_gp(value)
|
||||
"aux_item":
|
||||
return _pick_aux_item(value)
|
||||
_:
|
||||
push_error("[冒险结构] pick_bonus 拒绝——未知件: %s" % id)
|
||||
return false
|
||||
_finish_bonus_pick(id, value, hp_before)
|
||||
return true
|
||||
|
||||
|
||||
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)
|
||||
func _pick_aux_item(count: int) -> bool:
|
||||
var pool: Array = _bonus_aux_pool()
|
||||
if pool.is_empty():
|
||||
push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
|
||||
return false
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
var picked: Array[String] = []
|
||||
for i in maxi(1, count):
|
||||
picked.append(_roll_bonus_aux_item(pool))
|
||||
var slots := maxi(0, int(db.adventure.transitional_sync_slots))
|
||||
if _bonus_aux_carry.size() + picked.size() <= slots:
|
||||
for iid in picked:
|
||||
_bonus_aux_carry.append(iid)
|
||||
_finish_bonus_pick("aux_item", picked.size(), hp_before)
|
||||
return true
|
||||
# 满位替换子交互:现有持有件 + 新件 N+1 选 N(丢旧收新 / 放弃新件两路,终态恒 ≤ 同调位)
|
||||
_bonus_pending["chosen"] = "aux_item"
|
||||
_bonus_pending["incoming"] = picked.duplicate()
|
||||
aux_replace_presented.emit(_carry_records(), {"index": -1, "item_id": String(picked[0]), "count": picked.size()})
|
||||
return true
|
||||
|
||||
|
||||
# 替换子交互受理(discard_index = -1 放弃新件不获取;否则丢该件腾位收新)——终态恒 ≤ 同调位
|
||||
func resolve_aux_replace(discard_index: int) -> bool:
|
||||
if _bonus_pending.is_empty() or String(_bonus_pending.get("chosen", "")) != "aux_item":
|
||||
push_error("[冒险结构] resolve_aux_replace 拒绝——无替换在途")
|
||||
return false
|
||||
var incoming: Array = _bonus_pending.get("incoming", []) as Array
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
if discard_index >= 0:
|
||||
if discard_index >= _bonus_aux_carry.size():
|
||||
push_error("[冒险结构] resolve_aux_replace 拒绝——序号不在持有集: %d" % discard_index)
|
||||
return false
|
||||
_bonus_aux_carry.remove_at(discard_index)
|
||||
for iid in incoming:
|
||||
_bonus_aux_carry.append(String(iid))
|
||||
# 放弃新件(discard_index < 0)= 原持有集不动、新件不获取
|
||||
_finish_bonus_pick("aux_item", incoming.size(), hp_before)
|
||||
return true
|
||||
|
||||
|
||||
# 兑付收口(恰一次):清停点态 → 记已兑付态 → emit → 停点快照(等继续确认——中断续玩不重兑付)
|
||||
func _finish_bonus_pick(picked: String, value: int, hp_before: int) -> void:
|
||||
_bonus_pending = {}
|
||||
_bonus_result = {
|
||||
"picked": picked,
|
||||
"value": value,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": int(ledger.get("hero_hp", 0)),
|
||||
"level_index": level_index,
|
||||
}
|
||||
bonus_resolved.emit(_bonus_result.duplicate(true))
|
||||
_snapshot_breakpoint()
|
||||
|
||||
|
||||
# 加权洗牌不放回掷 3(E-S weight-key:每件 key = U(0,1)^(1/权重) 取前 3——单次零重复、逐件按权重出
|
||||
# 概率;达次数上限件先剔牌库〔AP 用尽 2 次/属性件达 3 次永不再现;GP/道具件无次数上限,牌库恒非空〕;
|
||||
# 道具件解锁池空集〔白板/门控态〕不入牌库。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同三候选)
|
||||
func _draw_bonus_candidates() -> Array:
|
||||
var deck: Array = []
|
||||
for id in db.adventure.BONUS_IDS:
|
||||
var cap := int(db.adventure.bonus_cap(String(id)))
|
||||
if cap != db.adventure.BONUS_CAP_UNLIMITED and int(_bonus_taken.get(String(id), 0)) >= cap:
|
||||
continue
|
||||
if String(id) == "aux_item" and _bonus_aux_pool().is_empty():
|
||||
continue
|
||||
deck.append(String(id))
|
||||
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
|
||||
var keyed: Array = []
|
||||
for id in deck:
|
||||
var w := float(maxi(1, int(db.adventure.bonus_weight(id))))
|
||||
keyed.append({"id": id, "key": pow(rg.randf(), 1.0 / w)})
|
||||
keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"]))
|
||||
var out: Array = []
|
||||
for i in mini(3, keyed.size()):
|
||||
out.append(String(keyed[i]["id"]))
|
||||
return out
|
||||
|
||||
|
||||
# 三选一道具件随机池(与投放门控同源:aux 活行 ∩ 解锁门控集——空集 = 道具件不入牌库)
|
||||
func _bonus_aux_pool() -> Array:
|
||||
var out: Array = []
|
||||
var live: Dictionary = db.aux_live_ids()
|
||||
for iid in live.keys():
|
||||
if _aux_unlocked.has(String(iid)):
|
||||
out.append(String(iid))
|
||||
return out
|
||||
|
||||
|
||||
func _roll_bonus_aux_item(pool: Array) -> String:
|
||||
var rg: RandomNumberGenerator = rng.open_stream("reward_pick")
|
||||
return String(pool[rg.randi_range(0, pool.size() - 1)])
|
||||
|
||||
|
||||
func _carry_records() -> Array:
|
||||
var out: Array = []
|
||||
for i in _bonus_aux_carry.size():
|
||||
out.append({"index": i, "item_id": _bonus_aux_carry[i]})
|
||||
return out
|
||||
|
||||
|
||||
# Boss 钥石逐杀掉落(迭代 10——渠道改 Boss/精英击杀掉落;首杀/复杀两档读 meta.tres boss_keystone;
|
||||
# 首杀判据 = 存档 boss_first_kill 标记位〔断点未注入 = 恒首杀——模拟/冒烟零扰动;标记仍在收束
|
||||
# apply_adventure_summary 逐通关关次置位〕;末关 Boss 照常入账;明细随摘要 additive 键供结算面归因)
|
||||
func _boss_keystone_drop(li: int) -> void:
|
||||
var first := true
|
||||
if _save_manager != null:
|
||||
first = not _save_manager.boss_first_kill_marked(li)
|
||||
var row: Dictionary = db.meta_boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
_keystone_drops.append({"level": li, "first": first, "amount": amount})
|
||||
keystone += amount
|
||||
|
||||
|
||||
# 主动放弃(仅非战斗期受理——战斗域单事务在飞;与失败同型结算,已获大奖励不追回。
|
||||
# MVP 无界面入口,语义先行预留;结算受理只算一次——非 running 态拒绝)
|
||||
func abandon() -> bool:
|
||||
@@ -222,6 +432,11 @@ func _start_current_level() -> bool:
|
||||
var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased,
|
||||
Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state)
|
||||
if ok:
|
||||
# 三选一道具件跨关携带注入(迭代 10——上关兑付的道具件进本关库存〔奖励只朝下一战生效〕;
|
||||
# 注入即移交——carry 清空,本关末随关卡域库存清空)
|
||||
for iid in _bonus_aux_carry:
|
||||
_runner.grant_aux_item(iid, "bonus_pick")
|
||||
_bonus_aux_carry = []
|
||||
level_started.emit(level_index, String(_runner.level_row.get("display_name", "")))
|
||||
# 入口停点断点(node_id=-1)与 defer_entry 同态(node_index=-1+单候选)——走入口呈现;
|
||||
# 中途停点(node_id>=0)走停点呈现(restore_aux_items 先于呈现=C 区库存就位)。
|
||||
@@ -249,25 +464,34 @@ func _on_level_finished(payload: Dictionary) -> void:
|
||||
push_error("[冒险结构] 关卡结束结果出域: %s" % result)
|
||||
return
|
||||
_last_aux_cleared = int(payload.get("aux_cleared", 0)) # additive(迭代 7 T4——末关 summary 取数面)
|
||||
# Boss 钥石逐杀掉落(迭代 10——渠道改击杀掉落,末关照常入账;首杀/复杀读 meta.tres boss_keystone)
|
||||
_boss_keystone_drop(level_index)
|
||||
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(_base_override["max_hp"]) + int(bonus["max_hp"])
|
||||
keystone += int(db.adventure.keystone_per_level)
|
||||
# 通关兑付(非末关恰一次;迭代 10 三层奖励框架):部分回血先行 → 加权洗牌掷 3 → 三选一停点
|
||||
#(顺序即契约——回血按当刻账本与上限结算,选择停点零自动推进)
|
||||
var hp_before := int(ledger.get("hero_hp", 0))
|
||||
var max_eff := int(db.hero.max_hp) + int(_base_override["max_hp"]) + int(bonus.get("max_hp", 0))
|
||||
var heal := mini(int(floor(float(max_eff) * float(int(db.adventure.clear_heal_pct)) / 100.0)), max_eff - hp_before)
|
||||
ledger["hero_hp"] = hp_before + heal
|
||||
# 置位先于 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(_base_override["max_hp"]) + int(bonus["max_hp"]),
|
||||
"keystone_gained": int(db.adventure.keystone_per_level),
|
||||
var pick_payload: Dictionary = {
|
||||
"level_index": level_index,
|
||||
"candidates": _draw_bonus_candidates(),
|
||||
"heal_gained": heal,
|
||||
"hp_before": hp_before,
|
||||
"hp_after": int(ledger.get("hero_hp", 0)),
|
||||
"keystone_gained": int(_keystone_drops.back().get("amount", 0)) if not _keystone_drops.is_empty() else 0,
|
||||
"keystone_total": keystone,
|
||||
"gp": int(ledger.get("gp", 0)),
|
||||
"aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数)
|
||||
})
|
||||
}
|
||||
_bonus_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
|
||||
"payload": pick_payload.duplicate(true)}
|
||||
bonus_choice_presented.emit(pick_payload)
|
||||
_snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
|
||||
return
|
||||
# 末关通关:钥石照记、不发上限提升(没有下一战可受益)——冒险完成结算
|
||||
keystone += int(db.adventure.keystone_per_level)
|
||||
# 末关通关:不发三选一与部分回血(没有下一战可受益)、Boss 钥石照常入账——冒险完成结算
|
||||
_settle("completed")
|
||||
|
||||
|
||||
@@ -280,7 +504,8 @@ func _settle(result: String) -> void:
|
||||
"stopped_at": 0 if result == "completed" else level_index,
|
||||
"keystone_total": keystone,
|
||||
"master_seed": master_seed,
|
||||
"aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 big_reward 载荷)
|
||||
"aux_cleared_last": _last_aux_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷)
|
||||
"keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源)
|
||||
}
|
||||
# 败退明细(数值验证工具取数面 2026-09-22——tools/balance_verifier.py 止步节点分布数据源;
|
||||
# 仅失败局附加,键缺省 = 未败退,既有五件事消费方零感知)
|
||||
@@ -292,6 +517,7 @@ func _settle(result: String) -> void:
|
||||
}
|
||||
if _runner != null:
|
||||
_runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现)
|
||||
_reset_bonus_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化)
|
||||
if _save_manager != null:
|
||||
_save_manager.clear_breakpoint() # 三结局清断点(迭代 8 卡①/T3——退出冒险=放弃流程+清除断点)
|
||||
_save_manager.write_to_disk()
|
||||
@@ -317,28 +543,36 @@ func _forward_choices_presented(candidates: Array) -> void:
|
||||
# 钥石累计(大奖励中途值——终局摘要五件事不失真)、已到节点序(呈现层图条重建)、道具库存
|
||||
# (奖励节点已选件——防静默丢失)。注入缺省 null = 整面静默(模拟/冒烟零扰动)。
|
||||
func _snapshot_breakpoint() -> void:
|
||||
if _save_manager == null or state != "running" or _runner == null:
|
||||
if _save_manager == null or state != "running":
|
||||
return
|
||||
var aux_items: Array = []
|
||||
if _runner.aux_inventory != null:
|
||||
if _runner != null and _runner.aux_inventory != null:
|
||||
aux_items = _runner.aux_inventory.snapshot_inventory()
|
||||
# 三选一停点续玩分支 _runner 为 null(关卡已收束、下一关未开)——节点域字段按缺省落档,
|
||||
# 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照,迭代 10)
|
||||
var bp: Dictionary = {
|
||||
"level_index": level_index,
|
||||
"node_id": _runner.current_node_id(),
|
||||
"node_id": _runner.current_node_id() if _runner != null else -1,
|
||||
"hero_hp": int(ledger.get("hero_hp", 0)),
|
||||
"bonus": {
|
||||
"atk": int(bonus.get("atk", 0)),
|
||||
"max_hp": int(bonus.get("max_hp", 0)),
|
||||
"armor_class": int(bonus.get("armor_class", 0)),
|
||||
"ap": int(bonus.get("ap", 0)),
|
||||
},
|
||||
"gp": int(ledger.get("gp", 0)),
|
||||
"shop_purchased": purchased.duplicate(true),
|
||||
"template_pick": _runner.active_template_index,
|
||||
"template_pick": _runner.active_template_index if _runner != null else -1,
|
||||
"master_seed": master_seed,
|
||||
"rng_counter": rng.counter(),
|
||||
"keystone": keystone,
|
||||
"visited_ids": _runner.visited_ids(),
|
||||
"visited_ids": _runner.visited_ids() if _runner != null else [],
|
||||
"aux_items": aux_items,
|
||||
"bonus_taken": _bonus_taken.duplicate(true), # 三选一件获取次数(上限判定不可由加算区反推——迭代 10)
|
||||
"bonus_pending": _bonus_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约)
|
||||
"bonus_result": _bonus_result.duplicate(true), # 已兑付待推进态(空 = 无——中断续玩不重兑付)
|
||||
"bonus_aux_carry": _bonus_aux_carry.duplicate(true), # 道具件跨关携带(注入前中断防静默丢失)
|
||||
"keystone_drops": _keystone_drops.duplicate(true), # 逐杀掉落明细(摘要归因续接——迭代 10)
|
||||
}
|
||||
_save_manager.set_breakpoint(bp)
|
||||
_save_manager.write_to_disk()
|
||||
|
||||
@@ -64,6 +64,15 @@ func inventory_count() -> int:
|
||||
return _items.size()
|
||||
|
||||
|
||||
# 持有数(迭代 10 奖励重构批——同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有)
|
||||
func holding_count() -> int:
|
||||
var n := 0
|
||||
for rec in _items:
|
||||
if not bool(rec.get("used", false)):
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
# 板级零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级):
|
||||
# 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用);
|
||||
# 纯预读行 = 板上还有未开格才可用。
|
||||
|
||||
@@ -130,6 +130,12 @@ func begin_battle(enc: Dictionary) -> bool:
|
||||
if not _install_new_board():
|
||||
return false
|
||||
_reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险)
|
||||
# 迭代 10 奖励重构批——跨冒险行动点加成轴(统一冒险加算区第四轴):经排队修正通道兑现
|
||||
# 「每场战斗开始首轮 AP +加成值」(queue_ap_delta = 下轮开轮基线加算、notify_round_start 兑现后
|
||||
# 清零——首轮即消费,后续轮次不重复兑现;语义锚 = 行动经济 GDD「跨冒险行动点加成轴」节)
|
||||
var ap_bonus: int = int(enc.get("hero_ap_bonus", 0))
|
||||
if ap_bonus > 0:
|
||||
action_economy.queue_ap_delta(ap_bonus)
|
||||
if not action_economy.notify_round_start():
|
||||
_fatal("行动经济 首轮 notify_round_start 被拒")
|
||||
return false
|
||||
@@ -366,6 +372,13 @@ func _grant_aux(snap: Dictionary, polarity: String, source_class: String, cell:
|
||||
{"effect_id": "", "aux_sink": String(snap.get("effect_id", ""))}, "")
|
||||
return false
|
||||
var iid := String(snap.get("effect_id", ""))
|
||||
# 同调位持有上限(迭代 10 奖励重构批过渡承载——transitional_sync_slots,provisional 1):
|
||||
# 战斗内渠道遇满位沉没不入(战斗域零交互纪律——替换交互归停点渠道;正主 = 同调位谓词表,
|
||||
# 全渠道统一收口随魔法道具同调实施批)。逐行归因纪律:沉没行照发(「获得」的零兑付形态)。
|
||||
if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)):
|
||||
_emit_row("effect", source_class, cell, ri, "同调位已满,%s 沉没" % String(db.aux_item_row(iid).get("display_name", iid)), 0,
|
||||
{"effect_id": "", "aux_sink": iid, "sink_reason": "sync_slots_full"}, "")
|
||||
return false
|
||||
var arow: Dictionary = db.aux_item_row(iid)
|
||||
var rec: Dictionary = aux_inventory.add_item(iid, source_class, cell, ri)
|
||||
_emit_row("effect", source_class, cell, ri, "获得 %s" % String(arow.get("display_name", iid)), 1,
|
||||
|
||||
@@ -105,6 +105,12 @@ func effective_ac() -> int:
|
||||
return maxi(0, int(db.hero.armor_class) + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0)))
|
||||
|
||||
|
||||
# 行动点加成生效值(迭代 10 奖励重构批——统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus
|
||||
# 传入战斗域,兑现 = 每场战斗开始首轮行动点基线 +加成值〔行动经济 GDD 跨冒险行动点加成轴节〕)
|
||||
func effective_ap_bonus() -> int:
|
||||
return maxi(0, int(bonus.get("ap", 0)))
|
||||
|
||||
|
||||
# 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留)
|
||||
func restore_hero_base() -> void:
|
||||
hero_atk_pct = 0
|
||||
@@ -452,7 +458,8 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
node_entered.emit(node, {})
|
||||
if not _settlement.begin_battle({"level_index": int(level_row.level_index), "node_depth": int(node.get("node_depth", 0)),
|
||||
"hero_hp": hero_hp, "hero_atk": effective_atk(), "hero_max_hp": effective_max_hp(),
|
||||
"hero_armor_class": effective_ac(), "node_type": String(node.get("type", ""))}):
|
||||
"hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(),
|
||||
"node_type": String(node.get("type", ""))}):
|
||||
push_error("[关卡节点地图] 战斗启动失败(depth %s)" % str(node.get("node_depth", -1)))
|
||||
return false
|
||||
return true
|
||||
@@ -550,13 +557,23 @@ func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
if _settlement != null:
|
||||
_settlement.set_aux_unlocked(ids)
|
||||
|
||||
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)
|
||||
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)。
|
||||
# 同调位持有上限(迭代 10 奖励重构批过渡承载):满位拒绝授予(奖励节点渠道过渡态 = 满位候选
|
||||
# 由呈现侧过滤不出;此处防御兜底)——替换交互归三选一渠道,全渠道统一收口随同调实施批。
|
||||
func grant_aux_item(item_id: String, source: String) -> Dictionary:
|
||||
if aux_inventory == null:
|
||||
aux_inventory = AuxItems.new(db)
|
||||
if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)):
|
||||
push_warning("[关卡节点地图] 同调位已满,授予拒绝: %s(%s)" % [item_id, source])
|
||||
return {}
|
||||
return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
|
||||
|
||||
return _settlement
|
||||
|
||||
# 三选一 GP 袋件入账(迭代 10——钱包收口扩「奖励入账」入口;复用战斗完成同一入账原语,
|
||||
# wallet_changed 载荷带 source 供呈现归因)
|
||||
func grant_bonus_gp(amount: int) -> void:
|
||||
gp = maxi(0, gp + amount)
|
||||
wallet_changed.emit("gain", gp, {"amount": amount, "source": "bonus_pick"})
|
||||
|
||||
|
||||
# ---- 玩家输入转发(呈现层只与 runner 对话)----
|
||||
|
||||
@@ -116,25 +116,21 @@ func refresh_workshop_unlocks() -> Array:
|
||||
|
||||
|
||||
# ---- Boss 两档与收束入账(结算屏文案数据源)----
|
||||
# 明细 {big_reward, boss_drops: [{level, first, amount}], total, new_unlocks};
|
||||
# 入账与标记经 save.apply_adventure_summary(boss_drops 副本随摘要入 recent_summaries)。
|
||||
# 明细 {big_reward, boss_drops, total, new_unlocks};入账与标记经 save.apply_adventure_summary
|
||||
# (keystone_drops 副本随摘要入 recent_summaries)。
|
||||
# 迭代 10 奖励重构批:钥石改 Boss 逐杀掉落——掉落金额已在冒险内逐杀入账(summary.keystone_total
|
||||
# 含全部掉落;首杀/复杀裁决在 AdventureRunner._boss_keystone_drop,首杀标记仍在下方
|
||||
# apply_adventure_summary 逐通关关次置位)。本函数不再派生掉落金额,只做标记+入账+归档。
|
||||
func settle_adventure(summary: Dictionary) -> Dictionary:
|
||||
# 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束)
|
||||
save.mark_first_adventure_done()
|
||||
var drops: Array[Dictionary] = []
|
||||
for li in range(1, int(summary.get("levels_cleared", 0)) + 1):
|
||||
var first: bool = not save.boss_first_kill_marked(li)
|
||||
var row: Dictionary = db.meta_boss_keystone_row(li)
|
||||
var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0))
|
||||
drops.append({"level": li, "first": first, "amount": amount})
|
||||
var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true)
|
||||
save.apply_adventure_summary(summary, drops)
|
||||
var new_unlocks := refresh_workshop_unlocks()
|
||||
save.write_to_disk()
|
||||
var total := int(summary.get("keystone_total", 0))
|
||||
for d in drops:
|
||||
total += int(d["amount"])
|
||||
return {
|
||||
"big_reward": int(summary.get("keystone_total", 0)),
|
||||
"big_reward": total,
|
||||
"boss_drops": drops,
|
||||
"total": total,
|
||||
"new_unlocks": new_unlocks,
|
||||
|
||||
@@ -15,7 +15,7 @@ class_name RngServiceCore
|
||||
extends Node
|
||||
|
||||
# 用途标签封闭词表(并流前七流名全保留;新增用途走扩签通道 = 改代码 + 评审——清单封闭语义由「流」改「标签」)
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5——标签不参与派生只进事件日志,派生/黄金向量零改动)
|
||||
const PURPOSE_TAGS := ["board_gen", "reward_roll", "encounter", "armor_roll", "behavior_roll", "variant_place", "gp_drop", "node_map", "reward_pick"] # node_map = 关卡节点图生成 v0.4 扩签(M-3 迭代 5 T5);reward_pick = 提升三选一加权洗牌扩签(M-3 迭代 10 奖励重构批——每关大奖励抽签恰一 scope:E-S weight-key 每候选一取,标签不参与派生只进事件日志,派生/黄金向量零改动)
|
||||
|
||||
# 分段表达式书写(数据配置 R3 实现级规约——禁 ≥2^63 完整字面量)
|
||||
const GOLDEN_RATIO := (0x9E3779B9 << 32) | 0x7F4A7C15 # 0x9E3779B97F4A7C15
|
||||
|
||||
@@ -30,8 +30,8 @@
|
||||
class_name SaveManager
|
||||
extends RefCounted
|
||||
|
||||
const SAVE_FORMAT_VERSION := 2 # registry schema_version:v2=断点域出生(M-3 迭代 8 卡①;v1=首版;
|
||||
# 高版本拒读 owner=存档序列化)
|
||||
const SAVE_FORMAT_VERSION := 3 # registry schema_version:v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构;
|
||||
# 旧档缺键 = 合法空态);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化
|
||||
const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前)
|
||||
|
||||
const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合法键(元进度.md 属性锻造柜台)
|
||||
@@ -47,6 +47,7 @@ func _init(path: String = "user://save.json") -> void:
|
||||
_data = _default_data()
|
||||
# 生产迁移链自注册(v2 起——测试钩 register_migration 仍可叠挂更早版本步骤)
|
||||
register_migration(1, _migrate_v1_v2)
|
||||
register_migration(2, _migrate_v2_v3)
|
||||
|
||||
|
||||
# v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务)
|
||||
@@ -57,6 +58,14 @@ func _migrate_v1_v2(data: Dictionary) -> Dictionary:
|
||||
return data
|
||||
|
||||
|
||||
# v2→v3:断点附随件扩列(迭代 10 奖励重构批——bonus_taken/bonus_pending/bonus_result/
|
||||
# bonus_aux_carry/keystone_drops + bonus.ap 轴)与摘要 additive 键 keystone_drops——旧档缺键
|
||||
# = 合法空态(读取方 .get 兜底),无结构迁移体;版本号推进仅为 schema_version 对齐
|
||||
func _migrate_v2_v3(data: Dictionary) -> Dictionary:
|
||||
data["version"] = SAVE_FORMAT_VERSION
|
||||
return data
|
||||
|
||||
|
||||
# ---- 默认数据模型(新档合法态——全零;白板存档 = 完整可玩〔元进度 单局循环零依赖〕)----
|
||||
|
||||
func _default_data() -> Dictionary[String, Variant]:
|
||||
@@ -203,14 +212,11 @@ func recent_summaries() -> Array:
|
||||
|
||||
|
||||
# 域更新原语(正式冒险收束唯一入账口——组合根经本函数入账,禁直改内部结构):
|
||||
# 钥石入账(summary.keystone_total + boss_drops 逐笔 amount)+ 逐通关关次 clear_count +1 与
|
||||
# Boss 首杀标记 + 摘要头部插入(cap 10,最新在前,元素附 boss_drops 副本——复盘可归因)。
|
||||
# boss_drops 归元进度批消费(两档金额读 meta 表——MetaProgression.settle_adventure 组装;
|
||||
# 缺省 [] 零行为兼容——调用方不带两档时入账面与批前逐位一致)。
|
||||
# 钥石入账(summary.keystone_total——迭代 10 起逐杀掉落已在冒险内计入,boss_drops 参数降级为
|
||||
# 摘要归档副本不重复入账)+ 逐通关关次 clear_count +1 与 Boss 首杀标记 + 摘要头部插入
|
||||
# (cap 10,最新在前,元素附 keystone_drops 副本——复盘可归因)。
|
||||
func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> void:
|
||||
add_keystone(int(summary.get("keystone_total", 0)))
|
||||
for drop in boss_drops:
|
||||
add_keystone(int(drop.get("amount", 0)))
|
||||
var cleared := int(summary.get("levels_cleared", 0))
|
||||
for li in range(1, cleared + 1):
|
||||
var key := str(li)
|
||||
|
||||
@@ -1,23 +1,98 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完,通关兑付的大奖励
|
||||
# 与钥石合计在冒险摘要里成立(P0)
|
||||
# Date: 2026-09-21(冒险结构实现批——AdventureTable 建表,registry adventure_table 条目 2026-09-15 登记)
|
||||
# Validation Question: 三关连探冒险(地精洞窟 → 幽暗地域 → 灰矮人矿城)能否以一条命走完,通关兑付的
|
||||
# 部分回血与提升三选一、Boss 逐杀钥石掉落在冒险摘要里成立(P0)
|
||||
# Date: 2026-09-21(冒险结构实现批建表;2026-10-01 迭代 10 奖励重构批扩列——registry adventure_table 条目同批改写)
|
||||
# 数据配置 AdventureTable schema(冒险结构 关卡大奖励行值;单行全局,Rule 4 豁免——HeroTable/SettlementTable 先例)。
|
||||
# 两字段 knob(调参表归 冒险结构;数值所有权归 难度曲线与Boss 曲线调参批,全部 provisional):
|
||||
# level_clear_hp_bonus = 关卡大奖励 HP 上限提升(写统一冒险加算区 max_hp 轴——与商店购买同一本账,
|
||||
# 经 begin_battle 覆盖值通道对后续每战生效;值锚 = 难度曲线与Boss 区间平移换算率,上调须重扫通关率带)。
|
||||
# keystone_per_level = 每通关一关钥石入账值(末关通关照记;消费方 元进度——本表只记账不做消费)。
|
||||
# 迭代 10 重构(2026-09-30 用户拍板案丙):大奖励「固定三件套」退役——level_clear_hp_bonus(血回满+上限提升件)
|
||||
# 与 keystone_per_level(钥石每关入账)两字段退役;改三层奖励框架 = 部分回血(clear_heal_pct)+ 提升三选一
|
||||
# (池六种:件值/权重/次数上限三组列)+ Boss/精英击杀钥石掉落(行值归 meta.tres boss_keystone,不在本表)。
|
||||
# 全部 knob provisional-pending-曲线调参批(数值所有权归 难度曲线与Boss,上调须重扫通关率带)。
|
||||
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——每通关一关钥石入账
|
||||
## 三选一提升池件 id 词表(封闭——断点附随件与牌库剔除态依赖它;扩件走本表+registry 修订)
|
||||
const BONUS_IDS: Array[String] = ["attack", "max_hp", "armor", "gp", "aux_item", "ap"]
|
||||
## 无次数上限件的 cap 哨兵值(gp/aux_item——牌库恒非空的结构保证)
|
||||
const BONUS_CAP_UNLIMITED := -1
|
||||
|
||||
# —— 通关部分回血(固定型,非末关通关兑付)——
|
||||
@export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满
|
||||
|
||||
# —— 三选一件值(兑付面)——
|
||||
@export var bonus_attack_flat: int = 1 # [0,999] knob——攻击轴累加(覆盖值通道)
|
||||
@export var bonus_max_hp_flat: int = 5 # [0,999] knob——生命上限轴累加(不连带当前生命)
|
||||
@export var bonus_armor_flat: int = 1 # [0,999] knob——护甲轴累加
|
||||
@export var bonus_gp_amount: int = 20 # [0,999] knob——GP 袋定额入钱包
|
||||
@export var bonus_ap_flat: int = 1 # [0,10] knob——AP 件每场战斗首轮行动点基线增量
|
||||
@export var bonus_aux_item_count: int = 1 # [1,9] knob——魔法道具件授予件数(受同调位约束)
|
||||
|
||||
# —— 三选一权重(加权洗牌不放回,E-S weight-key)——
|
||||
@export var bonus_weight_attack: int = 3 # [1,99] knob
|
||||
@export var bonus_weight_max_hp: int = 3 # [1,99] knob
|
||||
@export var bonus_weight_armor: int = 3 # [1,99] knob
|
||||
@export var bonus_weight_gp: int = 3 # [1,99] knob
|
||||
@export var bonus_weight_aux: int = 1 # [1,99] knob——魔法道具件(受同调位约束)
|
||||
@export var bonus_weight_ap: int = 1 # [1,99] knob——AP 件(全冒险最紧上限)
|
||||
|
||||
# —— 全冒险获取次数上限(达上限件先剔出牌库;gp/aux_item 无上限)——
|
||||
@export var bonus_cap_attack: int = 3 # [0,99] knob
|
||||
@export var bonus_cap_max_hp: int = 3 # [0,99] knob
|
||||
@export var bonus_cap_armor: int = 3 # [0,99] knob
|
||||
@export var bonus_cap_ap: int = 2 # [0,99] knob——全冒险最紧
|
||||
|
||||
# —— 同调位过渡读数源(正主 = attunement_growth.tres 谓词表,同调实施批落地后切换)——
|
||||
@export var transitional_sync_slots: int = 1 # [0,9] knob——道具持有上限
|
||||
|
||||
|
||||
func bonus_value(id: String) -> int:
|
||||
match id:
|
||||
"attack": return bonus_attack_flat
|
||||
"max_hp": return bonus_max_hp_flat
|
||||
"armor": return bonus_armor_flat
|
||||
"gp": return bonus_gp_amount
|
||||
"ap": return bonus_ap_flat
|
||||
"aux_item": return bonus_aux_item_count
|
||||
return 0
|
||||
|
||||
|
||||
func bonus_weight(id: String) -> int:
|
||||
match id:
|
||||
"attack": return bonus_weight_attack
|
||||
"max_hp": return bonus_weight_max_hp
|
||||
"armor": return bonus_weight_armor
|
||||
"gp": return bonus_weight_gp
|
||||
"aux_item": return bonus_weight_aux
|
||||
"ap": return bonus_weight_ap
|
||||
return 0
|
||||
|
||||
|
||||
## 次数上限(gp/aux_item 返回 BONUS_CAP_UNLIMITED = 无上限,牌库恒非空的结构保证)
|
||||
func bonus_cap(id: String) -> int:
|
||||
match id:
|
||||
"attack": return bonus_cap_attack
|
||||
"max_hp": return bonus_cap_max_hp
|
||||
"armor": return bonus_cap_armor
|
||||
"ap": return bonus_cap_ap
|
||||
return BONUS_CAP_UNLIMITED
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
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})
|
||||
if clear_heal_pct < 0 or clear_heal_pct > 100:
|
||||
errs.append({"category": "domain", "message": "clear_heal_pct 越域 [0,100]: %d" % clear_heal_pct})
|
||||
for id in BONUS_IDS:
|
||||
var v := bonus_value(id)
|
||||
if v < 0 or v > 999:
|
||||
errs.append({"category": "domain", "message": "bonus_value(%s) 越域 [0,999]: %d" % [id, v]})
|
||||
var w := bonus_weight(id)
|
||||
if w < 1 or w > 99:
|
||||
errs.append({"category": "domain", "message": "bonus_weight(%s) 越域 [1,99]: %d" % [id, w]})
|
||||
for id in ["attack", "max_hp", "armor", "ap"]:
|
||||
var c := bonus_cap(id)
|
||||
if c < 0 or c > 99:
|
||||
errs.append({"category": "domain", "message": "bonus_cap(%s) 越域 [0,99]: %d" % [id, c]})
|
||||
if bonus_aux_item_count < 1 or bonus_aux_item_count > 9:
|
||||
errs.append({"category": "domain", "message": "bonus_aux_item_count 越域 [1,9]: %d" % bonus_aux_item_count})
|
||||
if transitional_sync_slots < 0 or transitional_sync_slots > 9:
|
||||
errs.append({"category": "domain", "message": "transitional_sync_slots 越域 [0,9]: %d" % transitional_sync_slots})
|
||||
return errs
|
||||
@@ -4,11 +4,30 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
level_clear_hp_bonus = 5
|
||||
keystone_per_level = 10
|
||||
clear_heal_pct = 20
|
||||
bonus_attack_flat = 1
|
||||
bonus_max_hp_flat = 5
|
||||
bonus_armor_flat = 1
|
||||
bonus_gp_amount = 20
|
||||
bonus_ap_flat = 1
|
||||
bonus_aux_item_count = 1
|
||||
bonus_weight_attack = 3
|
||||
bonus_weight_max_hp = 3
|
||||
bonus_weight_armor = 3
|
||||
bonus_weight_gp = 3
|
||||
bonus_weight_aux = 1
|
||||
bonus_weight_ap = 1
|
||||
bonus_cap_attack = 3
|
||||
bonus_cap_max_hp = 3
|
||||
bonus_cap_armor = 3
|
||||
bonus_cap_ap = 2
|
||||
transitional_sync_slots = 1
|
||||
|
||||
; 冒险结构实现批(2026-09-21 建表——registry adventure_table 条目同批注记更新):
|
||||
; 关卡大奖励三件套的数值半边(上限提升)+ 钥石每关入账值;单行全局 Rule 4 豁免(HeroTable/SettlementTable 先例)。
|
||||
; 大奖励 = 非末关通关恰一次:上限提升(写统一冒险加算区 max_hp 轴)+ 血回满(= 新上限)+ 钥石入账;
|
||||
; 末关通关不发上限提升、钥石照记(设计意图:奖励只朝下一战生效)。钥石消费方 = 元进度(本表只记账)。
|
||||
; 两值全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss)。
|
||||
; 迭代 10 奖励重构批(2026-10-01 扩列——registry adventure_table 条目同批改写):
|
||||
; 大奖励 = 非末关通关恰一次:部分回血(clear_heal_pct,封顶上限不回满、满血回 0 合法)
|
||||
; + 提升三选一(池六种加权洗牌不放回 E-S weight-key 掷 3,玩家点选兑付恰一件;消费 reward_pick 封闭流)。
|
||||
; 权重:属性/GP 各 3、道具/AP 各 1;次数上限:属性件各 3、AP 件 2(达上限件先剔牌库)——gp/道具无次数上限。
|
||||
; 同调位过渡键 transitional_sync_slots=1(道具持有上限;正主 = attunement_growth.tres 谓词表,同调实施批切换)。
|
||||
; 末关通关不发三选一与回血(奖励只朝下一战生效)。
|
||||
; 退役字段:level_clear_hp_bonus(血回满+上限提升件)、keystone_per_level(钥石改 Boss/精英击杀掉落,行值归 meta.tres boss_keystone)。
|
||||
; 全部 provisional-pending-曲线调参批(值锚与上调约束归 难度曲线与Boss;回血 20% 系难度上压方向,带重扫随批呈报)。
|
||||
@@ -25,16 +25,16 @@ forge_products = [{
|
||||
}]
|
||||
boss_keystone = [{
|
||||
"level_index": 1,
|
||||
"first_kill": 5,
|
||||
"repeat_kill": 1
|
||||
"first_kill": 3,
|
||||
"repeat_kill": 2
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"first_kill": 8,
|
||||
"first_kill": 3,
|
||||
"repeat_kill": 2
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"first_kill": 12,
|
||||
"repeat_kill": 3
|
||||
"first_kill": 3,
|
||||
"repeat_kill": 2
|
||||
}]
|
||||
workshop_items = [{
|
||||
"item_id": "probe_dust",
|
||||
@@ -100,3 +100,9 @@ conversion = [{
|
||||
; 护甲行强条目重测义务随批履行:重测暴露并修复护甲断链(begin_battle hero_armor_class 参数此前
|
||||
; 零消费——_defender_ac 主角分支本次接线,缺参/-1 = 表值兜底);修复后甲轴读数 +1.2±0.4 为
|
||||
; 解锁后参数实测,非基线外推。战役轨迹三花费序(生命/攻击/护甲优先)随批采集验证。
|
||||
;
|
||||
; 迭代 10 奖励重构批(2026-10-01——boss_keystone 重锚,P-B 乙拍板):三关统一 首杀 3 / 复杀 2
|
||||
; (原 5/1 · 8/2 · 12/3 退役)——贴近「Boss 掉 2–3 个钥石」用户示例量级、保持 first_kill ≥ repeat_kill
|
||||
; 结构;子选项「复杀统一 2」归代拍板白名单(另一子选项 = 按关 2/2/3 递进)。钥石渠道自本批起 =
|
||||
; Boss/精英击杀逐杀掉落(大奖励 keystone_per_level 渠道退役;精英掉 1 预留行随怪物批实装落)。
|
||||
; 产出结构变化 → 元进度标定义务重开(06-待办清单 2026-10-01 登记条)。provisional 归曲线调参批。
|
||||
@@ -77,12 +77,48 @@ static func boss_name(runner, db) -> String:
|
||||
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))]
|
||||
# ---- 提升三选一文案族(迭代 10 奖励重构批——big_reward_text 随三件套退役)----
|
||||
|
||||
# 三选一停点头部行(回血反馈 + Boss 掉落钥石增量;读数 = bonus_choice_presented 载荷快照)
|
||||
static func bonus_pick_header(payload: Dictionary) -> String:
|
||||
var lines: Array[String] = ["第 %d 层通过!" % int(payload.get("level_index", 0))]
|
||||
var heal := int(payload.get("heal_gained", 0))
|
||||
if heal > 0:
|
||||
lines.append("通关回血 +%d(生命 %d → %d)" % [heal, int(payload.get("hp_before", 0)), int(payload.get("hp_after", 0))])
|
||||
else:
|
||||
lines.append("生命已满——通关回血 0(合法态)")
|
||||
lines.append("Boss 掉落钥石 +%d(累计 %d)" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))])
|
||||
lines.append("选择一件提升带走(另两件丢弃不可反悔):")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# 三选一选项卡文案(件名 + 效果读数;值 = 冒险表行值,组合根喂入)
|
||||
static func bonus_card_text(id: String, value: int) -> String:
|
||||
match id:
|
||||
"attack":
|
||||
return "攻击力提升\n攻击 +%d" % value
|
||||
"max_hp":
|
||||
return "生命上限提升\n生命上限 +%d" % value
|
||||
"armor":
|
||||
return "护甲等级提升\n护甲 +%d" % value
|
||||
"gp":
|
||||
return "金币袋\n金币 +%d" % value
|
||||
"aux_item":
|
||||
return "魔法道具\n随机获得一件"
|
||||
"ap":
|
||||
return "行动点提升\n每战首轮 AP +%d" % value
|
||||
return id
|
||||
|
||||
|
||||
# 三选一兑付反馈行(选中件效果 + 丢弃提示)
|
||||
static func bonus_result_text(result: Dictionary) -> String:
|
||||
var name := bonus_card_text(String(result.get("picked", "")), int(result.get("value", 0))).replace("\n", " · ")
|
||||
return "已选择:%s\n其余选项已丢弃(不可反悔)" % name
|
||||
|
||||
|
||||
# Boss 胜利战斗总结结算行(settlement-summary v1.1——钥石增量行;组合根拼 GP 行后追加)
|
||||
static func bonus_keystone_line(payload: Dictionary) -> String:
|
||||
return "钥石 +%d · 累计 %d" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))]
|
||||
|
||||
|
||||
# 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子)
|
||||
@@ -97,14 +133,14 @@ static func adventure_summary_text(summary: Dictionary) -> String:
|
||||
int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))]
|
||||
|
||||
|
||||
# 钥石入账明细文案(元进度实现批——settle_adventure 明细的结算屏追加段:大奖励 / 逐关 Boss
|
||||
# 首杀或重复两档 / 新解锁道具;name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
|
||||
# 钥石入账明细文案(元进度实现批;迭代 10 改口径——钥石全源 = Boss 逐杀掉落,「大奖励」行退役;
|
||||
# name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
|
||||
static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String:
|
||||
var parts: Array = ["大奖励 +%d" % int(detail.get("big_reward", 0))]
|
||||
var parts: Array = []
|
||||
for d in detail.get("boss_drops", []):
|
||||
parts.append("第 %d 关 Boss %s +%d" % [int(d.get("level", 0)),
|
||||
"首杀" if bool(d.get("first", false)) else "重复杀", int(d.get("amount", 0))])
|
||||
var line := "钥石入账:" + " · ".join(parts)
|
||||
var line := "钥石入账:" + (" · ".join(parts) if not parts.is_empty() else "本局无掉落")
|
||||
var unlocks: Array = detail.get("new_unlocks", [])
|
||||
if not unlocks.is_empty():
|
||||
var names: Array[String] = []
|
||||
|
||||
+95
-10
@@ -44,6 +44,8 @@ var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A
|
||||
var _settlement_stop_pending := false # 卡⑥ 战斗胜利总结结算框挂起态(确认「下一节点」后进选路页)
|
||||
var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷)
|
||||
var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷)
|
||||
var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
|
||||
var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载)
|
||||
var _start_overlay: Control
|
||||
var _end_overlay: BattleEndOverlay
|
||||
var _inter_overlay: Control
|
||||
@@ -191,8 +193,15 @@ func _build_ui() -> void:
|
||||
add_child(confirm_dialog)
|
||||
confirm_dialog.confirmed.connect(_on_confirm_new_adventure)
|
||||
confirm_dialog.cancelled.connect(_on_confirm_cancelled)
|
||||
# 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进)
|
||||
bonus_overlay = BonusPickOverlay.new()
|
||||
add_child(bonus_overlay)
|
||||
bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed)
|
||||
bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen)
|
||||
bonus_overlay.continue_pressed.connect(_on_bonus_continue)
|
||||
move_child(bonus_overlay, _end_overlay.get_index())
|
||||
_modals = ModalStack.new()
|
||||
for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, _end_overlay, shop_overlay, camp_overlay, tune_panel, settings_overlay, confirm_dialog]:
|
||||
for m: Control in [legend_overlay, review_overlay, node_overlay, bonus_overlay, level_screen, _end_overlay, shop_overlay, camp_overlay, tune_panel, settings_overlay, confirm_dialog]:
|
||||
_register_modal(m)
|
||||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||||
|
||||
@@ -329,7 +338,10 @@ func _wire_adventure() -> void:
|
||||
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)
|
||||
# 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线
|
||||
adventure.bonus_choice_presented.connect(_on_bonus_presented)
|
||||
adventure.bonus_resolved.connect(_on_bonus_resolved)
|
||||
adventure.aux_replace_presented.connect(_on_aux_replace_presented)
|
||||
adventure.adventure_finished.connect(_on_adventure_finished)
|
||||
adventure.level_started.connect(_on_adventure_level_started)
|
||||
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
|
||||
@@ -402,18 +414,87 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# ---- 冒险模式终局面(大奖励屏 / 冒险结算屏——冒险结构实现批)----
|
||||
# ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)----
|
||||
|
||||
# 大奖励兑付(非末关通关恰一次):复用 _inter_overlay(label + 确认按钮,零新增交互范式);
|
||||
# 文案装配归 Narrator
|
||||
func _on_big_reward(reward: Dictionary) -> void:
|
||||
if int(reward.get("aux_cleared", 0)) > 0:
|
||||
board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(reward.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
|
||||
_inter_label.text = Narrator.big_reward_text(reward)
|
||||
_inter_overlay.visible = true
|
||||
# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
|
||||
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
|
||||
func _on_bonus_presented(payload: Dictionary) -> void:
|
||||
if int(payload.get("aux_cleared", 0)) > 0:
|
||||
board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
|
||||
_pending_bonus = payload.duplicate(true)
|
||||
if bool(payload.get("replace_pending", false)):
|
||||
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
|
||||
_show_bonus_pick()
|
||||
return
|
||||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
|
||||
board_view.get_global_rect(),
|
||||
"%s\n%s" % [_gp_summary_text({"gp_dropped": _last_gain_amount}), Narrator.bonus_keystone_line(payload)])
|
||||
_open_modal(_end_overlay)
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
|
||||
func _show_bonus_pick() -> void:
|
||||
var payload: Dictionary = _pending_bonus
|
||||
_open_modal(bonus_overlay)
|
||||
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array))
|
||||
_refresh_presentations()
|
||||
|
||||
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
|
||||
var _last_replace_incoming := {} # 同上——incoming 记录
|
||||
|
||||
|
||||
func _bonus_card_texts(candidates: Array) -> Array:
|
||||
var out: Array = []
|
||||
for id in candidates:
|
||||
out.append({"id": String(id), "text": Narrator.bonus_card_text(String(id), int(db.adventure.bonus_value(String(id))))})
|
||||
return out
|
||||
|
||||
|
||||
func _on_bonus_pick_confirmed(item_id: String) -> void:
|
||||
if adventure == null or not adventure.pick_bonus(item_id):
|
||||
push_error("[组合根] 三选一兑付被拒——见输出日志")
|
||||
|
||||
|
||||
func _on_aux_replace_presented(items: Array, incoming: Dictionary) -> void:
|
||||
_last_replace_items = items.duplicate(true)
|
||||
_last_replace_incoming = incoming.duplicate(true)
|
||||
if not bonus_overlay.visible:
|
||||
_open_modal(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
|
||||
_open_bonus_replace(items, incoming)
|
||||
|
||||
|
||||
func _open_bonus_replace(items: Array, incoming: Dictionary) -> void:
|
||||
var named: Array = []
|
||||
for it in items:
|
||||
var rec: Dictionary = it.duplicate()
|
||||
rec["display_name"] = String(_item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
|
||||
named.append(rec)
|
||||
bonus_overlay.open_replace(named, String(_item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
|
||||
named.size(), int(db.adventure.transitional_sync_slots))
|
||||
|
||||
|
||||
func _on_bonus_replace_chosen(discard_index: int) -> void:
|
||||
if adventure == null or not adventure.resolve_aux_replace(discard_index):
|
||||
push_error("[组合根] 同调位替换受理被拒——见输出日志")
|
||||
|
||||
|
||||
# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续)
|
||||
func _on_bonus_resolved(result: Dictionary) -> void:
|
||||
_pending_bonus = {}
|
||||
bonus_overlay.show_result(Narrator.bonus_result_text(result))
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
# 继续钮:收三选一面 → 推进下一关(零自动推进——兑付后受理)
|
||||
func _on_bonus_continue() -> void:
|
||||
_close_modal(bonus_overlay)
|
||||
bonus_overlay.close_pick()
|
||||
if _adventure_mode and adventure != null:
|
||||
if not adventure.continue_to_next_level():
|
||||
_fatal_screen("关卡推进失败——见输出日志")
|
||||
|
||||
|
||||
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案
|
||||
func _on_adventure_finished(summary: Dictionary) -> void:
|
||||
_level_active = false
|
||||
@@ -457,6 +538,10 @@ func _on_battle_end_continue() -> void:
|
||||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
|
||||
_close_modal(_end_overlay)
|
||||
_end_overlay.close()
|
||||
# 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
|
||||
if not _pending_bonus.is_empty():
|
||||
_show_bonus_pick()
|
||||
return
|
||||
# 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
|
||||
if _settlement_stop_pending:
|
||||
_settlement_stop_pending = false
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# Date: 2026-10-01(迭代 10 奖励重构批——level-clear.md v2 三选一选择面承载)
|
||||
# 提升三选一覆盖层(模态):非末关通关的选择停点呈现——部分回血反馈 + 三卡点选 + 确认兑付 +
|
||||
# 同调位替换子交互。高内聚边界:文案拼装归 Narrator(组合根喂入),选择意图经信号外发;
|
||||
# 视图零核心引用。交互范式 = node_overlay choice_box 按钮族 + 选路两段式(点选→确认)复用,零新原语。
|
||||
# 状态机:pick(三卡点选→确认)→ [replace(满位 N+1 选 N)] → result(兑付反馈→继续)。
|
||||
class_name BonusPickOverlay
|
||||
extends Control
|
||||
|
||||
signal pick_confirmed(item_id: String)
|
||||
signal replace_chosen(discard_index: int) # -1 = 放弃新件不获取
|
||||
signal continue_pressed
|
||||
|
||||
var _label: Label
|
||||
var _choice_box: HBoxContainer # 三卡按钮区(pick 态)
|
||||
var _replace_box: VBoxContainer # 替换子面板(replace 态)
|
||||
var _confirm_btn: Button # 确认钮(点选后点亮——两段式第二拍)
|
||||
var _continue_btn: Button # 继续钮(兑付反馈态唯一出口)
|
||||
var _selected := "" # 点选暂存(确认前可改选)
|
||||
var _header := "" # 停点头部文案(选择态保持显示)
|
||||
var _card_meta: Array = [] # open_pick 缓存 [{id, text}]——高亮还原读面
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
var dim := ColorRect.new()
|
||||
dim.color = Color(0, 0, 0, 0.6)
|
||||
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
add_child(dim)
|
||||
var center := CenterContainer.new()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
dim.add_child(center)
|
||||
var box := VBoxContainer.new()
|
||||
box.add_theme_constant_override("separation", 12)
|
||||
center.add_child(box)
|
||||
_label = UiKit.label("", 20, true)
|
||||
_label.add_theme_color_override("font_color", UiKit.WARM)
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_label)
|
||||
_choice_box = HBoxContainer.new()
|
||||
_choice_box.add_theme_constant_override("separation", 12)
|
||||
_choice_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(_choice_box)
|
||||
_confirm_btn = UiKit.button("确认选择")
|
||||
_confirm_btn.pressed.connect(_on_confirm_pressed)
|
||||
_confirm_btn.visible = false
|
||||
box.add_child(_confirm_btn)
|
||||
_replace_box = VBoxContainer.new()
|
||||
_replace_box.add_theme_constant_override("separation", 8)
|
||||
_replace_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(_replace_box)
|
||||
_continue_btn = UiKit.button("继续")
|
||||
_continue_btn.pressed.connect(func(): continue_pressed.emit())
|
||||
_continue_btn.visible = false
|
||||
box.add_child(_continue_btn)
|
||||
visible = false
|
||||
|
||||
|
||||
# 选择态:header = 停点头部(回血+钥石增量行,Narrator 装配);cards = [{id, text}]
|
||||
#(组合根组装——视图纯呈现)。确认前可改选;确认经 pick_confirmed 外发。
|
||||
func open_pick(header: String, cards: Array) -> void:
|
||||
_header = header
|
||||
_label.text = header
|
||||
_selected = ""
|
||||
_card_meta = (cards as Array).duplicate(true)
|
||||
for c in _choice_box.get_children():
|
||||
c.queue_free()
|
||||
for ch in cards:
|
||||
var cid := String(ch.get("id", ""))
|
||||
var btn := UiKit.button(String(ch.get("text", "")))
|
||||
btn.custom_minimum_size = Vector2(170, 88)
|
||||
btn.pressed.connect(func(): _select_card(cid))
|
||||
_choice_box.add_child(btn)
|
||||
_choice_box.visible = true
|
||||
_confirm_btn.visible = false
|
||||
_replace_box.visible = false
|
||||
_continue_btn.visible = false
|
||||
visible = true
|
||||
|
||||
|
||||
func _select_card(id: String) -> void:
|
||||
_selected = id
|
||||
for i in _choice_box.get_child_count():
|
||||
var btn := _choice_box.get_child(i) as Button
|
||||
var meta: Dictionary = _card_meta[i] if i < _card_meta.size() else {}
|
||||
var base_text := String(meta.get("text", ""))
|
||||
btn.text = ("✓ " + base_text) if String(meta.get("id", "")) == id else base_text
|
||||
_confirm_btn.visible = true
|
||||
|
||||
|
||||
func _on_confirm_pressed() -> void:
|
||||
if _selected != "":
|
||||
pick_confirmed.emit(_selected)
|
||||
|
||||
|
||||
# 探针/测试复用:以真实按钮路径触发点选、确认与继续(树序遮挡纪律——断言走真实信号路径)
|
||||
func press_card(index: int) -> void:
|
||||
if index >= 0 and index < _choice_box.get_child_count():
|
||||
(_choice_box.get_child(index) as Button).pressed.emit()
|
||||
|
||||
|
||||
func press_confirm() -> void:
|
||||
_confirm_btn.pressed.emit()
|
||||
|
||||
|
||||
func press_continue() -> void:
|
||||
_continue_btn.pressed.emit()
|
||||
|
||||
|
||||
func selected_id() -> String:
|
||||
return _selected
|
||||
|
||||
|
||||
# 兑付反馈态(bonus_resolved 到达——结果行追加在头部之后;唯一出口 = 继续)
|
||||
func show_result(text: String) -> void:
|
||||
_label.text = _header + "\n" + text
|
||||
_choice_box.visible = false
|
||||
_confirm_btn.visible = false
|
||||
_replace_box.visible = false
|
||||
_continue_btn.visible = true
|
||||
visible = true
|
||||
|
||||
|
||||
# 同调位替换子态(aux_replace_presented 到达):items = [{index, item_id, display_name}](组合根
|
||||
# 喂显示名)。两路出口:丢旧收新(逐件钮)/ 放弃新件——终态恒 ≤ 同调位由核心受理面保证。
|
||||
func open_replace(items: Array, incoming_name: String, holding: int, slots: int) -> void:
|
||||
_label.text = "同调位已满(%d/%d)——收下「%s」需丢弃一件已有道具,或放弃新件" % [holding, slots, incoming_name]
|
||||
for c in _replace_box.get_children():
|
||||
c.queue_free()
|
||||
for it in items:
|
||||
var idx := int(it.get("index", -1))
|
||||
var btn := UiKit.button("丢弃「%s」换新" % String(it.get("display_name", it.get("item_id", ""))))
|
||||
btn.pressed.connect(func(): replace_chosen.emit(idx))
|
||||
_replace_box.add_child(btn)
|
||||
var decline := UiKit.button("放弃新件(不获取)")
|
||||
decline.pressed.connect(func(): replace_chosen.emit(-1))
|
||||
_replace_box.add_child(decline)
|
||||
_choice_box.visible = false
|
||||
_confirm_btn.visible = false
|
||||
_replace_box.visible = true
|
||||
_continue_btn.visible = false
|
||||
visible = true
|
||||
|
||||
|
||||
# 收屏(组合根在 continue_pressed 处理毕调——推进下一关同拍)
|
||||
func close_pick() -> void:
|
||||
_choice_box.visible = false
|
||||
_replace_box.visible = false
|
||||
_continue_btn.visible = false
|
||||
_confirm_btn.visible = false
|
||||
visible = false
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqlecn1dxpg0i
|
||||
@@ -15,9 +15,9 @@
|
||||
# 驱动循环按 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——签名与
|
||||
# 返回结构零变化,行为逐位不变。
|
||||
# 通关兑付停点确认、adventure_finished 置 result/summary;外层每关重取 runner 实例经 _drive_level
|
||||
# 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与返回结构零变化,行为逐位不变。
|
||||
# 2026-10-01 迭代 10 奖励重构批:大奖励立即确认改三选一停点驱动(候选取首件/替换放弃新件)。
|
||||
# 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。
|
||||
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
|
||||
extends RefCounted
|
||||
@@ -124,7 +124,11 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
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))
|
||||
# 三选一停点驱动(迭代 10 奖励重构批——big_reward_paid 退役:确定性策略候选取首件、
|
||||
# 道具件满位替换放弃新件、兑付后推进下一关——零随机流自开,同种子逐位可复放)
|
||||
adr.bonus_choice_presented.connect(_on_adventure_bonus.bind(st, adr))
|
||||
adr.bonus_resolved.connect(_on_adventure_bonus_resolved.bind(st, adr))
|
||||
adr.aux_replace_presented.connect(_on_adventure_aux_replace.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, {"hero_override": hero_override, "first_script": false}): # 测量夹具恒抽签(v0.4——首次脚本分支不进任何读数)
|
||||
@@ -132,7 +136,7 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
return st
|
||||
var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由 big_reward 确认与 runner 重取承载)
|
||||
var guard_drives := 10 # 每次外层迭代 = 驱动一关(关卡更替由三选一兑付确认与 runner 重取承载)
|
||||
while st.result == "" and guard_drives > 0:
|
||||
guard_drives -= 1
|
||||
_drive_level(adr.runner(), st, logger, policy, route)
|
||||
@@ -183,18 +187,34 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
# 冒险钱包事件采集(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", ""))})
|
||||
"amount": int(extra.get("amount", 0)), "product": String(extra.get("product", "")),
|
||||
"source": String(extra.get("source", ""))})
|
||||
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))
|
||||
# 三选一停点驱动(迭代 10——确定性策略:候选取首件;奖励事件随 reward_events 留档)
|
||||
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_pick": payload.duplicate(true)})
|
||||
var candidates: Array = payload.get("candidates", []) as Array
|
||||
if candidates.is_empty() or not adr.pick_bonus(String(candidates[0])):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一兑付完成 → 推进下一关(自动路径无停点等待——零自动推进契约由显式确认满足)
|
||||
static func _on_adventure_bonus_resolved(result: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"bonus_resolved": result.duplicate(true)})
|
||||
if not adr.continue_to_next_level():
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 同调位满位替换子交互(确定性策略:放弃新件不获取——-1)
|
||||
static func _on_adventure_aux_replace(items: Array, incoming: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"aux_replace": incoming.duplicate(true)})
|
||||
if not adr.resolve_aux_replace(-1):
|
||||
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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dv81k73ruw7xe
|
||||
+181
-67
@@ -189,17 +189,27 @@ func _smoke_rest_stub(pct: int, hp: int, max_hp: int) -> int:
|
||||
|
||||
# ---- 冒险结构实现批冒烟(2026-09-21——AdventureTable 建表 + AdventureRunner + 冒险验收断言)----
|
||||
|
||||
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸)
|
||||
# ① 冒险表(单行全局 Rule 4 豁免——建表与接线闸;迭代 10 扩列域检)
|
||||
func _smoke_adventure_table() -> void:
|
||||
print("--- 冒险表(AdventureTable 建表——大奖励/钥石行值单点)---")
|
||||
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)
|
||||
check(_db.adventure.validate().is_empty(), "AdventureTable.validate() 零违例(全键域检在案)")
|
||||
check(int(_db.adventure.clear_heal_pct) >= 0 and int(_db.adventure.clear_heal_pct) <= 100,
|
||||
"clear_heal_pct ∈ [0,100](%d——通关部分回血比例)" % int(_db.adventure.clear_heal_pct))
|
||||
for id in _db.adventure.BONUS_IDS:
|
||||
var v := int(_db.adventure.bonus_value(id))
|
||||
var w := int(_db.adventure.bonus_weight(id))
|
||||
check(v >= 0 and v <= 999 and w >= 1 and w <= 99, "三选一件 %s:值 ∈[0,999](%d)· 权重 ∈[1,99](%d)" % [id, v, w])
|
||||
check(int(_db.adventure.bonus_cap_ap) == 2 and int(_db.adventure.bonus_cap_attack) == int(_db.adventure.bonus_cap_max_hp)
|
||||
and int(_db.adventure.bonus_cap_max_hp) == int(_db.adventure.bonus_cap_armor),
|
||||
"次数上限口径:AP 件=%d(全冒险最紧)· 属性件=%d(三件同)" % [int(_db.adventure.bonus_cap_ap), int(_db.adventure.bonus_cap_attack)])
|
||||
check(int(_db.adventure.bonus_cap("gp")) == _db.adventure.BONUS_CAP_UNLIMITED
|
||||
and int(_db.adventure.bonus_cap("aux_item")) == _db.adventure.BONUS_CAP_UNLIMITED,
|
||||
"GP/道具件无次数上限(牌库恒非空的结构保证)")
|
||||
check(int(_db.adventure.transitional_sync_slots) >= 0 and int(_db.adventure.transitional_sync_slots) <= 9,
|
||||
"transitional_sync_slots ∈ [0,9](%d——同调位过渡读数源)" % int(_db.adventure.transitional_sync_slots))
|
||||
|
||||
|
||||
# ② 冒险结构 API 闸(状态机受理面——重复开始拒绝 / 入口停点 / 放弃两态)
|
||||
@@ -279,43 +289,86 @@ func _smoke_adventure() -> void:
|
||||
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 首战上限)
|
||||
# ② 满配通关 + ④ 三层奖励兑付(三选一恰 2 次 · 回血封顶 · 钥石逐杀 · 候选零重复)
|
||||
var st: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
var sum: Dictionary = st["summary"]
|
||||
check(String(st["result"]) == "completed" and bool(sum.get("cleared", false)),
|
||||
"满配通关(42 / atk+900 / hp+400——cleared=true;v2 怪阶梯后旧 200/100 档不再满配)")
|
||||
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)))
|
||||
var ks_exp := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_exp += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
check(int(sum.get("keystone_total", -1)) == ks_exp,
|
||||
"钥石合计 = ΣBoss 首杀行值(%d——逐杀掉落新口径;断点未注入恒首杀)" % int(sum.get("keystone_total", -1)))
|
||||
var drops: Array = sum.get("keystone_drops", []) as Array
|
||||
check(drops.size() == 3, "逐杀掉落明细恰 3 笔(L1/L2/L3——additive 摘要键 keystone_drops)")
|
||||
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 picks: Array = []
|
||||
var resolves: Array = []
|
||||
for ev in (st["reward_events"] as Array):
|
||||
if (ev as Dictionary).has("bonus_pick"):
|
||||
picks.append((ev as Dictionary)["bonus_pick"])
|
||||
elif (ev as Dictionary).has("bonus_resolved"):
|
||||
resolves.append((ev as Dictionary)["bonus_resolved"])
|
||||
check(picks.size() == 2 and resolves.size() == 2,
|
||||
"三选一恰呈现 2 次、恰兑付 2 次(L1/L2 后——末关不发,奖励只朝下一战生效)")
|
||||
var heal_ok := picks.size() == 2
|
||||
var cand_ok := picks.size() == 2
|
||||
var ks_gain_ok := picks.size() == 2
|
||||
var resolve_ok := resolves.size() == 2
|
||||
var max_flat := int(_db.adventure.bonus_max_hp_flat)
|
||||
var heal_pct := int(_db.adventure.clear_heal_pct)
|
||||
var hp_acc := 0 # max_hp 件累加(回血封顶断言的生效上限推导)
|
||||
var ks_prefix := 0
|
||||
for i in picks.size():
|
||||
var p: Dictionary = picks[i]
|
||||
var cands: Array = p.get("candidates", []) as Array
|
||||
if cands.size() < 2 or cands.size() > 3:
|
||||
cand_ok = false
|
||||
var seen := {}
|
||||
for c in cands:
|
||||
if seen.has(String(c)):
|
||||
cand_ok = false
|
||||
seen[String(c)] = true
|
||||
var heal: int = int(p.get("heal_gained", -1))
|
||||
var hp_before: int = int(p.get("hp_before", -1))
|
||||
var hp_after: int = int(p.get("hp_after", -1))
|
||||
var max_eff := int(_db.hero.max_hp) + hp_acc # 回血口径 = 表值+加算区(无 TunePanel 百分比)
|
||||
if heal != mini(int(floor(float(max_eff) * float(heal_pct) / 100.0)), max_eff - hp_before) \
|
||||
or hp_after != hp_before + heal or hp_after > max_eff:
|
||||
heal_ok = false
|
||||
ks_prefix += int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0))
|
||||
if int(p.get("keystone_gained", -1)) != int(_db.meta_boss_keystone_row(int(p.get("level_index", -1))).get("first_kill", 0)) \
|
||||
or int(p.get("keystone_total", -1)) != ks_prefix:
|
||||
ks_gain_ok = false
|
||||
if resolve_ok:
|
||||
var r: Dictionary = resolves[i]
|
||||
if not (p.get("candidates", []) as Array).has(String(r.get("picked", ""))) \
|
||||
or int(r.get("value", -1)) != int(_db.adventure.bonus_value(String(r.get("picked", "")))):
|
||||
resolve_ok = false
|
||||
if String(r.get("picked", "")) == "max_hp":
|
||||
hp_acc += max_flat
|
||||
check(cand_ok, "三选一候选 2–3 件且零重复(加权洗牌不放回)")
|
||||
check(heal_ok, "通关部分回血封顶(floor(上限×比例) 封顶缺口——血回满退役)")
|
||||
check(ks_gain_ok, "钥石逐杀入账(首杀档位 + 累计逐位)")
|
||||
check(resolve_ok, "点选恰兑付选中一件(值=表值;未选项零生效)")
|
||||
var seq: Array = st["encounter_max_hp_seq"]
|
||||
var k := 5.0 # hp+400 ⇒ TunePanel 百分比后乘 ×5(加算先加、百分比后乘——生效次序口径)
|
||||
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 valid_hp := {}
|
||||
for acc in 3:
|
||||
valid_hp[maxi(1, int(round(float(int(_db.hero.max_hp) + acc * max_flat) * k)))] = true
|
||||
var seq_ok := seq.size() > 0 and int(seq[0]) == exp_base
|
||||
for v in (seq as Array):
|
||||
if not valid_hp.has(int(v)):
|
||||
seq_ok = false
|
||||
check(seq_ok,
|
||||
"各战上限入参 ∈ {基础, 基础+件值, 基础+2×件值}(覆盖值通道传播——首战=基础表值 %d;AP 件不加上限)" % exp_base)
|
||||
# ③ 种子域可复现(同种子两局:summary + 上限序列 + 行进序列 + 三选一停点载荷逐位一致)
|
||||
var st2: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
check(st2["summary"] == sum and st2["encounter_max_hp_seq"] == seq
|
||||
and st2["visited_ids"] == st["visited_ids"],
|
||||
"同种子两局逐位可复现(summary/上限序列/行进序列——主种子单点派生)")
|
||||
and st2["visited_ids"] == st["visited_ids"] and st2["reward_events"] == st["reward_events"],
|
||||
"同种子两局逐位可复现(summary/上限序列/行进序列/三选一载荷——reward_pick 封闭流)")
|
||||
# ⑤ 败北线(零配 L1 败北 + 半配 L2 败北——种子扫描,仿商店房 range(42,92) 先例)
|
||||
var lose := {}
|
||||
for s in range(42, 92):
|
||||
@@ -343,9 +396,9 @@ func _smoke_adventure() -> void:
|
||||
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["keystone_total"]) == int(_db.meta_boss_keystone_row(1).get("first_kill", 0)) \
|
||||
and int(sh["stopped_at"]) == 2,
|
||||
"L2 败北摘要:L1 已过(钥石 1×表值)· 止步第 2 层")
|
||||
"L2 败北摘要:L1 已过(钥石=L1 首杀掉落)· 止步第 2 层")
|
||||
var dh: Dictionary = sh.get("defeat", {})
|
||||
check(int(dh.get("level_index", -1)) == 2 and int(dh.get("node_id", -1)) >= 0 \
|
||||
and String(dh.get("node_type", "")) in ["battle", "boss"],
|
||||
@@ -372,15 +425,15 @@ func _smoke_adventure() -> void:
|
||||
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"])])
|
||||
elif String(ev["event"]) == "gain" and String(ev.get("source", "")) != "bonus_pick":
|
||||
gains += 1 # 战斗完成入账计数(三选一 GP 袋件入账另有 source 标记——不计战斗口径)
|
||||
check(gains == int(st["battles"]), "每战完成入账恰一次(gain %d = battles %d——GP 袋件入账单列)" % [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 商店购买 → 大奖励直调——同一本账累加 + 回满新上限)
|
||||
# ⑧ 加算区同账(手工场景:L2 商店购买 → 三选一直调——同一本账累加 + 部分回血封顶)
|
||||
var same := {}
|
||||
for s in range(42, 92):
|
||||
var a8 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
@@ -406,14 +459,22 @@ func _smoke_adventure() -> void:
|
||||
check(gp0 >= hp_price + l2_price_bonus,
|
||||
"进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus])
|
||||
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))
|
||||
var acc_shop := int(a8.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子)
|
||||
check(acc_shop >= 3, "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop)
|
||||
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"])])
|
||||
"node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 通关(victory 载荷直调——非末关)
|
||||
check(not a8._bonus_pending.is_empty(), "L2 通关三选一停点在案(直调兑付前——零自动推进)")
|
||||
check(not a8.continue_to_next_level(), "未兑付推进拒绝(选择停点零自动推进契约)")
|
||||
var l2_cands: Array = a8._bonus_pending["candidates"]
|
||||
check(a8.pick_bonus(String(l2_cands[0])), "三选一点选兑付受理(候选取首件)")
|
||||
var picked2: String = String(a8._bonus_result.get("picked", ""))
|
||||
var expect_acc: int = acc_shop + (int(_db.adventure.bonus_max_hp_flat) if picked2 == "max_hp" else 0)
|
||||
check(int(a8.bonus.get("max_hp", -1)) == expect_acc,
|
||||
"三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.bonus.get("max_hp", -1)), acc_shop, picked2])
|
||||
var max_eff8 := int(_db.hero.max_hp) + int(a8.bonus["max_hp"])
|
||||
check(int(a8.ledger["hero_hp"]) <= max_eff8,
|
||||
"通关部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8])
|
||||
check(a8.continue_to_next_level(), "兑付后推进受理(下一关开始)")
|
||||
# ⑨ 恢复点数学(接口③封顶公式三读数 + 同式两落点一致)
|
||||
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 \
|
||||
@@ -436,6 +497,42 @@ func _smoke_adventure() -> void:
|
||||
g10 -= 1
|
||||
autoplay._autoplay_battle(a10.runner(), b10, st10, null)
|
||||
check(a10.runner().current_battle() == null and a10.abandon(), "非战斗期(选路态)abandon 受理")
|
||||
# ⑪ 上限剔牌库 + 同调位替换两路 + 满血回 0(迭代 10——white-box:计数/携带面直设)
|
||||
var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11))
|
||||
check(a11.start_adventure(11, {"first_script": true, "aux_unlocked": {"blast_kit": true}}),
|
||||
"剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 blast_kit)")
|
||||
a11._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack) # 直设计数(white-box——计数面本身即断点承载)
|
||||
a11._bonus_taken["ap"] = int(_db.adventure.bonus_cap_ap)
|
||||
var draw: Array = a11._draw_bonus_candidates()
|
||||
check(not (draw as Array).has("attack") and not (draw as Array).has("ap"),
|
||||
"达上限件剔牌库(attack 计满 %d / ap 计满 %d 不出现在候选——%s)" % [int(_db.adventure.bonus_cap_attack), int(_db.adventure.bonus_cap_ap), str(draw)])
|
||||
check((draw as Array).size() >= 2, "剔牌库后候选恒 ≥2(GP/道具件无次数上限——防「三选项全不可选」死角)")
|
||||
# 同调位替换·丢旧收新路(slots 过渡键=1,carry 满 1 件再取道具件 → 满位转替换)
|
||||
var slots := int(_db.adventure.transitional_sync_slots)
|
||||
a11._bonus_pending = {"candidates": ["aux_item", "gp", "attack"], "payload": {}}
|
||||
a11._bonus_aux_carry.assign(["probe_dust"])
|
||||
check(a11.pick_bonus("aux_item") and a11._bonus_pending.has("incoming"),
|
||||
"满位道具件转替换子交互(持有 %d/%d)" % [a11._bonus_aux_carry.size(), slots])
|
||||
check(a11.resolve_aux_replace(0) and (a11._bonus_aux_carry as Array).has("blast_kit") \
|
||||
and (a11._bonus_aux_carry as Array).size() <= slots,
|
||||
"替换受理·丢旧收新(终态 ≤ 同调位 %d——%s)" % [slots, str(a11._bonus_aux_carry)])
|
||||
# 同调位替换·放弃新件路(原持有集不动、新件不获取)
|
||||
a11._bonus_pending = {"candidates": ["aux_item"], "payload": {}}
|
||||
a11._bonus_result = {}
|
||||
a11._bonus_aux_carry.assign(["probe_dust"])
|
||||
check(a11.pick_bonus("aux_item"), "替换场景二:满位再转替换子交互")
|
||||
check(a11.resolve_aux_replace(-1) and a11._bonus_aux_carry.size() == 1 and String(a11._bonus_aux_carry[0]) == "probe_dust",
|
||||
"替换受理·放弃新件(原持有集不动、新件不获取——两路终态恒 ≤ 同调位)")
|
||||
# 满血回 0 合法(通关回血封顶口径)
|
||||
var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12))
|
||||
a12.start_adventure(12, {"first_script": true})
|
||||
var cap := {"payload": {}}
|
||||
a12.bonus_choice_presented.connect(func(p): cap["payload"] = p)
|
||||
var max_full := int(_db.hero.max_hp) + int(a12._base_override.get("max_hp", 0))
|
||||
a12.ledger["hero_hp"] = max_full
|
||||
a12._on_level_finished({"result": "victory", "hp": max_full, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
check(int(cap["payload"].get("heal_gained", -1)) == 0,
|
||||
"满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)")
|
||||
var s10: Dictionary = a10.summary
|
||||
check(String(s10.get("result", "")) == "abandoned" and not bool(s10.get("cleared", true)),
|
||||
"放弃摘要:abandoned · 未打穿(已获大奖励不追回——同失败型结算)")
|
||||
@@ -450,6 +547,10 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
|
||||
if String(st["result"]) != "":
|
||||
return false
|
||||
if a._pending_next_level:
|
||||
# 三选一停点确认(迭代 10——候选取首件后推进,与 run_adventure 自动策略同口径)
|
||||
if not a._bonus_pending.is_empty():
|
||||
var bcands: Array = a._bonus_pending["candidates"]
|
||||
a.pick_bonus(String(bcands[0]))
|
||||
a.continue_to_next_level()
|
||||
continue
|
||||
var r = a.runner()
|
||||
@@ -3397,27 +3498,37 @@ func _meta_settle_and_workshop() -> void:
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var row1: Dictionary = _db.meta_boss_keystone_row(1)
|
||||
var row2: Dictionary = _db.meta_boss_keystone_row(2)
|
||||
# ⑥ 新档首通 L1:first = true、金额 = first_kill;total = big_reward + Σ drops;余额 = 累计入账
|
||||
# ⑥ 逐杀掉落收束入账(迭代 10——掉落已在冒险内逐杀入账〔summary.keystone_total 含全部〕,
|
||||
# keystone_drops 明细随摘要归档;首杀标记仍由 apply 逐通关关次置位)
|
||||
var d1: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 11})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("first_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": true, "amount": int(row1.get("first_kill", 0))}],
|
||||
"master_seed": 11})
|
||||
var drop1: Dictionary = d1["boss_drops"][0]
|
||||
check(bool(drop1.get("first", false)) and int(drop1.get("amount", 0)) == int(row1.get("first_kill", -1))
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5)")
|
||||
check(int(d1.get("total", 0)) == 3 + int(row1.get("first_kill", 0)), "明细 total = big_reward + Σ drops")
|
||||
check(sv.keystone_balance() == 3 + int(row1.get("first_kill", 0)), "余额 = 累计入账(大奖励 + Boss 两档)")
|
||||
# ⑥ 同档再通 → 重复杀档且恒定不递减(第三轮同额)
|
||||
and int(drop1.get("level", 0)) == 1, "新档首通 L1:boss_drops[0] {first:true, amount=first_kill}(验收5——明细随摘要归档)")
|
||||
check(int(d1.get("total", 0)) == int(row1.get("first_kill", 0)), "明细 total = keystone_total(逐杀掉落全源——无二次派生加账)")
|
||||
check(sv.keystone_balance() == int(row1.get("first_kill", 0)), "余额 = 累计入账(逐杀掉落合计——无重复加账)")
|
||||
# ⑥ 再通转复杀档(首杀标记置位后小额恒定不递减——标记面在 apply,金额面在逐杀裁决)
|
||||
var d2: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 12})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
|
||||
"master_seed": 12})
|
||||
var drop2: Dictionary = d2["boss_drops"][0]
|
||||
check(not bool(drop2.get("first", false)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
check(not bool(drop2.get("first", true)) and int(drop2.get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"再通 L1:{first:false, amount=repeat_kill}(首杀标记置位后转小额)")
|
||||
var d3: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 1,
|
||||
"stopped_at": 2, "keystone_total": 3, "master_seed": 13})
|
||||
"stopped_at": 2, "keystone_total": int(row1.get("repeat_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))}],
|
||||
"master_seed": 13})
|
||||
check(not bool(d3["boss_drops"][0].get("first", true)) and int(d3["boss_drops"][0].get("amount", 0)) == int(row1.get("repeat_kill", -1)),
|
||||
"重复杀第三轮恒定不递减(验收5——无衰减曲线)")
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀
|
||||
# 混合归因:通关两关 → L1 重复 + L2 首杀(明细随摘要逐笔归档)
|
||||
var d4: Dictionary = mp.settle_adventure({"result": "failed", "cleared": false, "levels_cleared": 2,
|
||||
"stopped_at": 0, "keystone_total": 4, "master_seed": 14})
|
||||
"stopped_at": 0, "keystone_total": int(row1.get("repeat_kill", 0)) + int(row2.get("first_kill", 0)),
|
||||
"keystone_drops": [{"level": 1, "first": false, "amount": int(row1.get("repeat_kill", 0))},
|
||||
{"level": 2, "first": true, "amount": int(row2.get("first_kill", 0))}],
|
||||
"master_seed": 14})
|
||||
check(not bool(d4["boss_drops"][0].get("first", true)) and bool(d4["boss_drops"][1].get("first", false))
|
||||
and int(d4["boss_drops"][1].get("amount", 0)) == int(row2.get("first_kill", -1)),
|
||||
"两关通关逐关归因:L1 重复档 + L2 首杀档(first_kill 金额)")
|
||||
@@ -3457,7 +3568,8 @@ func _meta_settle_and_workshop() -> void:
|
||||
# ⑩ recent_summaries:元素含 boss_drops 副本、最新在前、cap 10
|
||||
var rs: Array = sv.recent_summaries()
|
||||
check(rs.size() >= 5 and String(rs[0].get("result", "")) != "" and rs[0].has("boss_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含 boss_drops 副本(复盘可归因——验收5)")
|
||||
and (rs[1] as Dictionary).has("keystone_drops")
|
||||
and int(rs[0].get("master_seed", -1)) == 15, "摘要最新在前 + 元素含掉落明细副本(复盘可归因——验收5)")
|
||||
var sv_c = _meta_new_save(_META_SMOKE_PATHS[4])
|
||||
var mp_c = load("res://core/meta_progression.gd").new(_db, sv_c)
|
||||
for i in 12:
|
||||
@@ -3603,8 +3715,8 @@ func _smoke_save() -> void:
|
||||
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)) == 2,
|
||||
"迁移链 0→1(测试钩)→2(生产) 写盘 version == 2(v2=断点域出生,迭代 8 卡①)")
|
||||
check(f7.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[5], FileAccess.READ).get_as_text()).get("version", -1)) == 3,
|
||||
"迁移链 0→1(测试钩)→3(生产) 写盘 version == 3(v3=断点附随件扩列,迭代 10 奖励重构)")
|
||||
|
||||
# ⑧ 域界干净:meta 永久进度域不含局内资源键(行动点/RAP/GP/道具库存/生命账本不入档——契约 2;
|
||||
# 迭代 8 卡①修订:扫描收窄到 meta 域——断点域=冒险进行中瞬态快照,生命/GP 按拍板面入域豁免)
|
||||
@@ -3632,9 +3744,11 @@ func _smoke_save() -> void:
|
||||
var st9: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
var f9 = sm_script.new(paths[8])
|
||||
f9.apply_adventure_summary(st9.get("summary", {}))
|
||||
var kpl := int(_db.adventure.keystone_per_level)
|
||||
var ks_run := 0
|
||||
for li in range(1, int(_db.level.rows.size()) + 1):
|
||||
ks_run += int(_db.meta_boss_keystone_row(li).get("first_kill", 0))
|
||||
check(String(st9.get("result", "")) == "completed", "摘要集成:冒险驱动满配通关(seed 42)")
|
||||
check(f9.keystone_balance() == 3 * kpl, "集成钥石余额 = 3 × keystone_per_level(%d)" % f9.keystone_balance())
|
||||
check(f9.keystone_balance() == ks_run, "集成钥石余额 = ΣBoss 首杀行值(%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),
|
||||
"集成三关首杀标记齐")
|
||||
@@ -3643,11 +3757,11 @@ func _smoke_save() -> void:
|
||||
var st9b: Dictionary = autoplay.run_adventure(_db, 42, 900, 400)
|
||||
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.keystone_balance() == 2 * ks_run, "第二局后余额翻倍(2 × Σ首杀——模拟驱动恒首杀口径)")
|
||||
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
|
||||
check(r9.load_from_disk() and r9.keystone_balance() == 2 * ks_run and r9.clear_count(3) == 2
|
||||
and r9.recent_summaries().size() == 2 and r9.boss_first_kill_marked(3),
|
||||
"集成写盘读回全量保持")
|
||||
|
||||
@@ -3698,9 +3812,9 @@ func _smoke_save() -> void:
|
||||
fold.store_string("{\"version\": 1, \"meta\": {\"keystone_balance\": 4}}")
|
||||
fold.close()
|
||||
check(fo.load_from_disk() and fo.keystone_balance() == 4 and not fo.has_breakpoint(),
|
||||
"v1 旧档迁移 v2:断点域补空(合法空态,无断点域不炸)")
|
||||
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 2,
|
||||
"v1 旧档迁移后写盘 version == 2")
|
||||
"v1 旧档迁移 v3:断点域补空(合法空态,无断点域不炸)")
|
||||
check(fo.write_to_disk() and int(JSON.parse_string(FileAccess.open(paths[9], FileAccess.READ).get_as_text()).get("version", -1)) == 3,
|
||||
"v1 旧档迁移后写盘 version == 3")
|
||||
|
||||
# 段尾清理(测试文件绝不残留)
|
||||
for p in stale:
|
||||
|
||||
@@ -91,6 +91,47 @@ func _init() -> void:
|
||||
var battle_d = runner_d.current_battle()
|
||||
check(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)")
|
||||
|
||||
# ---- Part 5:三选一停点中断→续玩(迭代 10 奖励重构——候选不重抽、已兑付不重付、次数计数一致)----
|
||||
var rng_e = load("res://core/rng_service.gd").new()
|
||||
var sm_e = SaveManagerScript.new(_bp_path)
|
||||
var adr_e = AdventureRunnerScript.new(db, rng_e)
|
||||
adr_e.set_save_manager(sm_e)
|
||||
var params_e := {"hero_override": {}, "aux_unlocked": {"blast_kit": true}}
|
||||
check(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)")
|
||||
adr_e._bonus_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推)
|
||||
adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"})
|
||||
var cands_e: Array = (adr_e._bonus_pending.get("candidates", []) as Array).duplicate()
|
||||
check(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)")
|
||||
var sm_f = SaveManagerScript.new(_bp_path)
|
||||
check(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)")
|
||||
var bp_f: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var bp_f_cands: Array = (bp_f.get("bonus_pending", {}) as Dictionary).get("candidates", []) as Array
|
||||
check(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)")
|
||||
check(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)")
|
||||
var got_cands := {"cands": [] as Array}
|
||||
var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构)
|
||||
adr_f.bonus_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate())
|
||||
check(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)")
|
||||
check(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)")
|
||||
check(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)")
|
||||
check(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)")
|
||||
check(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)")
|
||||
check(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理")
|
||||
var picked_e: String = String(adr_f._bonus_result.get("picked", ""))
|
||||
if adr_f._bonus_taken.has(picked_e):
|
||||
check(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1),
|
||||
"P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e)
|
||||
check(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)")
|
||||
var bp_g: Dictionary = sm_f.breakpoint_snapshot()
|
||||
var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new())
|
||||
adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载
|
||||
var got_result := {"r": {}}
|
||||
adr_g.bonus_resolved.connect(func(r): got_result["r"] = r.duplicate())
|
||||
check(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理")
|
||||
check(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)")
|
||||
check(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)")
|
||||
|
||||
_clear_disk()
|
||||
if _problems.is_empty():
|
||||
print("[breakpoint_resume_test] PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")
|
||||
|
||||
Reference in new issue
Block a user