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:
DKChan committed 2026-10-01 04:30:13 +08:00
1 parent 49b263fbc8
commit 71502ceecd
18 files changed
+992 -169

No files matched your search

+259 -25
View File
@@ -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()
+9
View File
@@ -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——任一效果有信息即可用);
# 纯预读行 = 板上还有未开格才可用。
+13
View File
@@ -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,
+20 -3
View File
@@ -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 对话)----
+7 -11
View File
@@ -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,
+1 -1
View File
@@ -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
+14 -8
View File
@@ -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)