Files
gplan-2/game/presentation/battle_narrator.gd
T
DKChan 416ca84aeb Merge branch 'master' into iter38
# Conflicts:
#	game/README.md
#	game/tests/验证集清单.md
2026-10-26 15:29:55 +08:00

186 lines
9.4 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
## 战斗流程叙事文案装配器:模式流转点的玩家可见文案(棋盘日志横幅/节点行/终局文案/相位指示)统一在此拼装,静态无状态。
## 文案内数值 = 载荷快照值(每关口径),非实时现值——日志断言与排障按此口径读。
extends RefCounted
# 节点进入横幅(棋盘日志——battle/boss 用;节点编号 = 行进器编号空间读面;行色 = UiKit.HEX_* 语义档单点)
static func node_banner(runner: LevelRunner, 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]
# 商店房行(到达即开店;购买/离开归玩家意图)
static func shop_line(gp: int) -> String:
return "[color=#%s]商店房:GP %d——购买即时生效,离开后选下一跳[/color]" % [UiKit.HEX_LOG_GAIN, gp]
# 选路口行(零自动推进停点——战斗胜利/兑付确认后出现)
static func route_line(candidates: Array, runner: LevelRunner) -> 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 != "":
# 判定文案两处措辞统一:相位行随结算判定行同词(战斗胜利/战斗失败,
# 与 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 表行名;缺省遗迹守卫)。
# 行集多行时取**首行** display_name(行序即契约的呈现面后果,
# Boss 行集混编护卫时 Boss 名显示可能为护卫名,知情代价登记 敌人系统 v0.15 / 出口报告;
# 每坐标恰一行时无此歧义)
static func boss_name(runner: Variant, db: ConfigDb) -> String:
if runner != null:
var node: Dictionary[Variant, Variant] = runner.current_node()
if String(node.get("type", "")) == "boss":
var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1)), "boss")
if not rowset.is_empty():
return String(rowset[0].get("display_name", ""))
return "遗迹守卫"
# ---- 提升三选一文案族 ----
# 三选一停点头部行(回血反馈 + 选择引导;钥石增量行不上本面——单一展示面归战斗总结结算框,
# 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 "GP 袋%s\nGP +%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", ""))}).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
# 特性卡片文案(第二行=效果摘要——须可读「不知道件是
# 干嘛的」。文案表值化:摘要=数据行 desc 字段,零硬编码映射。
# 「件名 · 流派」族名字样不上卡片(内部分类禁上玩家可见面——29 行
# 表无 archetype,fallback 兜底词「流派」会实显僵尸文案),卡片=件名+效果摘要两行)
static func feat_card_text(row: Dictionary) -> String:
if row.is_empty() or String(row.get("feat_id", "")) == "":
return "专长\n???"
var summary := String(row.get("desc", ""))
if summary != "":
return "%s\n%s" % [String(row.get("display_name", row.get("feat_id", ""))), summary]
return 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))]
# 奖励节点三选一头部行(与关卡完成三选一共享同库;渠道白名单分流——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))]
# 钥石入账明细文案(钥石全源 = Boss 逐杀掉落,无「大奖励」行;
# name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入)
static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String:
var parts: Array[Variant] = []
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[Variant] = 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))