Files
gplan-2/game/presentation/battle_narrator.gd
T
DKChan e28a6cb3c4 专长迁移完成:27 行 triggers 订阅形态全量生效+旧 23 挂点枚举消亡+消费点拆除+断言面随迁(T3——内容数据驱动扩展批)
接手前任中断的 T3 半成品(脏文件三件:battle_orchestrator/effects engine/feats.tres),方向核实无误后续做收口;
承接修正四处随本提交留痕:①perfect_defense 的 when 用了 fact 封闭 9 项外的 flags_hit_all(装载校验必红)——
改 flags_count gte 1 保守判定,全中闸=flag_settle 发射点施加面承载(载荷字段仅供观测);②has 算子语义与行
不匹配(数组包含对 String 恒假)——落非空在场判定(value 缺省)+数组包含(value 给定),查询面落法=代拍板
白名单;③前任漏写 _harmful_adjacent 助手(flag_hit 发射点调用必炸);④counter_add set 键语义 bool() 吞 0
(清零行 set:0 被当 false 走加零通道)——改 has(set) 置值模式,四处施加点同修。

本提交主干(专长族全量迁移——任务书 §六 T3):
- damage_controller:feat_flags 旗面整体退役,五事件施加面接线——attack_commit 命中随单据 context 携带
  (发射点=开单入口,任务书 §3.1 定稿位;主角攻击单恒段内首单,开单现场=结账现场),rider/改写/加值/
  段数/状态五族按迁移前算序施加(重锤两拍/专注/spike 恒全额/猎弱移档/魔网湛蓝猎手克制直调/整备每层
  +1/厚积满层倍乘/奥术+猎巢+执拗 rider 合计后档位暴击);armor_roll 免掷恒折扣、clamp_check 双面
  (hero+board_typed)delta 窗口、kill 溢出弹射(landed×value%)、damage_taken shield 加法族合计。
  _feat_clamp_bonus/13 处旗消费点拆除。
- battle_orchestrator:_combat_segment 换代(dig_streak/charge_layers 注伤害控制器运行态;连珠矢段数
  施加位紧随主角攻击单开立=迁移前开单位,flush 兑现序逐位一致);round_start 发射位前移至行动经济轮
  切换前(树规则 grant_ap queue 当轮入队当轮兑现——先手压制首轮语义锚);深挖快照滚动拆除(计数器
  reset_at 声明承载);熔火/连击旧计数变量退役。
- action_economy:feat_ap_carry/feat_dig_extra/deep_paid_count 旧注入旗拆除;dig_paid 施加面落地
  (counter_spend 对价:AP 富余付全价积计数、不足 fallback 只付基础 1——2026-10-04 降级口径逐位保留)。
- feat_table schema:HOOKS 23 挂点枚举/MINE_HOOKS/EFFECT_KEYS/hook+effects 双形态校验全删——triggers
  订阅形态唯一合法行形(行键白名单收口 feat_id/display_name/rarity/archetype/triggers/desc/note)。
- feats_engine:hooks_for 挂点投影退役(订阅装载序=feats 表序,由效果引擎 rebuild 承载)。
- battle_narrator:专长卡片文案表值化(summaries 硬编码映射拆除,读行 desc——27 件措辞逐字迁移)。
- smoke 断言面随迁(任务书 v1.1 补列项):构造表 5 行换代 triggers 形态;挂点投影断言换代订阅装载面;
  ④-⑧ 求值断言(_feat_flags_for 直呼+旗读数)全量改写为 rebuild+dispatch 命中面(27 件逐事件覆盖);
  深挖钻头付费开掘回归闸改 counter_spend 计数器面;特性卡 desc 直读断言新增。

映射表对拍注记(行为等价优先,.tres 尾注同步留痕):猎巢人/矿工的执拗映射表标 modify_ticket/attack_bonus,
现行为必中附加通道(hero_rider 后置于乘区与下限)——落 add_ticket_rider(per 取数载荷);整备军备现行为
双效果(轮末转蓄力+每层首段攻 +1)——两行订阅;深挖钻头 per-round 计数器对(paid/given)复刻旧 v2
跨轮不对称语义(given 不随轮重置)。

验证:冒烟 800 项 0 失败(三闸过)· 单元 17 套全绿 · S1 魔法流对拍预检=MC formed/off 两态与 T0 pre 锚
(83000 种子段)同种子逐位一致(2×5 局,三档只测不判)——14 件迁移行实战斗承载零分岔。

设计权威:design/research/研究-内容数据驱动扩展架构-2026-10-06.md(案乙)。
任务书:design/research/任务书-内容数据驱动扩展批-2026-10-06.md §六 T3、§五映射表、§七契约 2/4。
2026-10-06 23:20:05 +08:00

