- 新增 design/gdd/systems/冒险循环/特性.md(概念唯一定义文档) 定义=一次冒险内角色自身得到的提升(不是持有物);作用范围、生命周期、 获取途径(战场奖励/商店·训练)、关联与边界五要素齐全;数值一律指路表不入正文 - 02-系统清单:冒险循环补行 + 命名对照表补 ADVENTURE_FEATURE - registry entities.yaml:新增 feature 概念条目(owner/referenced_by)+ adventure_table 字段名同步 - 数据层标识符 112 处换名(值零变更):BONUS_IDS→FEATURE_KINDS、bonus_token_count→feature_quota、 bonus_value→feature_value、token_*/bonus_*_flat→feature_quota_*/feature_value_* - 称谓统一 94 处:旧称「流派件/数值件/本关强化」→ 特性 + 形态修饰语(规则改写型/属性加成型) - 两张 schema 顶部加概念指路注释,代码可直接查到定义文档 - 06-待办清单:BD 分层条目现状改写 + 新增「命名统一后续」条目(relic 词族与类名对齐待实施) 未动:类名与文件名(Godot .uid/.tscn 引用需跑测试自证)、断点存档键(兼容)
242 lines
12 KiB
GDScript
242 lines
12 KiB
GDScript
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||
# 提升三选一与替换子交互控制器(迭代 13 T4/D4——二期家族① 自 main.gd 迁出;规范 1.1 单一职责):
|
||
# 三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ 同调位替换子态
|
||
#(presented→chosen)+ TP0 令牌池卡片文案装配("kind|tier|relic_id" 复合键)+ 工坊显示名投影。
|
||
# 装配纪律(ADR-0001,仿 aux_interact_controller 先例):信号连接不在此发生——bonus_overlay
|
||
# 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/
|
||
# 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一
|
||
# 经 adventure.pick_bonus/apply_node_bonus/resolve_bonus_replace(core 侧单口,本类零直写)。
|
||
extends RefCounted
|
||
|
||
const Narrator := preload("res://presentation/battle_narrator.gd")
|
||
|
||
var db
|
||
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证)
|
||
var bonus_overlay: BonusPickOverlay
|
||
var end_overlay: BattleEndOverlay
|
||
var board_view: BoardView
|
||
var toast: ToastStack
|
||
var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面)
|
||
var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替)
|
||
var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进)
|
||
var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda)
|
||
var _open_modal: Callable # 模态栈单口(组合根)
|
||
var _open_modal_after_playback: Callable # 演播后排窗单口(T17——组合根)
|
||
var _close_modal: Callable
|
||
var _refresh_presentations: Callable
|
||
var _fatal_screen: Callable
|
||
var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根)
|
||
|
||
var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
|
||
var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
|
||
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
|
||
var _last_replace_incoming := {} # 同上——incoming 记录
|
||
|
||
|
||
func _init(db_ref, meta_get: Callable, bonus_overlay_ref: BonusPickOverlay, end_overlay_ref: BattleEndOverlay,
|
||
board_view_ref: BoardView, toast_ref: ToastStack, save_get: Callable,
|
||
adventure_get: Callable, runner_get: Callable, adventure_mode_get: Callable,
|
||
open_modal: Callable, open_modal_after_playback: Callable, close_modal: Callable,
|
||
refresh_presentations: Callable, fatal_screen: Callable, gp_summary: Callable) -> void:
|
||
db = db_ref
|
||
_meta_get = meta_get
|
||
bonus_overlay = bonus_overlay_ref
|
||
end_overlay = end_overlay_ref
|
||
board_view = board_view_ref
|
||
toast = toast_ref
|
||
_save_get = save_get
|
||
_adventure_get = adventure_get
|
||
_runner_get = runner_get
|
||
_adventure_mode_get = adventure_mode_get
|
||
_open_modal = open_modal
|
||
_open_modal_after_playback = open_modal_after_playback
|
||
_close_modal = close_modal
|
||
_refresh_presentations = refresh_presentations
|
||
_fatal_screen = fatal_screen
|
||
_gp_summary = gp_summary
|
||
|
||
|
||
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
|
||
func has_pending() -> bool:
|
||
return not _pending_bonus.is_empty()
|
||
|
||
|
||
# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)
|
||
func resume_if_pending() -> bool:
|
||
if _pending_bonus.is_empty():
|
||
return false
|
||
show_pick()
|
||
return true
|
||
|
||
|
||
# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
|
||
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
|
||
func on_presented(payload: Dictionary) -> void:
|
||
if int(payload.get("aux_cleared", 0)) > 0:
|
||
toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
|
||
_pending_bonus = payload.duplicate(true)
|
||
_bonus_context = "level_clear"
|
||
if bool(payload.get("replace_pending", false)):
|
||
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
|
||
show_pick()
|
||
return
|
||
end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
|
||
board_view.get_global_rect(),
|
||
"%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)])
|
||
_open_modal_after_playback.call(end_overlay)
|
||
_refresh_presentations.call()
|
||
|
||
|
||
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
|
||
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面,
|
||
# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。
|
||
func present_node_bonus() -> void:
|
||
var adventure = _adventure_get.call()
|
||
_bonus_context = "reward_node"
|
||
var node_draw: Dictionary = adventure.node_bonus_candidates()
|
||
var node_cands: Array = node_draw.get("candidates", []) as Array
|
||
_open_modal.call(bonus_overlay)
|
||
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)
|
||
bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂
|
||
|
||
|
||
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
|
||
func show_pick() -> void:
|
||
var payload: Dictionary = _pending_bonus
|
||
_open_modal.call(bonus_overlay)
|
||
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
|
||
var adventure = _adventure_get.call()
|
||
bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂
|
||
_refresh_presentations.call()
|
||
|
||
|
||
func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array:
|
||
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind
|
||
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed)
|
||
var out: Array = []
|
||
var aux_i := 0
|
||
for tok in candidates:
|
||
var kind := String((tok as Dictionary).get("kind", ""))
|
||
var tier := String((tok as Dictionary).get("tier", ""))
|
||
var rid := String((tok as Dictionary).get("relic_id", ""))
|
||
var cid := "%s|%s|%s" % [kind, tier, rid]
|
||
var text := ""
|
||
if kind == "relic":
|
||
text = Narrator.relic_card_text(db.relic_row(rid))
|
||
elif kind == "aux_item":
|
||
var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else ""
|
||
aux_i += 1
|
||
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.feature_value(kind, tier)), _aux_card_line(iid))
|
||
else:
|
||
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.feature_value(kind, tier)))
|
||
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)]
|
||
|
||
|
||
# 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源;通关停点经重发 bonus_choice_presented 自然重建,
|
||
# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
|
||
func on_refresh() -> void:
|
||
var adventure = _adventure_get.call()
|
||
if adventure == null:
|
||
return
|
||
if not adventure.refresh_bonus():
|
||
return
|
||
if _bonus_context == "reward_node":
|
||
var st: Dictionary = adventure.node_bonus_pending_state()
|
||
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
|
||
bonus_overlay.set_refresh_info(adventure.attune_refresh_left())
|
||
|
||
|
||
func on_pick_confirmed(item_id: String) -> void:
|
||
var adventure = _adventure_get.call()
|
||
if adventure == null:
|
||
return
|
||
# TP0 令牌池——卡片 id "kind|tier|relic_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段)
|
||
var parts := item_id.split("|")
|
||
var kind := parts[0]
|
||
var tier := parts[1] if parts.size() > 1 else ""
|
||
var rid := parts[2] if parts.size() > 2 else ""
|
||
var ok: bool = adventure.apply_node_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid)
|
||
if not ok:
|
||
push_error("[bonus_interact] 三选一兑付被拒——见输出日志")
|
||
|
||
|
||
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.call(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
|
||
_open_replace(items, incoming)
|
||
|
||
|
||
func _open_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(), Attunement.slots(db, _save_get.call())) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
|
||
|
||
|
||
func on_replace_chosen(discard: int) -> void:
|
||
var adventure = _adventure_get.call()
|
||
if adventure == null or not adventure.resolve_bonus_replace(discard):
|
||
push_error("[bonus_interact] 同调位替换受理被拒——见输出日志")
|
||
|
||
|
||
# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续)
|
||
func on_resolved(result: Dictionary) -> void:
|
||
_pending_bonus = {}
|
||
bonus_overlay.show_result(Narrator.bonus_result_text(result))
|
||
_refresh_presentations.call()
|
||
|
||
|
||
# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
|
||
func on_continue() -> void:
|
||
_close_modal.call(bonus_overlay)
|
||
bonus_overlay.close_pick()
|
||
var adventure = _adventure_get.call()
|
||
if _bonus_context == "reward_node":
|
||
var runner = _runner_get.call()
|
||
if runner != null:
|
||
runner.continue_after_node()
|
||
return
|
||
if bool(_adventure_mode_get.call()) and adventure != null:
|
||
if not adventure.continue_to_next_level():
|
||
_fatal_screen.call("关卡推进失败——见输出日志")
|
||
|
||
|
||
# 工坊 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator)
|
||
func item_name_lookup() -> Dictionary:
|
||
var lookup := {}
|
||
var meta = _meta_get.call()
|
||
if meta != null:
|
||
for row in db.meta_workshop_items():
|
||
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||
return lookup
|