iter10 目检修复批: 掷定前置落籍(用户推荐案获批)——道具件具体道具随候选抽定(_last_draw_aux/reward_pick流序内挪)+卡片直显「魔法道具/道具名·效果摘要」(_aux_card_line+Narrator aux_line参)+断点快照随载aux_detail(续玩详情不丢)+node_bonus_candidates扩dict+候选一致要求降级(冒险结构/level-clear 措辞软化——用户裁决非验收义务)+fx_shot_probe演出抓帧探针入册+目检台账收口(7/9/10/11/14机测承载或实测PASS/8项用户终裁非本迭代不追)(冒烟628/0+run_all 15套+五探针+三闸零新增+熵增PASS)

This commit is contained in:
DKChan committed 2026-10-01 16:10:11 +08:00
1 parent 1a46854337
commit cc8807e66c
11 files changed
+255 -37

No files matched your search

+44 -12
View File
@@ -80,6 +80,7 @@ var _bonus_pending := {} # 停点态:{"candidates": Array, "p
var _bonus_result := {} # 已兑付待推进态(bonus_resolved 载荷副本——中断续玩不重兑付)
var _bonus_aux_carry: Array[String] = [] # 道具件跨关携带(兑付后注入下一关库存——清库后授予的体验窗)
var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源)
var _last_draw_aux: Array[String] = [] # 最近一次抽签掷定的道具件(掷定前置——卡片直显具体道具;追加批二)
var _node_bonus_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽)
@@ -166,6 +167,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
if not bp_pending.is_empty():
_pending_next_level = true
_bonus_pending = {"candidates": (bp_pending.get("candidates", []) as Array).duplicate(true)}
if bp_pending.has("aux_detail"):
_bonus_pending["aux_detail"] = (bp_pending.get("aux_detail", []) 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"
@@ -277,7 +280,9 @@ func pick_bonus(id: String) -> bool:
return true
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)
# 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)。
# 掷定前置(追加批二):具体道具随候选抽定(_last_draw_aux / _bonus_pending.aux_detail)——本函数消费预掷值,
# 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。
func _pick_aux_item(count: int) -> bool:
var pool: Array = _bonus_aux_pool()
if pool.is_empty():
@@ -285,8 +290,14 @@ func _pick_aux_item(count: int) -> bool:
return false
var hp_before := int(ledger.get("hero_hp", 0))
var picked: Array[String] = []
var detail: Array = (_bonus_pending.get("aux_detail", []) as Array).duplicate()
if detail.is_empty():
detail = _last_draw_aux.duplicate()
for i in maxi(1, count):
picked.append(_roll_bonus_aux_item(pool))
if i < detail.size():
picked.append(String(detail[i]))
else:
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:
@@ -335,14 +346,16 @@ func _finish_bonus_pick(picked: String, value: int, hp_before: int) -> void:
# ---- 奖励节点三选一(迭代 10 追加批——用户拍板:与通关三选一共享同池同权重同次数上限)----
# 候选呈现(节点到达时调用——恰一 reward_pick scope 取数;本停点断点不载,
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)
func node_bonus_candidates() -> Array:
return _draw_bonus_candidates()
# 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载,
# 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。
# 掷定前置(追加批二):返回 dict 带 aux_detail(道具件具体道具)供卡片直显。
func node_bonus_candidates() -> Dictionary:
var cands: Array = _draw_bonus_candidates()
return {"candidates": cands, "aux_detail": _last_draw_aux.duplicate()}
func present_node_bonus(candidates: Array) -> void:
_node_bonus_pending = {"candidates": candidates.duplicate(true)}
func present_node_bonus(candidates: Array, aux_detail: Array = []) -> void:
_node_bonus_pending = {"candidates": candidates.duplicate(true), "aux_detail": aux_detail.duplicate(true)}
# 点选兑付(节点域——无部分回血无钥石;件获取计数与牌库剔除与通关三选一同账)
@@ -381,15 +394,20 @@ func apply_node_bonus(id: String) -> bool:
# 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用);
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨)
# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨)。
# 掷定前置:消费 _node_bonus_pending.aux_detail 预掷值(缺省回退当场掷定)。
func _apply_node_aux(count: int) -> bool:
var pool: Array = _bonus_aux_pool()
if pool.is_empty():
push_error("[冒险结构] apply_node_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)")
return false
var picked: Array[String] = []
var detail: Array = (_node_bonus_pending.get("aux_detail", []) as Array).duplicate()
for i in maxi(1, count):
picked.append(_roll_bonus_aux_item(pool))
if i < detail.size():
picked.append(String(detail[i]))
else:
picked.append(_roll_bonus_aux_item(pool))
var slots := maxi(0, int(db.adventure.transitional_sync_slots))
var records: Array = []
if _runner != null and _runner.aux_inventory != null:
@@ -442,9 +460,11 @@ func _finish_node_bonus(picked: String, value: int) -> void:
# 加权洗牌不放回掷 3(E-S weight-key:每件 key = U(0,1)^(1/权重) 取前 3——单次零重复、逐件按权重出
# 概率;达次数上限件先剔牌库〔AP 用尽 2 次/属性件达 3 次永不再现;GP/道具件无次数上限,牌库恒非空〕;
# 道具件解锁池空集〔白板/门控态〕不入牌库。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同三候选)
# 道具件解锁池空集〔白板/门控态〕不入牌库。消费 reward_pick 封闭流——一次停点恰一 scope,同种子同三候选;
# 道具件具体道具在本调用内掷定(_last_draw_aux——掷定前置,卡片直显,追加批二)。
func _draw_bonus_candidates() -> Array:
var deck: Array = []
_last_draw_aux = []
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:
@@ -460,10 +480,20 @@ func _draw_bonus_candidates() -> Array:
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"]))
var cid := String(keyed[i]["id"])
out.append(cid)
if cid == "aux_item":
var roll_rg: RandomNumberGenerator = rng.open_stream("reward_pick")
for j in maxi(1, int(db.adventure.bonus_aux_item_count)):
_last_draw_aux.append(_roll_from(roll_rg))
return out
func _roll_from(rg: RandomNumberGenerator) -> String:
var pool: Array = _bonus_aux_pool()
return String(pool[rg.randi_range(0, pool.size() - 1)])
# 三选一道具件随机池(与投放门控同源:aux 活行 ∩ 解锁门控集——空集 = 道具件不入牌库)
func _bonus_aux_pool() -> Array:
var out: Array = []
@@ -587,6 +617,7 @@ func _on_level_finished(payload: Dictionary) -> void:
var pick_payload: Dictionary = {
"level_index": level_index,
"candidates": _draw_bonus_candidates(),
"aux_detail": _last_draw_aux.duplicate(), # 掷定前置(追加批二——道具件具体道具随候选抽定,卡片直显)
"heal_gained": heal,
"hp_before": hp_before,
"hp_after": int(ledger.get("hero_hp", 0)),
@@ -596,6 +627,7 @@ func _on_level_finished(payload: Dictionary) -> void:
"aux_cleared": int(payload.get("aux_cleared", 0)), # additive 扩签(迭代 7 T4——关卡末清空反馈取数)
}
_bonus_pending = {"candidates": (pick_payload["candidates"] as Array).duplicate(true),
"aux_detail": _last_draw_aux.duplicate(),
"payload": pick_payload.duplicate(true)}
bonus_choice_presented.emit(pick_payload)
_snapshot_breakpoint() # 选择停点可中断(契约 5——续玩不重抽不静默丢失)
+5 -2
View File
@@ -92,8 +92,9 @@ static func bonus_pick_header(payload: Dictionary) -> String:
return "\n".join(lines)
# 三选一选项卡文案(件名 + 效果读数;值 = 冒险表行值,组合根喂入)
static func bonus_card_text(id: String, value: int) -> String:
# 三选一选项卡文案(件名 + 效果读数;值 = 冒险表行值,组合根喂入;道具件掷定前置时
# aux_line = 具体道具「名 · 效果摘要」行——追加批二,空 = 无预掷回退通用文案)
static func bonus_card_text(id: String, value: int, aux_line: String = "") -> String:
match id:
"attack":
return "攻击力提升\n攻击 +%d" % value
@@ -104,6 +105,8 @@ static func bonus_card_text(id: String, value: int) -> String:
"gp":
return "金币袋\n金币 +%d" % value
"aux_item":
if aux_line != "":
return "魔法道具\n%s" % aux_line
return "魔法道具\n随机获得一件"
"ap":
return "行动点提升\n每战首轮 AP +%d" % value
+39 -6
View File
@@ -397,10 +397,11 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面,
# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。
_bonus_context = "reward_node"
var node_cands: Array = adventure.node_bonus_candidates()
var node_draw: Dictionary = adventure.node_bonus_candidates()
var node_cands: Array = node_draw.get("candidates", []) as Array
_open_modal(bonus_overlay)
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands))
adventure.present_node_bonus(node_cands)
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array)
log_view.append_bb(Narrator.reward_line())
elif t == "rest":
var healed: int = int(info.get("healed", 0))
@@ -441,20 +442,52 @@ func _on_bonus_presented(payload: Dictionary) -> void:
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))
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
_refresh_presentations()
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
var _last_replace_incoming := {} # 同上——incoming 记录
func _bonus_card_texts(candidates: Array) -> Array:
func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array:
var out: Array = []
var aux_i := 0
for id in candidates:
out.append({"id": String(id), "text": Narrator.bonus_card_text(String(id), int(db.adventure.bonus_value(String(id))))})
var cid := String(id)
var text := ""
if cid == "aux_item":
var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else ""
aux_i += 1
text = Narrator.bonus_card_text(cid, int(db.adventure.bonus_value(cid)), _aux_card_line(iid))
else:
text = Narrator.bonus_card_text(cid, int(db.adventure.bonus_value(cid)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)
func _aux_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.aux_item_row(iid)
if row.is_empty():
return ""
var parts: Array[String] = []
for k in (row.get("effect_kinds", []) as Array):
if String(k) == "mine_reveal":
parts.append("真视")
elif String(k) == "reward_preview":
parts.append("预读")
elif String(k) == "ap_grant":
parts.append("AP +%d" % int(row.get("magnitude", 0)))
var tgt := String(row.get("target", "none"))
if tgt == "area_3x3":
parts.append("3×3")
elif tgt == "board_wide":
parts.append("全图")
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
func _on_bonus_pick_confirmed(item_id: String) -> void:
if adventure == null:
return
+131
View File
@@ -0,0 +1,131 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 演出渲染证据探针(渲染层测试解冻批 2026-10-01——fx_shot_probe:真实渲染设备抓战斗演出瞬间帧)。
# 必须**窗口化**运行(真渲染设备——headless 无渲染面):Godot_console.exe --path . res://smoke/fx_shot_probe.tscn -- <输出目录>
# 流程 = shot_probe 同款隔离面(存档/设置/元进度换注隔离档,禁碰真实档)+ 真实按钮路径开冒险进首战;
# 自动玩家挖格驱动行结算,在 行到达/弹道飞行/结算排播 三时刻抓帧——演出链「逻辑在(fx_probe PASS)×
# 渲染在(本探针 PNG)」双证的第二半边。PNG 落输出目录(artwork/qa-fx-*/,素材纪律不入 git)。
extends Node
var main
var out_dir := ""
var _problems: Array = []
func _ready() -> void:
var args := OS.get_cmdline_user_args()
out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://qa-fx"
DirAccess.make_dir_recursive_absolute(out_dir)
await get_tree().process_frame
main = load("res://presentation/main.tscn").instantiate()
add_child(main)
await get_tree().process_frame
for p0 in ["user://fx_shot_probe.json", "user://fx_shot_probe.json.tmp", "user://fx_shot_probe_settings.cfg"]:
if FileAccess.file_exists(p0):
DirAccess.remove_absolute(p0)
var iso_c = load("res://core/save_manager.gd").new("user://fx_shot_probe.json")
iso_c.load_from_disk()
main.save_manager = iso_c
main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c)
main.settings_store = load("res://presentation/settings_store.gd").new("user://fx_shot_probe_settings.cfg")
for i in 8:
await get_tree().process_frame # UI 装配完成等足帧(canvas_item 空引用窗口)
var lvbtn: Button = _find_button_by_sub(main._start_overlay, "开始冒险")
if lvbtn == null:
_fail("开始冒险按钮缺失")
return
lvbtn.pressed.emit()
# 等关卡画面出图(choices_presented 异步到达——最多 3s 轮询)
for i in 60:
await get_tree().process_frame
if main.level_screen != null and main.level_screen.visible:
break
if main.level_screen == null or not main.level_screen.visible:
_fail("关卡画面未出现(lvbtn 路径)")
return
for i in 5:
await get_tree().process_frame
main.level_screen.press_candidate(0)
for i in 20:
await get_tree().process_frame
var st = main.settlement
if st == null:
_fail("首战未开(settlement 空)")
return
var rules = main.settlement.minesweeper
var board = rules.current_board()
# 自有驱动(只挖/走——测试驱动可用板面数据挑安全格;新手策略档的插旗拒绝会耗尽预算):挖到 3 格停
var dug := 0
var guard := 40
while dug < 3 and guard > 0:
guard -= 1
var target = null
for n in board.neighbors8(rules.hero_pos()):
if not rules.is_revealed(n) and not board.is_mine(n):
target = n
break
if target != null:
var out: Array = rules.settle_batch([{"action": "dig", "cell": target}])
if out.is_empty() or not bool(out[0].get("accepted", false)):
continue
dug += 1
await get_tree().create_timer(0.12).timeout
await _snap("fx_dig%d" % dug) # 行到达/弹道在飞窗口
await get_tree().create_timer(0.35).timeout
await _snap("fx_settle%d" % dug) # 结算排播窗口
continue
# 无相邻可挖 → 走向有未开安全格邻居的已揭开格(挖掘期移动免费)
var dest = null
for c in rules.revealed_cells():
if c == rules.hero_pos() or rules.compute_move_path(c).is_empty():
continue
for n in board.neighbors8(c):
if not rules.is_revealed(n) and not board.is_mine(n):
dest = c
break
if dest != null:
break
if dest == null:
break
rules.settle_batch([{"action": "move", "cell": dest}])
# 停手 → 轮末结算(敌方攻击弹道/受击闪白/结算排播在 0.2-1.6s 窗口流过)——连抓 6 帧
rules.settle_batch([{"action": "stop"}])
for i in 6:
await get_tree().create_timer(0.25).timeout
await _snap("fx_round%d" % (i + 1))
if dug == 0:
_fail("无安全格可挖(首板全雷不可信)")
return
await get_tree().create_timer(0.6).timeout
await _snap("fx_final")
if _problems.is_empty():
print("[fx_shot_probe] PASS — 演出帧 %d 张落 %s(挖格 %d)" % [dug * 2 + 1, out_dir, dug])
get_tree().quit(0)
else:
for p in _problems:
push_error("[fx_shot_probe] FAIL — " + p)
get_tree().quit(1)
func _snap(tag: String) -> void:
await RenderingServer.frame_post_draw
var img: Image = get_viewport().get_texture().get_image()
if img == null or img.is_empty():
_problems.append("抓帧空纹理: " + tag)
return
img.save_png("%s/%s.png" % [out_dir, tag])
func _find_button_by_sub(root: Node, needle: String) -> Button:
if root is Button and String(root.text).contains(needle):
return root
for c in root.get_children():
var r := _find_button_by_sub(c, needle)
if r != null:
return r
return null
func _fail(msg: String) -> void:
_problems.append(msg)
push_error("[fx_shot_probe] FAIL — " + msg)
get_tree().quit(1)
+6
View File
@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://smoke/fx_shot_probe.gd" id="1"]
[node name="FxShotProbe" type="Node"]
script = ExtResource("1")
+3 -2
View File
@@ -174,12 +174,13 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
elif t == "reward":
# 奖励节点三选一(迭代 10 追加批——共享池确定性策略:候选取首件、替换放弃新件;
# 兑付后 continue_after_node 回图选路)
var ncands: Array = adr.node_bonus_candidates()
var ndraw: Dictionary = adr.node_bonus_candidates()
var ncands: Array = ndraw.get("candidates", []) as Array
st.reward_events.append({"node_bonus_pick": ncands.duplicate(true)})
if ncands.is_empty():
r.continue_after_node() # 牌库空合法态直通(结构上不可达——gp/道具件无上限,防御保留)
else:
adr.present_node_bonus(ncands)
adr.present_node_bonus(ncands, ndraw.get("aux_detail", []) as Array)
if adr.apply_node_bonus(String(ncands[0])):
if not adr._node_bonus_pending.is_empty() and adr._node_bonus_pending.has("incoming"):
adr.resolve_bonus_replace(-1) # 满位替换:放弃新件
+15 -7
View File
@@ -551,9 +551,14 @@ func _smoke_adventure() -> void:
check(a13.start_adventure(13, {"first_script": true, "aux_unlocked": {"blast_kit": true}}),
"节点共享池场景:冒险启动(seed 13)")
a13._bonus_taken["attack"] = int(_db.adventure.bonus_cap_attack)
var ndraw: Array = a13.node_bonus_candidates()
check(not (ndraw as Array).has("attack"),
"奖励节点共享池:达上限件同样剔牌库(同 _draw 同上限——%s)" % str(ndraw))
var ndraw: Dictionary = a13.node_bonus_candidates()
var ncands13: Array = ndraw.get("candidates", []) as Array
check(not ncands13.has("attack"),
"奖励节点共享池:达上限件同样剔牌库(同 _draw 同上限——%s)" % str(ncands13))
if ncands13.has("aux_item"):
check(not (ndraw.get("aux_detail", []) as Array).is_empty(),
"掷定前置:道具件候选自带具体道具(aux_detail 随抽签掷定——卡片直显数据源)")
a13.present_node_bonus(ncands13, ndraw.get("aux_detail", []) as Array)
a13.present_node_bonus(["gp"])
var gp13 := int(a13.ledger.get("gp", 0))
check(a13.apply_node_bonus("gp") and int(a13.ledger.get("gp", 0)) == gp13 + int(_db.adventure.bonus_gp_amount),
@@ -590,10 +595,13 @@ func _adv_drive_to_shop(a, st: Dictionary, autoplay) -> bool:
var t := String(r.current_node().get("type", ""))
if t == "reward":
# 奖励节点三选一(迭代 10 追加批——候选取首件、替换放弃新件、兑付确认回图)
var ncands: Array = a.node_bonus_candidates()
if not ncands.is_empty() and a.apply_node_bonus(String(ncands[0])):
if not a._node_bonus_pending.is_empty() and a._node_bonus_pending.has("incoming"):
a.resolve_bonus_replace(-1)
var ndraw: Dictionary = a.node_bonus_candidates()
var ncands: Array = ndraw.get("candidates", []) as Array
if not ncands.is_empty():
a.present_node_bonus(ncands, ndraw.get("aux_detail", []) as Array)
if a.apply_node_bonus(String(ncands[0])):
if not a._node_bonus_pending.is_empty() and a._node_bonus_pending.has("incoming"):
a.resolve_bonus_replace(-1)
r.continue_after_node()
elif t == "rest":
r.continue_after_node()