feat(t5): 奖励节点三选一+工坊解锁门控+封存呈现+node-reward spec 出生
①node_overlay:show_reward_items 三选一(池≤3 全列——当前三件即全列;池空直通=切片期合法空态保留;选择前继续隐藏=强制构筑点语义)+item_chosen 信号+close_reward 收屏 ②main:奖励分支接三选一(活行∩解锁集,注册序前 3,呈现侧零 RNG)+_on_reward_item_chosen(入库存来源归因 reward_node→toast→_on_node_continue 兑付确认回选路零自动推进)+_reward_node_choices ③解锁门控链(拍板③甲=解锁进投放池):main _aux_unlocked_set(工坊解锁∩活行;罗盘退役/失义件不入集)→adventure params.aux_unlocked→LevelRunner.set_aux_unlocked→BattleOrchestrator 公有 setter→RewardEconomy.set_aux_unlocked+_rollable_rows aux 行过滤——空集默认零投放=白板/夹具态模拟读数零扰动 ④工坊呈现:workshop_status 投影+deferred/retired 状态面;camp 文案=已解锁·能力封存中(失义件)/已退役(罗盘) ⑤node-reward.md spec v1.0 出生(06 条目兑现) 坑一轮:BattleOrchestrator 无公有 reward_economy(E1/E3 封装)——公有 setter 转发;竖线锚四处拼接病自愈 闸:冒烟 577/0+四探针 PASS(click 问题=0)+单测 0 FAIL
This commit is contained in:
1 parent
2f3a87e98f
commit
9553d6b182
9 files changed
+146
-4
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# 节点奖励屏 UX spec(node-reward)
|
||||
|
||||
> 状态:v1.0(2026-09-29 迭代 7 T5 随批出生——06「节点奖励屏 UX spec」条目兑现;上游拍板=奖励节点三选一〔用户 2026-09-29 拍板②丙〕)
|
||||
> 事实源:`game/presentation/views/node_overlay.gd`(show_reward_items/close_reward)+ `main.gd`(_reward_node_choices/_on_reward_item_chosen)
|
||||
> 相邻:[level-clear](level-clear.md)(关卡结算屏)/ [level-map](level-map.md)(关卡画面)/ [魔法道具](../systems/排雷循环/魔法道具.md)(内容权威)
|
||||
|
||||
## 定位
|
||||
|
||||
奖励节点(reward node)到达即开的**模态构筑点**:从可用道具池取一件加入关卡背包。「节点奖励 = 功能道具的正式渠道」(关卡节点地图 v1.0 ⑤ 定稿)的呈现承载。
|
||||
|
||||
## 布局与交互
|
||||
|
||||
- 全屏变暗层 + 居中标题「关卡奖励点 / 取一件魔法道具(加入关卡背包——关卡结束清空)」。
|
||||
- 道具按钮横排三枚(150×72:字形大字=道具名首字 + 全名两行);**选择前不给出口**(继续按钮隐藏——强制三选一的构筑点语义)。
|
||||
- 池 ≤3 全列(现行有效道具三件:工兵令/探巢摆钟/爆破工兵包——选一即全列);池扩充后按注册序取前 3(呈现侧零 RNG 消费纪律)。
|
||||
- 池空(全未解锁/表空合法态)= 直通节点确认「此行无道具可取——继续前行」(切片期空态语义保留)。
|
||||
- 选择 → 道具入关卡域库存(来源归因 `reward_node`)→ toast「获得 X(入关卡背包)」→ C 区槽位刷新 → 兑付确认回选路(零自动推进同款)。
|
||||
|
||||
## 口径
|
||||
|
||||
- 可用池 = aux 活行 ∩ 工坊解锁集(拍板③甲「解锁→进投放池」同源门控——未解锁不入奖励池与掉落池)。
|
||||
- 退役件(探宝罗盘)/失义件(显影尘/地穴全图卷)不入池;营地呈现「已退役 / 已解锁·能力封存中」。
|
||||
- 库存语义=关卡域(跨战斗延续、关卡末清空+快照反馈);使用=挖掘期两步交互(C 区槽选中→棋盘目标格)。
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 改动 |
|
||||
|---|---|---|
|
||||
| v1.0 | 2026-09-29 | 迭代 7 T5 随批出生(三选一构筑点形态——用户拍板②丙) |
|
||||
@@ -65,6 +65,7 @@ var _override := [0, 0] # TunePanel 百分比通道 [atk_pct,
|
||||
var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0}
|
||||
var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板)
|
||||
var _last_aux_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面)
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.aux_unlocked 接收面)
|
||||
|
||||
|
||||
func _init(db_ref, rng_ref) -> void:
|
||||
@@ -94,6 +95,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
_pending_next_level = false
|
||||
_base_override = _normalize_override(params.get("hero_override", {}))
|
||||
_first_script = bool(params.get("first_script", false)) # 缺省 = 非首次(抽签路径;真实侧由呈现层读存档布尔位传入)
|
||||
_aux_unlocked = params.get("aux_unlocked", {}) if params.get("aux_unlocked", {}) is Dictionary else {} # additive(迭代 7 T5)
|
||||
ledger["hero_hp"] = int(db.hero.max_hp) + int(_base_override["max_hp"]) # 生命账本置满(表值+永久强化 flat——加算区刚归零)
|
||||
state = "running"
|
||||
return _start_current_level()
|
||||
|
||||
@@ -401,6 +401,11 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary:
|
||||
return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec}
|
||||
|
||||
|
||||
# aux 解锁门控转发(迭代 7 T5——LevelRunner 注入;E1/E3 封装:外部经公有面,禁直读 _reward_economy)
|
||||
func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
_reward_economy.set_aux_unlocked(ids)
|
||||
|
||||
|
||||
func _minesweeper_ref() -> Variant:
|
||||
return minesweeper
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ var _shop_purchased := {} # product_id → 已购档数(现价
|
||||
# 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口
|
||||
var _settlement = null # 当前战斗编排器(关卡结束置 null)
|
||||
var aux_inventory = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续,
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放)
|
||||
# 关卡末清空+快照计数入 level_finished 载荷)
|
||||
|
||||
var hero_atk_pct := 0 # 测试面板覆盖值(切片专用)
|
||||
@@ -369,6 +370,8 @@ func _start_battle(node: Dictionary) -> bool:
|
||||
if aux_inventory == null:
|
||||
aux_inventory = AuxItems.new(db) # 惰性建——关卡域单实例(跨战斗延续)
|
||||
_settlement.aux_inventory = aux_inventory # 迭代 7 T3——库存注入(授予路由+use_aux 编排消费面)
|
||||
if _settlement != null:
|
||||
_settlement.set_aux_unlocked(_aux_unlocked) # 迭代 7 T5——解锁门控下传(装板摇号前)
|
||||
# core↔core 接线只许方法 Callable(ADR-0001 静态规则 S6——lambda 捕获 self 会延长
|
||||
# RefCounted 生命周期乃至成环:settlement 的信号持捕获 runner 的 lambda,runner 又持 settlement)
|
||||
_settlement.step_row.connect(_forward_step_row)
|
||||
@@ -472,6 +475,20 @@ func pick_next_node(node_id: int) -> bool:
|
||||
func current_battle():
|
||||
return _settlement
|
||||
|
||||
# aux 解锁门控集注入(迭代 7 T5——AdventureRunner 自 params 传递)
|
||||
func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
_aux_unlocked = ids
|
||||
if _settlement != null:
|
||||
_settlement.set_aux_unlocked(ids)
|
||||
|
||||
# 奖励节点兑付透传(迭代 7 T5——呈现层三选一 → 关卡域库存;来源归因 reward_node)
|
||||
func grant_aux_item(item_id: String, source: String) -> Dictionary:
|
||||
if aux_inventory == null:
|
||||
aux_inventory = AuxItems.new(db)
|
||||
return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0)
|
||||
|
||||
return _settlement
|
||||
|
||||
|
||||
# ---- 玩家输入转发(呈现层只与 runner 对话)----
|
||||
|
||||
|
||||
@@ -86,12 +86,15 @@ func workshop_status() -> Array:
|
||||
var out: Array = []
|
||||
for row in db.meta_workshop_items():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
var arow: Dictionary = db.aux_item_row(iid) # 迭代 7 T5——失义/退役状态面(呈现「能力封存/已退役」)
|
||||
out.append({
|
||||
"item_id": iid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"threshold": int(row.get("clear_threshold", 0)),
|
||||
"unlocked": save.item_unlocked(iid),
|
||||
"progress": _workshop_progress(String(row.get("channel", "total_clears"))),
|
||||
"deferred": bool(arow.get("deferred", false)),
|
||||
"retired": bool(arow.get("retired", false)),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
@@ -204,10 +204,23 @@ func weighted_pick(rows: Array, r: int) -> String:
|
||||
return ""
|
||||
|
||||
|
||||
# aux 解锁门控集(迭代 7 T5——拍板③甲「解锁→进投放池」:未解锁 aux 行不入摇号池。
|
||||
# 空集默认 = 全部 aux 不投放(白板/夹具态零投放——模拟读数零扰动;真实侧 main 组装工坊解锁集传入)
|
||||
var _aux_unlocked := {}
|
||||
|
||||
|
||||
func set_aux_unlocked(ids: Dictionary) -> void:
|
||||
_aux_unlocked = ids
|
||||
|
||||
|
||||
func _rollable_rows() -> Array:
|
||||
# 摇号池 = cell 池正向行 weight>0(负向行纯字典不消费权重;封锁格不在本函数职责域——清单由调用方裁剪)
|
||||
var out: Array = []
|
||||
for row in db.reward.rows:
|
||||
if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE:
|
||||
var iid := String(row.get("effect_id", ""))
|
||||
if iid == "" or not _aux_unlocked.has(iid):
|
||||
continue
|
||||
if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" and int(row.weight) > 0:
|
||||
out.append(row)
|
||||
return out
|
||||
|
||||
@@ -68,6 +68,7 @@ var _adventure_gp_view := 0 # GP 冒险钱包呈现侧影子(迭
|
||||
# wallet_changed gain/buy 载荷=入账后总额 level_runner.gd:482/539)
|
||||
var _adventure_gp_final := 0 # 收束清零拍暂存的最终合计("clear" 到达时自影子转存)
|
||||
var _aux_use_selection := 0 # 魔法道具选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取)
|
||||
var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——工坊解锁×活行交集,冒险开始组装)
|
||||
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
|
||||
var tune_panel: TunePanel
|
||||
|
||||
@@ -149,6 +150,7 @@ func _build_ui() -> void:
|
||||
legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay))
|
||||
review_overlay.closed.connect(_on_modal_closed.bind(review_overlay))
|
||||
node_overlay.continue_pressed.connect(_on_node_continue)
|
||||
# 迭代 7 T5——item_chosen 连接在奖励分支首唤时防重接线(上方)
|
||||
level_screen.route_chosen.connect(_on_route_chosen)
|
||||
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||||
@@ -250,7 +252,7 @@ func _start_adventure() -> void:
|
||||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||||
# 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签
|
||||
var first_script: bool = not save_manager.is_first_adventure_done()
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script}):
|
||||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux_unlocked_set()}):
|
||||
_fatal_screen("冒险启动失败——见输出日志")
|
||||
return
|
||||
|
||||
@@ -284,7 +286,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
|
||||
log_view.append_bb(Narrator.node_banner(runner, node, t))
|
||||
elif t == "reward":
|
||||
_open_modal(node_overlay)
|
||||
node_overlay.show_reward()
|
||||
node_overlay.item_chosen.connect(_on_reward_item_chosen) if not node_overlay.item_chosen.is_connected(_on_reward_item_chosen) else null
|
||||
node_overlay.show_reward_items(_reward_node_choices())
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
log_view.append_bb(Narrator.reward_line())
|
||||
elif t == "rest":
|
||||
var healed: int = int(info.get("healed", 0))
|
||||
@@ -728,6 +732,51 @@ func _on_shop_leave() -> void:
|
||||
|
||||
# ---- 魔法道具 HUD 接线(迭代 7 T4——库存条+两步交互+叠加层呈现;数据源=runner 级库存只读面)----
|
||||
|
||||
# ---- 奖励节点三选一(迭代 7 T5——拍板②丙:奖励屏=小构筑点;「节点奖励=功能道具正式渠道」兑现)----
|
||||
# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集)
|
||||
func _aux_unlocked_set() -> Dictionary:
|
||||
var out := {}
|
||||
if meta == null:
|
||||
return out
|
||||
for row in db.aux_active_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if meta.save.item_unlocked(iid):
|
||||
out[iid] = true
|
||||
return out
|
||||
|
||||
|
||||
|
||||
# 摇取面:活行 ∩ 工坊解锁集(拍板③甲——解锁进投放池同源口径);池 ≤3 全列(当前三件=选一即全列),
|
||||
# 池扩充后加权摇三(确定性摇取:读 seed 流外——呈现侧零 RNG 消费纪律 → 全列前 3 按注册序)。
|
||||
func _reward_node_choices() -> Array:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return []
|
||||
var out: Array = []
|
||||
for row in db.aux_active_rows():
|
||||
var iid := String(row.get("item_id", ""))
|
||||
if not _aux_unlocked.has(iid):
|
||||
continue # 未解锁不入奖励池(与掉落投放同门控)
|
||||
out.append({
|
||||
"item_id": iid,
|
||||
"display_name": String(row.get("display_name", "")),
|
||||
"glyph": String(row.get("display_name", "?")).left(1),
|
||||
})
|
||||
if out.size() >= 3:
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
# 选择兑付:入关卡域库存(来源归因 reward_node)→ 收屏 → 兑付确认回选路(零自动推进同款)
|
||||
func _on_reward_item_chosen(item_id: String) -> void:
|
||||
if runner == null or runner.aux_inventory == null:
|
||||
return
|
||||
runner.grant_aux_item(item_id, "reward_node")
|
||||
node_overlay.close_reward()
|
||||
_refresh_aux_slots()
|
||||
board_zone.toast.show_toast("获得 %s(入关卡背包)" % String(db.aux_item_row(item_id).get("display_name", item_id)), "info")
|
||||
_on_node_continue()
|
||||
|
||||
|
||||
# 槽点击(第一步):AP 类(target=none)点击即用(无目标);空间类进/出选中态(toggle 取消)。
|
||||
func _on_aux_slot_clicked(slot_handle: int) -> void:
|
||||
if settlement == null or runner == null or runner.aux_inventory == null:
|
||||
|
||||
@@ -138,8 +138,20 @@ func refresh_camp(data: Dictionary) -> void:
|
||||
var prog := int(row.get("progress", 0))
|
||||
var unlocked := bool(row.get("unlocked", false))
|
||||
card.progress_l.text = "累计通关 %d / %d 次" % [prog, th]
|
||||
card.status_l.text = "已解锁" if unlocked else "未解锁"
|
||||
card.status_l.add_theme_color_override("font_color", UiKit.GOLD if unlocked else UiKit.WARM_FAINT)
|
||||
# 迭代 7 T5——失义件(显影尘/地穴全图卷 Alpha 重定)与退役件(探宝罗盘 2026-09-29 拍板①乙)的呈现口径
|
||||
var st_text := "未解锁"
|
||||
var st_col: Color = UiKit.WARM_FAINT
|
||||
if bool(row.get("retired", false)):
|
||||
st_text = "已退役"
|
||||
st_col = UiKit.WARM_FAINT
|
||||
elif bool(row.get("deferred", false)) and unlocked:
|
||||
st_text = "已解锁 · 能力封存中"
|
||||
st_col = UiKit.GOLD
|
||||
elif unlocked:
|
||||
st_text = "已解锁"
|
||||
st_col = UiKit.GOLD
|
||||
card.status_l.text = st_text
|
||||
card.status_l.add_theme_color_override("font_color", st_col)
|
||||
|
||||
|
||||
func _rebuild_forge(data: Dictionary) -> void:
|
||||
|
||||
@@ -6,9 +6,11 @@ class_name NodeOverlay
|
||||
extends Control
|
||||
|
||||
signal continue_pressed
|
||||
signal item_chosen(item_id: String) # 迭代 7 T5——奖励节点三选一兑付(拍板②丙:奖励屏=小构筑点)
|
||||
|
||||
var _label: Label
|
||||
var _btn: Button
|
||||
var _choice_box: HBoxContainer # 道具三选一按钮区(show_reward_items 时呈现)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
@@ -27,6 +29,10 @@ func _init() -> void:
|
||||
_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(_label)
|
||||
_btn = UiKit.button("继续")
|
||||
_choice_box = HBoxContainer.new()
|
||||
_choice_box.add_theme_constant_override("separation", 12)
|
||||
_choice_box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_child(_choice_box)
|
||||
_btn.pressed.connect(func(): continue_pressed.emit())
|
||||
box.add_child(_btn)
|
||||
visible = false
|
||||
@@ -46,6 +52,12 @@ func show_rest(healed: int, hp_after: int, max_hp: int) -> void:
|
||||
visible = true
|
||||
|
||||
|
||||
# 选择完成收屏(组合根在 item_chosen 处理毕调——入库存+确认后回选路由同拍)
|
||||
func close_reward() -> void:
|
||||
_choice_box.visible = false
|
||||
_btn.visible = true
|
||||
visible = false
|
||||
|
||||
# 探针复用:以真实按钮路径触发继续
|
||||
func press_continue() -> void:
|
||||
_btn.pressed.emit()
|
||||
Reference in new issue
Block a user