186 lines
9.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1/1.3 文案拼装独立成装配器)
# 战斗流程叙事文案装配器:组合根在模式流转点的玩家可见文案(棋盘日志横幅 / 节点行 / 终局文案 /
# 相位指示)在此统一拼装——main.gd 头注"本类不拼呈现字符串"由本类兑现。静态无状态。
# 数值口径注记:胜利文案携带的 HP/道具数 = 关卡载荷快照值(每关口径)。
extends RefCounted
# 节点进入横幅(棋盘日志——battle/boss 用;节点编号 = 行进器编号空间读面;行色 = UiKit.HEX_* 语义档单点)
static func node_banner(runner, node: Dictionary, t: String) -> String:
return "[color=#%s]── 节点 %d/%d · %s(编号 %d)──[/color]" % [UiKit.HEX_LOG_GAIN,
runner.node_index + 1, runner.nodes().size(),
"Boss 房" if t == "boss" else "战斗", runner.node_id_of(node)]
static func reward_line() -> String:
return "[color=#%s]关卡奖励点(节点内容待定——直通)[/color]" % UiKit.HEX_ROUTE_NOTE
static func rest_line(healed: int, hp: int, max_hp: int) -> String:
return "[color=#%s]恢复点:回复 %d HP(→ %d/%d)[/color]" % [UiKit.HEX_LOG_HEAL, healed, hp, max_hp]
# 商店房行(GP+商店房实施批——到达即开店;购买/离开归玩家意图)
static func shop_line(gp: int) -> String:
return "[color=#%s]商店房:金币 %d——购买即时生效,离开后选下一跳[/color]" % [UiKit.HEX_LOG_GAIN, gp]
# 选路口行(零自动推进停点——战斗胜利/兑付确认后出现)
static func route_line(candidates: Array, runner) -> String:
var names: Array[String] = []
for cand in candidates:
names.append("节点 %d" % runner.node_id_of(cand))
return "[color=#%s]── 岔路口:可选下一跳 %s ──[/color]" % [UiKit.HEX_LOG_GAIN, " / ".join(names)]
# 关卡开场行(种子/加成口径注记——面板加成是否生效由调用方传入)
static func level_open_line(level_display: String, seed_val: int, atk: int, max_hp: int, boosted: bool) -> String:
return "[color=#%s]%s · 种子 %d · 攻击 %d / 生命 %d%s[/color]" % [UiKit.HEX_SEED_NOTE,
level_display, seed_val, atk, max_hp, "(含面板加成)" if boosted else ""]
# 调试开场行
static func debug_open_line(seed_val: int, level: int, level_mode: bool) -> String:
return "[color=#%s]种子 %d · 遭遇 %d 层%s[/color]" % [UiKit.HEX_SEED_NOTE,
seed_val, level, "(关卡模式 · HP 延续)" if level_mode else ""]
# 调试战斗终局文案(level_end = 关内两层模式的关卡末)
static func battle_end_text(result: String, level_end: bool) -> String:
if level_end:
return "关卡通关!"
if result == "victory":
return "战斗胜利!敌人全灭。"
return "战斗失败……探险者倒下了。"
# 相位指示文本(B 区轮次·阶段行)
static func phase_text(round_index: int, battle_result: String, waiting: bool, boss_active: bool, boss_name: String) -> String:
var boss_tag := " · Boss:%s" % boss_name if boss_active else ""
if battle_result != "":
# 打磨包 2026-09-29——判定文案两处措辞统一:相位行随结算判定行同词(战斗胜利/战斗失败,
# 与 battle_end_text 同源措辞),「战斗结束」退役
return "第 %d 轮 · %s%s" % [round_index, "战斗胜利" if battle_result == "victory" else "战斗失败", boss_tag]
if waiting:
return "第 %d 轮 · 结算中%s" % [round_index, boss_tag]
return "第 %d 轮 · 挖掘中%s" % [round_index, boss_tag]
# Boss 名解析(runner Boss 节点 → enemy 表行名;缺省遗迹守卫)
static func boss_name(runner, db) -> String:
if runner != null:
var node: Dictionary = runner.current_node()
if String(node.get("type", "")) == "boss":
var row: Dictionary = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)))
return String(row.get("display_name", ""))
return "遗迹守卫"
# ---- 提升三选一文案族(迭代 10 奖励重构批——big_reward_text 随三件套退役)----
# 三选一停点头部行(回血反馈 + 选择引导;钥石增量行不上本面——单一展示面归战斗总结结算框,
# level-clear.md v2 禁入清单口径)
static func curio_pick_header(payload: Dictionary) -> String:
var lines: Array[String] = ["第 %d 层通过!" % int(payload.get("level_index", 0))]
var heal := int(payload.get("heal_gained", 0))
if heal > 0:
lines.append("通关回血 +%d(生命 %d → %d)" % [heal, int(payload.get("hp_before", 0)), int(payload.get("hp_after", 0))])
else:
lines.append("生命已满——通关回血 0(合法态)")
lines.append("选择一件提升带走(另两件丢弃不可反悔):")
return "\n".join(lines)
# 三选一选项卡文案(件名 + 档位 + 效果读数;值 = 冒险表行值,组合根喂入;TP0 令牌池——档位词
# 对齐 v2.2 追认 b 口径:小/大前缀(小GP/大GP 族)、恢复/AP 不带档位词;道具件掷定前置时
# treasure_line = 具体道具「名 · 效果摘要」行——池种退役后恒空走通用分支,机械惰性保留)
static func bonus_card_text(id: String, tier: String, value: int, treasure_line: String = "") -> String:
var ts := ""
if tier == "common":
ts = " · 小"
elif tier == "rare":
ts = " · 大"
match id:
"attack":
return "攻击力提升%s\n攻击 +%d" % [ts, value]
"max_hp":
return "生命上限提升%s\n生命上限 +%d" % [ts, value]
"armor":
return "护甲等级提升%s\n护甲 +%d" % [ts, value]
"gp":
return "金币袋%s\n金币 +%d" % [ts, value]
"heal":
return "恢复药剂\n生命 +%d(当即回复)" % value
"treasure_item":
if treasure_line != "":
return "秘宝\n%s" % treasure_line
return "秘宝\n随机获得一件"
"ap":
return "行动点提升\n每战首轮 AP +%d" % value
return id
# 三选一兑付反馈行(选中件效果 + 丢弃提示;tier 缺省普通档——旧载荷防御口径)
static func bonus_result_text(result: Dictionary) -> String:
var name := ""
if String(result.get("picked", "")) == "relic" and String(result.get("feat_id", "")) != "":
name = feat_card_text({"feat_id": String(result.get("feat_id", "")), "display_name": String(result.get("feat_name", "")), "archetype": String(result.get("feat_archetype", ""))}).replace("\n", " · ")
else:
name = bonus_card_text(String(result.get("picked", "")), String(result.get("tier", "")), int(result.get("value", 0))).replace("\n", " · ")
return "已选择:%s\n其余选项已丢弃(不可反悔)" % name
# 特性卡片文案(迭代 12 T1;2026-10-04 呈现补强——第三行=效果摘要,用户反馈「不知道件是
# 干嘛的」缺口承载。内容数据驱动扩展批 T3 文案表值化:摘要=数据行 desc 字段,零硬编码映射——
# 新件写行即有名有姓,措辞与旧 summaries 表逐字一致)
static func feat_card_text(row: Dictionary) -> String:
if row.is_empty() or String(row.get("feat_id", "")) == "":
return "特性\n???"
var arch_names := {"magic": "魔法", "pierce": "穿刺", "normal": "普攻", "dig": "开格",
"flag": "标旗", "charge": "蓄力", "devour": "吞噬"}
var arch := String(arch_names.get(String(row.get("archetype", "")), "流派"))
var summary := String(row.get("desc", ""))
if summary != "":
return "%s · %s\n%s" % [String(row.get("display_name", row.get("feat_id", ""))), arch, summary]
return "特性 · %s\n%s" % [arch, String(row.get("display_name", row.get("feat_id", "")))]
# Boss 胜利战斗总结结算行(settlement-summary v1.1——钥石增量行;组合根拼 GP 行后追加)
static func bonus_keystone_line(payload: Dictionary) -> String:
return "钥石 +%d · 累计 %d" % [int(payload.get("keystone_gained", 0)), int(payload.get("keystone_total", 0))]
# 奖励节点三选一头部行(迭代 10 追加批——与通关三选一共享同池;node-reward.md v2)
static func node_curio_header() -> String:
return "关卡奖励点\n从提升池抽三选一(与通关三选一同池同权重同上限):\n选择一件带走:"
# 冒险结算屏文案(摘要五件事:打穿没有 / 通关几关 / 止步第几关 / 钥石合计 / 主种子)
static func adventure_summary_text(summary: Dictionary) -> String:
var verdict := "冒险失败 · 未打穿"
if bool(summary.get("cleared", false)):
verdict = "冒险完成 · 已打穿"
elif String(summary.get("result", "")) == "abandoned":
verdict = "冒险放弃 · 未打穿"
return "%s\n通关 %d 关 · 止步第 %d 层 · 钥石合计 %d · 主种子 %d" % [
verdict, int(summary.get("levels_cleared", 0)), int(summary.get("stopped_at", 0)),
int(summary.get("keystone_total", 0)), int(summary.get("master_seed", 0))]
# 钥石入账明细文案(元进度实现批;迭代 10 改口径——钥石全源 = Boss 逐杀掉落,「大奖励」行退役;
# name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String:
var parts: Array = []
for d in detail.get("boss_drops", []):
parts.append("第 %d 关 Boss %s +%d" % [int(d.get("level", 0)),
"首杀" if bool(d.get("first", false)) else "重复杀", int(d.get("amount", 0))])
var line := "钥石入账:" + (" · ".join(parts) if not parts.is_empty() else "本局无掉落")
var unlocks: Array = detail.get("new_unlocks", [])
if not unlocks.is_empty():
var names: Array[String] = []
for iid in unlocks:
names.append(String(name_lookup.get(String(iid), String(iid))))
line += "\n工坊新解锁:" + "、".join(names)
return line + "\n本局入账合计 +%d 钥石" % int(detail.get("total", 0))