From b7a2eadd2c22a5e8caa865eae5c00f401e339cef Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 12 Oct 2026 19:11:11 +0800 Subject: [PATCH] =?UTF-8?q?T4(=E8=BF=AD=E4=BB=A322=E7=A7=98=E5=AE=9D?= =?UTF-8?q?=E6=89=B9):=20=E5=91=88=E7=8E=B0=E6=8D=A2=E4=BB=A3=E2=80=94?= =?UTF-8?q?=E2=80=94=E7=A7=98=E5=AE=9D=E5=8C=A3=E6=9F=9C=E5=8F=B0=E8=B4=AD?= =?UTF-8?q?=E4=B9=B0=E9=92=AE(buy=5Frequested=E4=BF=A1=E5=8F=B7=E9=93=BE?= =?UTF-8?q?=E2=86=92presenter=20buy=5Ftreasure=5Funlock=E5=88=86=E6=94=AF,?= =?UTF-8?q?=E7=A6=81=E7=94=A8=E6=80=81=3D=E4=BD=99=E9=A2=9D=E4=B8=8D?= =?UTF-8?q?=E8=B6=B3)+=E5=88=86=E6=A1=A3=E5=91=88=E7=8E=B0(=E5=90=8D?= =?UTF-8?q?=E5=90=8E=E7=BC=80=C2=B7=E6=A1=A3=E4=BD=8D/=E8=A3=85=E9=85=8D?= =?UTF-8?q?=E8=A1=8C=E6=A1=A3=E4=BD=8D=E6=A0=87=E7=AD=BE/=E6=82=AC?= =?UTF-8?q?=E5=81=9Cdesc)+=E8=A3=85=E9=85=8D=E6=AE=B5=E6=9C=AA=E8=A7=A3?= =?UTF-8?q?=E9=94=81=E7=A6=81=E8=A3=85;=20=E5=91=88=E7=8E=B0=E6=A1=A5?= =?UTF-8?q?=E6=8D=A2=E4=BB=A3(=E6=97=A7=E6=8F=90=E7=81=AF=E5=B8=83?= =?UTF-8?q?=E5=B0=94=E6=A1=A5set=5Ffog=5Flantern/FOG=5FLANTERN=5FBONUS?= =?UTF-8?q?=E9=9A=8F=E8=A1=8C=E9=80=80=E5=BD=B9=E2=86=92set=5Ffog=5Fglow?= =?UTF-8?q?=5Fbonus=E5=8F=82=E6=95=B0=E9=9D=A2,main.=5Fsync=5Ftreasure=5Fg?= =?UTF-8?q?low=E6=B6=88=E8=B4=B9attune=5Fparam=5Fsum,off=E6=80=81=E5=86=99?= =?UTF-8?q?0=E5=A4=8D=E5=8E=9F=E5=A5=91=E7=BA=A6=E4=BF=9D=E6=8C=81);=20?= =?UTF-8?q?=E5=B7=A2=E7=A9=B4=E9=A2=84=E5=91=8Atoast(nest=5Fpreview=5Fcoun?= =?UTF-8?q?t=E8=AF=BB=E6=95=B0=E5=8F=A3=E6=B6=88=E8=B4=B9);=20=E8=AF=95?= =?UTF-8?q?=E7=82=BC=E9=95=90HUD=E5=85=A5=E5=8F=A3(ap=5Fbar=E5=85=91?= =?UTF-8?q?=E6=8D=A2=E9=92=AE+convert=5Fpressed=E8=BD=AC=E5=8F=91=E9=93=BE?= =?UTF-8?q?+attune=5Fconvert=5Fstate=E8=AF=BB=E6=95=B0=E5=8F=A3,=E6=9C=AA?= =?UTF-8?q?=E8=A3=85=E9=85=8D=E9=9A=90=E8=97=8F);=20ui=5Finput=5Fprobe=20c?= =?UTF-8?q?ase=E2=91=AE=E6=8D=A2=E4=BB=A3=E4=B8=BB=E8=AF=ADwide=5Fscout=5F?= =?UTF-8?q?band+4;=20V13=E5=8A=9F=E8=83=BD=E6=AE=8B=E7=95=99=E6=8D=A2?= =?UTF-8?q?=E4=BB=A3(breakpoint=5Fresume/=20smoke=E5=89=94=E7=89=8C?= =?UTF-8?q?=E5=BA=93/reward=5Foverlay=E5=A4=B9=E5=85=B7=E6=8D=A2=E6=96=B0?= =?UTF-8?q?=E4=BB=B6id)+=E8=BF=87=E6=97=B6=E6=B3=A8=E9=87=8A=E6=B8=85?= =?UTF-8?q?=E7=90=86;=20=E4=BF=AE=E5=91=88=E7=8E=B0=E7=BB=84=E8=A3=85?= =?UTF-8?q?=E6=96=AD=E8=A3=82=3DBoardZoneView=E8=A1=A5convert=5Fpressed?= =?UTF-8?q?=E8=BD=AC=E5=8F=91(=E6=8C=82=E6=AD=BB=E6=A0=B9=E5=9B=A0=3Dmain?= =?UTF-8?q?=E7=9B=B4=E8=BF=9E=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84=E6=9D=BF?= =?UTF-8?q?=E5=8C=BA=E4=BF=A1=E5=8F=B7=E8=87=B4=5Fbuild=5Fui=E4=B8=AD?= =?UTF-8?q?=E6=96=AD)=E3=80=82=E5=86=92=E7=83=9F895/0+run=5Fall=2022?= =?UTF-8?q?=E5=A5=97+=E6=8E=A2=E9=92=88=E5=90=ABclick=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=E3=80=94=E7=A7=98=E5=AE=9D=E6=89=B9=20V12/V13/V19=E3=80=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 22 ++++++- game/data/schemas/treasure_item_table.gd | 9 ++- game/presentation/camp_shop_presenter.gd | 11 +++- game/presentation/main.gd | 51 +++++++++++----- game/presentation/views/ap_bar_view.gd | 17 ++++++ game/presentation/views/board_view.gd | 15 +++-- game/presentation/views/board_zone_view.gd | 6 ++ game/presentation/views/camp_overlay_view.gd | 26 +++++++-- game/smoke/smoke_all.gd | 8 +-- game/smoke/ui_input_probe.gd | 58 ++++++++++--------- .../tests/unit/core/breakpoint_resume_test.gd | 16 ++--- .../unit/presentation/reward_overlay_test.gd | 2 +- 12 files changed, 167 insertions(+), 74 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 3011a104..ab02386f 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -77,7 +77,7 @@ var _flag_consumed := {} # 兑付资格以雷格实例为键 # 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) var attunement: Array = [] # 装备中同调件 item_id 集 var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改) -var _attune_safe_seen := 0 # 掘进蓄能 battle 域安全开格计数(进度呈现取数面;积点账本=效果引擎计数器 dig_charge_bank——行 triggers counter_spend 订阅同账本) +var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(迭代 22 T3 通用化——装配集 ap_charge_safe 行扫描;积点账本=行 triggers counter_spend 声明的计数器,旧 dig_charge_bank 硬编码随行退役) # ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零) @@ -204,7 +204,7 @@ func _attune_has(item_id: String) -> bool: return attunement.has(item_id) -# 装配集只读出口(迭代 19 卡④——呈现桥消费面:组合根每战装配链判「装配集含 fog_lantern」→ +# 装配集只读出口(迭代 19 卡④立——呈现桥消费面现归 attune_param_sum〔迭代 22 T4 换代〕;本口保留 # board_view.set_fog_lantern。_attune_has 契约化出口,save v6 只读访问器先例;private 面内部调用 # 零改动。只读零副作用——呈现侧判定专用,非结算写入口) func attune_has(item_id: String) -> bool: @@ -291,6 +291,24 @@ func use_attune_convert(item_id: String) -> Dictionary: return {"ok": true, "item_id": item_id} +# 积点制件受理状态读数(迭代 22 秘宝批 T4——HUD 兑换钮态数据源:assembled=装配在册/ +# points=积点账现值/need=受理闸阈值〔行 counter_spend value〕。未装配=assembled false) +func attune_convert_state() -> Dictionary: + var row := _attune_charge_safe_row() + if row.is_empty() or String(row.get("form", "")) != "active_charged": + return {"assembled": false, "points": 0, "need": 0, "display_name": ""} + var bank := "" + var need := 1 + for tr_v in (row.get("triggers", []) as Array): + var dd: Dictionary = (tr_v as Dictionary).get("do", {}) as Dictionary + if String(dd.get("kind", "")) == "counter_spend": + bank = String(dd.get("counter_id", "")) + need = maxi(1, int(dd.get("value", 1))) + break + return {"assembled": true, "points": _fx.counter(bank) if bank != "" else 0, "need": need, + "display_name": String(row.get("display_name", ""))} + + # 特性求值(内容数据驱动扩展批 T3——_feat_flags_for 挂点求值退役:效果行为=数据行 triggers 订阅, # 效果引擎 dispatch 分发;旧 23 hook 枚举与旗注入消亡。流派档位质变=FeatsEngine 纯函数求值 # (档位机制本身系引擎基建非内容效果),两运行态注伤害控制器:档一 rider+1 / 档二 rider 可暴击) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index b8aefed8..f6db6863 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -9,11 +9,10 @@ # effect_kinds ⊆ {mine_reveal, reward_preview, ap_grant}(活性校验见下)/ target ∈ {none, area_3x3, board_wide} / # magnitude [0,999](仅 ap_grant 行消费,加载校验 ≥1)/ use_phase ∈ {active_dig}。 # additive 两标记(迭代 7 T1——拍板①④的表内表达,非 registry fields 预登记字段的扩列随批登记): -# retired = 退役留痕行(loot_compass——未开格类型微光常显覆盖预读价值,2026-09-29 用户拍板退役; -# 行名保留=meta.tres workshop_items 六件词表恰全校验兼容,存档 item_unlocks 旧键无害; -# 免活性校验、不入投放校验、不入 F2 覆盖面、奖励节点/掉落池排除) -# deferred = 效果失义待重定行(probe_dust/catacomb_scroll——原显影半边随雾格移除失义,效果与目标 -# 随秘宝整体重设计归 Alpha(2026-09-20 用户拍板);免 effect_kinds 非空校验、不入投放与 F2) +# retired = 退役留痕行(历史形态——迭代 11-19 期载体;新目录 12 行零 retired 行〔迭代 22 T1 全量 +# 换代〕,机制面保留=deferred/retired 双标记的加载豁免通道仍在役) +# deferred = 效果失义待重定行(历史形态——显影尘/地穴全图卷 2026-09-20 拍板;新目录零 deferred 行, +# 机制面保留同上;免 effect_kinds 非空校验、不入投放与 F2) # 承重护栏(秘宝 GDD §结构冻结,schema 校验强制不靠运行时拦截): # 真视窄域——effect_kinds 含 mine_reveal 的活行 target 必须 area_3x3(真视一次最多 3×3,禁全图看雷); # board_wide 仅恢复推理原料类——target=board_wide 的活行 effect_kinds 不得含 mine_reveal(现行无此类活行, diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index a2234f22..b64c6857 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -79,9 +79,18 @@ func on_camp_buy(attr: String) -> void: var meta = _meta_get.call() if meta == null: return + # 秘宝钥石购买(迭代 22 秘宝批 T4——秘宝匣柜台购买钮意图;写口唯一=buy_treasure_unlock, + # 余额闸/重复购/条件件误购三重拒绝零副作用) + if attr != "attune_refresh": + if not meta.buy_treasure_unlock(attr): + toast.show_toast("购买被拒(余额不足或不可购买)", "warn") + return + camp_overlay.refresh_camp(camp_data()) + _refresh_presentations.call() + return # Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;双入口之一 Q1 裁乙——树上 ks_refresh 同账; # 按钮禁用为常态防线,兜底拒绝可见 pattern 1。属性锻造面已退役——buy_forge 分支随批删除) - if attr != "attune_refresh" or not meta.buy_attune_refresh(): + if not meta.buy_attune_refresh(): toast.show_toast("购买被拒", "warn") return camp_overlay.refresh_camp(camp_data()) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index e2718d77..312ab523 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -235,6 +235,7 @@ func _build_ui() -> void: info_panel.treasure_slot_clicked.connect(_treasure_ui.on_slot_clicked) # 迭代 7 T4——两步交互第一步 board_zone.stop_pressed.connect(_on_stop_pressed) board_zone.refresh_confirmed.connect(_on_refresh_confirmed) + board_zone.convert_pressed.connect(_on_convert_pressed) # 积点兑换(迭代 22 秘宝批 T4——试炼镐 HUD 入口) board_zone.legend_toggled.connect(_on_legend_toggled) board_zone.hover_forbidden.connect(_on_hover_forbidden) board_zone.chord_attempted.connect(_on_chord_attempted) @@ -849,14 +850,15 @@ func _on_round_started(ri: int) -> void: else: _row_pacer.enqueue({"__round_line": ri}) settlement_fx.play_round_banner(ri) - # 提灯视野同步(迭代 19 卡④——呈现桥:每战首轮按装配集显式重算 set_fog_lantern 含否〔🔴 契约 7 - # Q8.1 复原语义:禁依赖战斗结束隐式复原——下一战装配链重算即含「未装配→写 0」复原〕)。挂点时序 - # 依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)——board_changed 在 - # _install_new_board 内先发、装配集后注入,同帧直挂会读到上一战的旧集,故挂此处不挂 _on_board_changed。 - # settlement 空防御照 :854 既有形态(接手先于本信号:冒险路径 board_changed 内 runner 同步、debug - # 路径 battle_created 先于 begin_battle)。未装配=false→写 0=缺省零扰动基线逐位不变。 + # 秘宝呈现同步(迭代 22 秘宝批 T4 换代——旧提灯桥 _sync_fog_lantern 随行退役;每战首轮装配链 + # 显式重算〔🔴 契约复原语义沿迭代 19 卡④ Q8.1:下一战装配链重算即含「未装配→写 0」复原〕。 + # 挂点时序依据不变:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。 + # settlement 空防御照既有形态。未装配=参数 0→写 0=缺省零扰动基线逐位不变。) if ri == 1: - _sync_fog_lantern() + _sync_treasure_glow() + # 巢穴预告呈现(迭代 22 秘宝批 T4——装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) + if settlement != null and int(settlement.nest_preview_count) >= 0: + board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(settlement.nest_preview_count), "info") # 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 if ri == 1 and settlement != null: @@ -880,14 +882,14 @@ func _on_board_changed() -> void: _refresh_presentations() -# 提灯视野呈现桥(迭代 19 卡④——装配集只读出口消费面:settlement.attune_has("fog_lantern") 真值 -# 显式写给 board_view.set_fog_lantern(true=半径 8+2 / false=复原 0),写入即 setter 内 refresh 全板 -# 重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:只写呈现态布尔, -# minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 -func _sync_fog_lantern() -> void: +# 秘宝视野呈现桥(迭代 22 秘宝批 T4 换代——旧提灯桥随 fog_lantern 行退役;阔视野带 light_radius_bonus +# 装配参数合计显式写给 board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter +# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: +# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 +func _sync_treasure_glow() -> void: if settlement == null or board_view == null: return - board_view.set_fog_lantern(settlement.attune_has("fog_lantern")) + board_view.set_fog_glow_bonus(settlement.attune_param_sum("light_radius_bonus")) # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- @@ -1233,9 +1235,32 @@ func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round) HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining) + _sync_convert_button() _sync_settlement_visuals() +# 积点兑换钮态(迭代 22 秘宝批 T4——读数口命令式重查;未装配=隐藏零占位) +func _sync_convert_button() -> void: + if settlement == null: + board_zone.ap_bar.set_convert_state(false, 0, 0, "") + return + var cs: Dictionary = settlement.attune_convert_state() + board_zone.ap_bar.set_convert_state(bool(cs.get("assembled", false)), int(cs.get("points", 0)), + int(cs.get("need", 0)), String(cs.get("display_name", ""))) + + +func _on_convert_pressed() -> void: + if settlement == null: + return + var v: Dictionary = settlement.use_attune_convert("trial_pick") + if not bool(v.get("ok", false)): + board_zone.toast.show_toast(String(v.get("reason", "无法兑换")), "warn") + return + logger.log_action("convert", Vector2i(-1, -1), true) + board_view.refresh() + _refresh_presentations() + + func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 743595cf..efa27b5f 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -13,6 +13,7 @@ extends PanelContainer signal stop_pressed signal refresh_confirmed signal legend_toggled +signal convert_pressed # 积点兑换意图(迭代 22 秘宝批 T4——写口唯一=BattleOrchestrator.use_attune_convert) const COL_GOLD := UiKit.GOLD const COL_DOT_OFF := UiKit.DOT_OFF @@ -21,6 +22,7 @@ const COL_INK := UiKit.INK var _dots: HBoxContainer var _ap_label: Label var _btn_stop: Button +var _btn_convert: Button # 积点兑换钮(迭代 22 秘宝批 T4——试炼镐 HUD 入口;未装配隐藏) var _btn_legend: Button var _btn_refresh: Button var _refresh_sub: Label @@ -61,6 +63,12 @@ func _init() -> void: _btn_stop.pressed.connect(_emit_stop_pressed) row.add_child(_btn_stop) + _btn_convert = UiKit.button("兑换", 0.0) # 行板紧凑钮豁免——积点兑换(试炼镐;未装配隐藏) + _btn_convert.size_flags_vertical = Control.SIZE_SHRINK_CENTER + _btn_convert.visible = false + _btn_convert.pressed.connect(func(): convert_pressed.emit()) + row.add_child(_btn_convert) + var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL row.add_child(spacer) @@ -128,6 +136,15 @@ func set_refresh_cost(cost: int) -> void: _confirm_lab.text = "RAP−%d · 旧板旗作废" % cost +# 积点兑换钮(迭代 22 秘宝批 T4——试炼镐转化获取 HUD 入口:装配时在列〔visible〕、 +# 积点未达 disabled;文案=件名+进度。未装配=隐藏零占位) +func set_convert_state(assembled: bool, points: int, need: int, display_name: String) -> void: + _btn_convert.visible = assembled + if assembled: + _btn_convert.text = "%s %d/%d" % [display_name, points, need] + _btn_convert.disabled = points < need + + func set_refresh_state(remaining: int, ready: bool) -> void: _refresh_sub.text = "RAP−%d · 剩 %d 次" % [refresh_cost, remaining] _btn_refresh.disabled = not ready diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 3be5b0c8..7e8bd7ba 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -136,16 +136,15 @@ var _digit_plate := false # 数字色牌垫底模式(双峰纹理 # ---- 视野迷雾(迭代 11 第④批 T2/T4——纯呈现层;正主=HUD框架「视野迷雾」节 v0.13,渲染规格=art-bible §6.5 v3.5)---- const FOG_RADIUS := 8 # 菱形灯光区基准半径(曼哈顿距离 ≤R——与微光环「周围 8 格」同圈起步;半径基准值改动=非白名单须用户) -const FOG_LANTERN_BONUS := 2 # 提灯视野外扩插值位(+2=2026-09-30 拍板值;载体=迭代 19 卡④ fog_lantern 秘宝行落籍 2026-10-08——原「归 Alpha 信息件批」指路句随批换代) var _fog_enabled := true # 迷雾开关(呈现态布尔——真实游戏恒 on;探针/夹具可 off=无迷雾基线,两态结算逐位一致断言面) -var _fog_lantern := 0 # 提灯加成现值(缺省 0=无提灯基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_fog_lantern) +var _fog_lantern := 0 # 视野加成现值(缺省 0=无加成基线零扰动;生产写入面=组合根每战装配链显式重算——main._sync_treasure_glow) -# 提灯视野外扩接线(迭代14⑤——2026-09-30 拍板④:灯甲视野外扩 +2、类型微光不随视野扩〔微光锚=可挖域 -# 贴身 8 邻零改动〕。FOG_LANTERN_BONUS 常量与半径算式迷雾批已在位,本 setter=公共接线面:载体道具 -# 〔fog_lantern 魔法提灯——迭代 19 卡④落籍 2026-10-08,treasure_items.tres 秘宝·同调位新行〕已到位, -# 组合根 main._sync_fog_lantern 每战首轮装配链显式重算调用;缺省 0=无提灯基线零扰动。 -func set_fog_lantern(enabled: bool) -> void: - _fog_lantern = FOG_LANTERN_BONUS if enabled else 0 +# 视野加成接线(迭代 22 秘宝批 T4 换代——旧提灯布尔桥 set_fog_lantern 与 FOG_LANTERN_BONUS 常量 +# 随 fog_lantern 行退役摘除〔V12〕;新主语=阔视野带 light_radius_bonus 装配参数(magnitude 4→半径 +# 8+4=12 格 provisional);类型微光不随视野扩〔微光锚=可挖域贴身 8 邻零改动〕沿迭代 14⑤ 拍板。 +# 组合根 main._sync_treasure_glow 每战首轮装配链显式重算调用;缺省 0=off 态与旧基线逐位一致。 +func set_fog_glow_bonus(bonus: int) -> void: + _fog_lantern = maxi(0, bonus) refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用) var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口) diff --git a/game/presentation/views/board_zone_view.gd b/game/presentation/views/board_zone_view.gd index 1fef6e35..79b7a61b 100644 --- a/game/presentation/views/board_zone_view.gd +++ b/game/presentation/views/board_zone_view.gd @@ -18,6 +18,7 @@ var _board_area: Control # resized 命名转发用(原 lambda 局部捕获 signal action_requested(cell: Vector2i, kind: String) signal stop_pressed signal refresh_confirmed +signal convert_pressed # 积点兑换意图转发(迭代 22 秘宝批 T4——试炼镐 HUD 入口,源=ap_bar) signal legend_toggled signal hover_forbidden(cell: Vector2i) signal chord_attempted(cell: Vector2i) @@ -82,6 +83,7 @@ func _init() -> void: board_view.chord_attempted.connect(_forward_chord) ap_bar.stop_pressed.connect(_emit_stop_pressed) ap_bar.refresh_confirmed.connect(_emit_refresh_confirmed) + ap_bar.convert_pressed.connect(_emit_convert_pressed) ap_bar.legend_toggled.connect(_emit_legend_toggled) @@ -105,6 +107,10 @@ func _emit_stop_pressed() -> void: stop_pressed.emit() +func _emit_convert_pressed() -> void: + convert_pressed.emit() + + func _emit_refresh_confirmed() -> void: refresh_confirmed.emit() diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 49822af8..5b62eecf 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -333,6 +333,9 @@ func refresh_camp(data: Dictionary) -> void: card.status_l.text = st_text card.status_l.add_theme_color_override("font_color", st_col) card.status_l.tooltip_text = st_text # 截断兜底(状态列 EXPAND_FILL 吸余宽——悬停看全文) + # 购买钮余额联动(迭代 22 秘宝批 T4——禁用态=余额不足,常态防线;pattern 1 兜底拒绝可见归 presenter) + if card.get("buy_btn") != null: + card.buy_btn.disabled = balance < int(card.get("price", 0)) # 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动) var att: Dictionary = data.get("attune", {}) if _attune_header_l != null: @@ -345,7 +348,8 @@ func refresh_camp(data: Dictionary) -> void: for card2 in _attune_btns: var on: bool = eq_set.has(String(card2.item_id)) card2.btn.text = "卸下" if on else "装配" - card2.btn.disabled = (not on) and eq.size() >= slots_n # 满位禁选 + # 满位禁选+未解锁禁装(迭代 22 T4——已装件恒可卸下;unlocked 快照随 rebuild 刷新) + card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) if _refresh_l != null: var rf: Dictionary = att.get("refresh", {}) _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] @@ -368,10 +372,12 @@ func _rebuild_workshop(data: Dictionary) -> void: var line := HBoxContainer.new() line.add_theme_constant_override("separation", 12) card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK) + var tier_name := String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "")) + var name_l := UiKit.label(String(row.get("display_name", "")) + ("·" + tier_name if tier_name != "" else ""),UiKit.FS_HINT, true, UiKit.INK) name_l.custom_minimum_size = Vector2(132, 0) name_l.clip_text = true name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_l.tooltip_text = String(row.get("desc", "")) # 件效果摘要悬停可见(分档呈现契约——槽位档位=名后缀) line.add_child(name_l) var progress_l := UiKit.label("",UiKit.FS_HINT, false, UiKit.STONE_DEEP) # 尾裁微批a 2026-10-08——同行价格墨同案 progress_l.custom_minimum_size = Vector2(150, 0) @@ -384,8 +390,15 @@ func _rebuild_workshop(data: Dictionary) -> void: status_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS status_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL line.add_child(status_l) + # 钥石购买钮(迭代 22 秘宝批 T4——购买件且未解锁时在列;写口唯一=buy_treasure_unlock, + # 经 buy_requested(item_id) 信号走 presenter 分支;禁用态=余额不足) + var buy_btn: Button = null + if not bool(row.get("unlocked", false)) and String(row.get("mode", "")) == "keystone": + buy_btn = UiKit.button("购买 %d" % int(row.get("price", 0)), 0.0) # 行板紧凑钮豁免 + buy_btn.pressed.connect(_emit_buy_requested.bind(String(row.get("item_id", "")))) + line.add_child(buy_btn) _workshop_holder.add_child(card) - _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l}) + _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l, "buy_btn": buy_btn, "price": int(row.get("price", 0)), "unlocked": bool(row.get("unlocked", false))}) _rebuild_attune(data) @@ -408,12 +421,17 @@ func _rebuild_attune(data: Dictionary) -> void: var form_l := UiKit.label("被动" if String(row.get("form", "")) == "passive" else "主动·充能",UiKit.FS_CAPTION, false, UiKit.STONE) form_l.custom_minimum_size = Vector2(58, 0) line.add_child(form_l) + # 档位标签(迭代 22 秘宝批 T4——槽位档位显示:件档=普通/优秀/稀有,装入须件档≤格档契约 2 的呈现半边) + var tier_l := UiKit.label(String(UiKit.TREASURE_TIER_LABELS.get(String(row.get("tier", "")), "—")),UiKit.FS_CAPTION, false, UiKit.STONE) + tier_l.custom_minimum_size = Vector2(36, 0) + line.add_child(tier_l) var btn := UiKit.button("装配", 0.0) # 行板紧凑钮豁免 btn.pressed.connect(attune_toggled.emit.bind(iid)) + btn.disabled = not bool(row.get("unlocked", false)) # 未解锁禁装(解锁为装配前置——toggle 写口同守卫) line.add_child(btn) _workshop_holder.add_child(line) _attune_nodes.append(line) - _attune_btns.append({"item_id": iid, "btn": btn}) + _attune_btns.append({"item_id": iid, "btn": btn, "unlocked": bool(row.get("unlocked", false)), "equipped": bool(row.get("equipped", false))}) var refresh_line := HBoxContainer.new() refresh_line.add_theme_constant_override("separation", 6) _refresh_l = UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE) diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 6502eb5f..cb0c5b65 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -601,8 +601,8 @@ func _smoke_adventure() -> void: # ⑪ 池耗即枯竭 + 池收缩 + 满血回 0(TP0——white-box:耗尽 attack/ap 令牌后候选零出现; # 种类次数上限退役,「达上限剔除」语义由牌库耗尽承载) var a11 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(11)) - check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}), - "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 blast_kit)") + check(a11.start_adventure(11, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}), + "剔牌库/替换场景:冒险启动(seed 11——道具解锁集只含 wide_scout_band)") for di in range(a11._curio_deck.size() - 1, -1, -1): var dk := String((a11._curio_deck[di] as Dictionary).get("kind", "")) if dk == "attack" or dk == "ap": @@ -635,7 +635,7 @@ func _smoke_adventure() -> void: "满血通关回血 0 合法(封顶口径——heal_gained=0 非负溢出)") # ⑬ 奖励节点共享池(迭代 10 追加批——同库同池同摘牌;TP0:attack 令牌耗尽后节点域同样零出现) var a13 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(13)) - check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"blast_kit": true}}), + check(a13.start_adventure(13, {"first_script": true, "treasure_unlocked": {"wide_scout_band": true}}), "节点共享池场景:冒险启动(seed 13)") for di13 in range(a13._curio_deck.size() - 1, -1, -1): if String((a13._curio_deck[di13] as Dictionary).get("kind", "")) == "attack": @@ -4744,7 +4744,7 @@ func _meta_settle_and_workshop() -> void: # 三键恒空(缺陷①),冒烟夹具直呼 runner 参数面拦不住——此处直呼 main.build_adventure_params # 单测统一构造面(static 免实例化整菜单场景;真实两入口共用本构造),取值与营地投影同源) var sv_e = _meta_new_save(_META_SMOKE_PATHS[8]) - sv_e.set_attunement_equipped(["luck_coin"]) + sv_e.set_attunement_equipped(["morning_order"]) sv_e.set_attune_refresh_level(2) sv_e.set_fork_side("fg_posture", "a") var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e) diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index 14caae64..cba6f874 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -328,9 +328,9 @@ func _ready() -> void: # ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位; # 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ---- var fogp: Dictionary = bv.fog_params_probe() - if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(fogp["lantern"]) != 0 or not fogp["enabled"]: - _problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)" - % [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(fogp["lantern"]), str(fogp["enabled"])]) + if int(bv.FOG_RADIUS) != 8 or int(fogp["lantern"]) != 0 or not fogp["enabled"]: + _problems.append("case⑮ 迷雾参数漂移(R=%s lantern=%s enabled=%s 应=8/0/true——旧提灯常量随行退役摘除)" + % [str(bv.FOG_RADIUS), str(fogp["lantern"]), str(fogp["enabled"])]) else: var hero15: Vector2i = ms.hero_pos() var b15 = ms.current_board() @@ -392,8 +392,9 @@ func _ready() -> void: if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \ and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS): _problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)") - # 提灯 +2 接线断言(迭代14⑤——set_fog_lantern(true) → 半径 8+2:dist==R+1 格由雾态转可见, - # 关闭复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位) + # 视野加成接线断言(迭代 22 秘宝批 T4 换代——旧提灯 +2 主语随 fog_lantern 行退役; + # 新主语=阔视野带参数注入 set_fog_glow_bonus(4) → 半径 8+4:dist==R+1 格由雾态转可见, + # 卸装复原 lantern=0;纯接线零逻辑——半径算式迷雾批已在位) var ring_cell := Vector2i(-99, -99) for y in b15.height: for x in b15.width: @@ -403,39 +404,40 @@ func _ready() -> void: if ring_cell.x != -99: break if ring_cell.x == -99: - _problems.append("case⑮ 板面无 dist==R+1 格(提灯断言取数异常)") + _problems.append("case⑮ 板面无 dist==R+1 格(视野加成断言取数异常)") else: var pre_lantern: int = int(bv.cells_probe()[ring_cell].fog_state) - bv.set_fog_lantern(true) - if int(bv.fog_params_probe()["lantern"]) != 2: - _problems.append("case⑮ 提灯接雾参数未变(lantern 应=2)") + bv.set_fog_glow_bonus(4) + if int(bv.fog_params_probe()["lantern"]) != 4: + _problems.append("case⑮ 视野加成接雾参数未变(lantern 应=4)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: - _problems.append("case⑮ 提灯 +2 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern) - bv.set_fog_lantern(false) + _problems.append("case⑮ 视野 +4 后 dist==R+1 格仍被遮(应转可见 fog_state=0,改前=%d)" % pre_lantern) + bv.set_fog_glow_bonus(0) if int(bv.fog_params_probe()["lantern"]) != 0: - _problems.append("case⑮ 提灯关闭后雾参数未复原(lantern 应=0)") + _problems.append("case⑮ 视野加成清零后雾参数未复原(lantern 应=0)") if int(bv.cells_probe()[ring_cell].fog_state) == 0: - _problems.append("case⑮ 提灯关闭后 dist==R+1 格未复原遮蔽") - # 载体级断言(迭代 19 卡④——fog_lantern 装配链贯通:①装配→开局参数面〔build_adventure_params - # 统一构造=卡⓪①缺陷修复面回归〕②装配集→呈现桥〔_sync_fog_lantern 消费 attune_has 只读出口 - # →set_fog_lantern〕→视野 +2 生效与卸装复原。settlement.attunement 手动注入=模拟 begin_battle - # enc 注入后的运行态〔注入面本体归冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用) - main.save_manager.set_attunement_equipped(["fog_lantern"]) + _problems.append("case⑮ 视野加成清零后 dist==R+1 格未复原遮蔽") + # 装配链级断言(迭代 22 秘宝批 T4 换代——wide_scout_band 装配链贯通:①装配→开局参数面 + # 〔build_adventure_params 统一构造〕②装配集→呈现桥〔_sync_treasure_glow 消费 + # attune_param_sum 只读出口→set_fog_glow_bonus〕→视野 +4 生效与卸装复原。 + # settlement.attunement 手动注入=模拟 begin_battle enc 注入后的运行态〔注入面本体归 + # 冒烟+单测覆盖〕;不重启冒险——case⑯ 起既有板面继续用) + main.save_manager.set_attunement_equipped(["wide_scout_band"]) var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) - if not (p_lantern.get("attunement", []) as Array).has("fog_lantern"): - _problems.append("case⑮ 载体装配未进开局参数面(attunement=%s 应含 fog_lantern)" % str(p_lantern.get("attunement", []))) - main.settlement.attunement = ["fog_lantern"] - main._sync_fog_lantern() - if int(bv.fog_params_probe()["lantern"]) != 2: - _problems.append("case⑮ 载体装配后视野未扩(lantern 应=2——呈现桥未生效)") + if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"): + _problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", []))) + main.settlement.attunement = ["wide_scout_band"] + main._sync_treasure_glow() + if int(bv.fog_params_probe()["lantern"]) != 4: + _problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: - _problems.append("case⑮ 载体装配后 dist==R+1 格仍被遮(应转可见)") + _problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)") main.settlement.attunement = [] - main._sync_fog_lantern() + main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 0: - _problems.append("case⑮ 载体卸装后视野未复原(lantern 应=0——契约 7 显式重算含否)") + _problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)") if int(bv.cells_probe()[ring_cell].fog_state) == 0: - _problems.append("case⑮ 载体卸装后 dist==R+1 格未复原遮蔽") + _problems.append("case⑮ 卸装后 dist==R+1 格未复原遮蔽") main.save_manager.set_attunement_equipped([]) # ---- case⑯ 暂停层输入锁语义(迭代 17 卡③ R3——开层锁棋盘、关层恢复无残留;真实 push_input # 路径:入口角标本体 press+release 真点击 → 暂停层开;锁期挖掘点击零穿透;「继续」关层后 diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index fba5cce5..e03e481f 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -91,7 +91,7 @@ func _init() -> void: 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": {}, "treasure_unlocked": {"blast_kit": true}} + var params_e := {"hero_override": {}, "treasure_unlocked": {"wide_scout_band": true}} Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)") adr_e._curio_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推) adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"}) @@ -128,24 +128,24 @@ func _init() -> void: Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)") Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)") - # ---- Part 6:装配存档回归(迭代 19 卡④——装配→停点→回局载体态一致:params.attunement 随续玩 - # 受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归呈现桥(main._sync_fog_lantern), - # 本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)---- + # ---- Part 6:装配存档回归(迭代 19 卡④立,迭代 22 秘宝批 T4 主语换代——装配→停点→回局载体态 + # 一致:params.attunement 随续玩受理面重建再注入战斗〔begin_battle enc 携带〕;呈现态每战重算归 + # 呈现桥(main._sync_treasure_glow),本断言钉核心层装载往返——回局后 attune_has 只读出口读数一致)---- var rng_h = load("res://core/rng_service.gd").new() var sm_h = SaveManagerScript.new(_bp_path) var adr_h = AdventureRunnerScript.new(db, rng_h) adr_h.set_save_manager(sm_h) - var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["fog_lantern"]} - Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 fog_lantern)") + var params_h := {"hero_override": {}, "treasure_unlocked": {}, "attunement": ["morning_order"]} + Kit.check_quiet(adr_h.start_adventure(5678, params_h), "P6 装配冒险启动(seed 5678——params 带 morning_order)") Kit.check_quiet(adr_h.runner().pick_next_node(int(adr_h.runner().next_candidate_ids()[0])), "P6 选路进首战") - Kit.check_quiet(adr_h.runner().current_battle().attune_has("fog_lantern"), "P6 首战装配集含 fog_lantern(attune_has 只读出口)") + Kit.check_quiet(adr_h.runner().current_battle().attune_has("morning_order"), "P6 首战装配集含 morning_order(attune_has 只读出口)") var sm_i = SaveManagerScript.new(_bp_path) Kit.check_quiet(sm_i.load_from_disk() and sm_i.has_breakpoint(), "P6 断点在档(停点可中断)") var adr_i = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) adr_i.set_save_manager(sm_i) Kit.check_quiet(adr_i.resume_adventure(sm_i.breakpoint_snapshot(), params_h), "P6 续玩受理(装配集随 params 再注入)") Kit.check_quiet(adr_i.runner().pick_next_node(int(adr_i.runner().next_candidate_ids()[0])), "P6 续玩选路进首战") - Kit.check_quiet(adr_i.runner().current_battle().attune_has("fog_lantern"), "P6 回局首战装配集仍含 fog_lantern(停点往返载体态一致)") + Kit.check_quiet(adr_i.runner().current_battle().attune_has("morning_order"), "P6 回局首战装配集仍含 morning_order(停点往返载体态一致)") _clear_disk() quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点/装配存档回归 全绿")) diff --git a/game/tests/unit/presentation/reward_overlay_test.gd b/game/tests/unit/presentation/reward_overlay_test.gd index 2aefa086..13fdd17b 100644 --- a/game/tests/unit/presentation/reward_overlay_test.gd +++ b/game/tests/unit/presentation/reward_overlay_test.gd @@ -69,7 +69,7 @@ func _run() -> void: ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"): return # ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通) - ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1) + ov.open_replace([{"index": 0, "item_id": "incense_stick", "display_name": "线香"}], "天眼", 1, 1) var replace_btns := 0 for c in ov._replace_box.get_children(): if c is Button: