146 lines
8.2 KiB
GDScript
146 lines
8.2 KiB
GDScript
# tests/unit/presentation/battle_narrator_test.gd — 战斗叙述直接单测
|
||
# 断言面 = 纯函数文案:休整/商店/终局三类叙述把数值带进去(玩家读数面由这里出)、
|
||
# 胜负两态文案互异、未知输入有兜底不吐空串。期望值由注入内容数出来。
|
||
# 运行:godot --headless --path game --script res://tests/unit/presentation/battle_narrator_test.gd
|
||
extends SceneTree
|
||
|
||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||
const NarratorScript := preload("res://presentation/battle_narrator.gd")
|
||
|
||
|
||
# 叙述寻址的 runner 替身:只提供 boss_name 实际读的两项(当前节点、关卡行)
|
||
class _BossRunner:
|
||
var level_row: Dictionary = {"level_index": 1}
|
||
var node: Dictionary = {}
|
||
|
||
func current_node() -> Dictionary:
|
||
return node
|
||
|
||
|
||
func _init() -> void:
|
||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (战斗叙述宿主——装载组)
|
||
_test_numeric_lines_carry_values()
|
||
_test_end_text_differs_by_result()
|
||
_test_phase_text_nonempty()
|
||
_test_boss_name_uses_boss_rowset()
|
||
_test_level_clear_wording()
|
||
_test_snapshot_effects_per_unit()
|
||
quit(Kit.finish("battle_narrator_test"))
|
||
|
||
|
||
func _test_numeric_lines_carry_values() -> void:
|
||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (结算行数值带入组)
|
||
var rest: String = NarratorScript.rest_line(12, 30, 40)
|
||
Kit.check("休整叙述带入回复量与当前血量(实际「%s」)" % rest,
|
||
rest.contains("12") and rest.contains("30") and rest.contains("40"))
|
||
var shop: String = NarratorScript.shop_line(77)
|
||
Kit.check("商店叙述带入 GP 数(实际「%s」)" % shop, shop.contains("77"))
|
||
Kit.check("奖励行与休整行互异(不是同一句套所有场景)",
|
||
NarratorScript.reward_line() != rest)
|
||
|
||
|
||
func _test_end_text_differs_by_result() -> void:
|
||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (胜负互异文案组)
|
||
# 关卡收束(level_end=true)恒为「关卡完成!」——单场胜负文案只在非收束态出现
|
||
var level: String = NarratorScript.battle_end_text("lose", true)
|
||
Kit.check("关卡收束态恒为「关卡完成!」(实际「%s」)" % level, level == "关卡完成!")
|
||
var win: String = NarratorScript.battle_end_text("victory", false)
|
||
var lose: String = NarratorScript.battle_end_text("lose", false)
|
||
Kit.check("单场胜负文案互异(胜「%s」/负「%s」)" % [win, lose], win != lose)
|
||
Kit.check("胜利文案非空(实际「%s」)" % win, win != "")
|
||
var unknown: String = NarratorScript.battle_end_text("something_else", false)
|
||
Kit.check("未知结果有兜底文案不吐空串(实际「%s」)" % unknown, unknown != "")
|
||
|
||
|
||
func _test_phase_text_nonempty() -> void:
|
||
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (阶段行非空组)
|
||
var t: String = NarratorScript.phase_text(1, "", false, false, "")
|
||
Kit.check("相位叙述非空(实际「%s」)" % t, t != "")
|
||
var boss: String = NarratorScript.phase_text(2, "", false, true, "灰矮人石君王")
|
||
Kit.check("Boss 在场时叙述带入 Boss 名(实际「%s」)" % boss, boss.contains("灰矮人石君王"))
|
||
|
||
|
||
# 叙述寻址按行类取 Boss 行集(S 清单 S15 换代):Boss 节点名必须来自 (关卡, boss, 带) 行集,
|
||
# 不能落到同深度的战斗行集——期望名由表行集首行读出,不写死字面量。
|
||
func _test_boss_name_uses_boss_rowset() -> void:
|
||
# oracle: design/gdd/systems/战斗循环/敌人系统.md:295 (Boss 名读行集组)
|
||
var db = load("res://core/config_db.gd").new()
|
||
var loaded: bool = db.load_tables("res://data/tables", false)
|
||
Kit.check_quiet(loaded, "叙述寻址用例:数值表加载成功")
|
||
if not loaded:
|
||
return
|
||
var boss_row := {}
|
||
for row in db.combat.enemy.rows:
|
||
if int(row.level_index) == 1 and String(row.get("row_kind", "")) == "boss":
|
||
boss_row = row
|
||
break
|
||
Kit.check_quiet(not boss_row.is_empty(), "叙述寻址用例:第一关有 Boss 行")
|
||
if boss_row.is_empty():
|
||
return
|
||
var boss_depth: int = int(boss_row.depth_band)
|
||
var boss_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "boss")
|
||
var battle_rows: Array = db.combat.enemy.resolve_rowset(1, boss_depth, "battle")
|
||
var boss_expect := String(boss_rows[0].get("display_name", ""))
|
||
var battle_name := String(battle_rows[0].get("display_name", ""))
|
||
var runner := _BossRunner.new()
|
||
runner.node = {"node_depth": boss_depth, "type": "boss"}
|
||
var got: String = NarratorScript.boss_name(runner, db)
|
||
Kit.check("Boss 名取自 Boss 行集首行(期望「%s」实际「%s」)〔fv24〕" % [boss_expect, got], got == boss_expect)
|
||
Kit.check("Boss 名与同深度战斗行集名不同(期望「%s」≠「%s」——行类路由可判别)〔fv24〕" % [boss_expect, battle_name], boss_expect != battle_name)
|
||
|
||
|
||
# 关卡完成停点文案族措辞钉子(口径换代批):回血行/满血合法态/渠道句/摘要关数行——
|
||
# 旧措辞退役后玩家可见面由本组断言钉在新措辞上(撤销证明:回退 battle_narrator 对应行→断言红→复原复绿)
|
||
func _test_level_clear_wording() -> void:
|
||
# oracle: design/ux/level-clear.md:11 (关卡结算屏文案组)
|
||
var healed: String = NarratorScript.curio_pick_header({"level_index": 1, "heal_gained": 5, "hp_before": 10, "hp_after": 15})
|
||
Kit.check("关卡完成回血行带入回血量与生命区间(实际「%s」)〔口径换代 V1〕" % healed,
|
||
healed.contains("关卡完成回血 +5(生命 10 → 15)"))
|
||
var full_hp: String = NarratorScript.curio_pick_header({"level_index": 1, "heal_gained": 0, "hp_before": 12, "hp_after": 12})
|
||
Kit.check("满血零回血合法态文案(实际「%s」)〔口径换代 V1〕" % full_hp,
|
||
full_hp.contains("生命已满——关卡完成回血 0(合法态)"))
|
||
var node_header: String = NarratorScript.node_curio_header()
|
||
Kit.check("奖励节点头部行渠道句点明关卡完成三选一(实际「%s」)〔口径换代 V2〕" % node_header,
|
||
node_header.contains("与关卡完成专长三选一同库分流"))
|
||
var summary: String = NarratorScript.adventure_summary_text(
|
||
{"cleared": false, "result": "failed", "levels_cleared": 1, "stopped_at": 2, "keystone_total": 4, "master_seed": 42})
|
||
Kit.check("冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" % summary,
|
||
summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关"))
|
||
|
||
|
||
# ---- 呈现各读各的(增益状态 v1.5——敌方状态个体化批)----
|
||
# 真实入口=呈现状态快照函数 presentation_snapshot:A 有削弱 B 无 → 两卡投影各现各的(零共享引用)
|
||
|
||
func _test_snapshot_effects_per_unit() -> void:
|
||
# oracle: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
|
||
var db = load("res://core/config_db.gd").new()
|
||
Kit.check("V8 前置:表加载零 error", db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0)
|
||
var rng = load("res://core/rng_service.gd").new()
|
||
rng.set_master_seed(20261103)
|
||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||
# depth 3 = 混编行集(熊地精 1 + 地精 1–2,最少 2 只)——两卡对照面
|
||
var open_ok: bool = settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(db.combat.hero.max_hp)})
|
||
Kit.check("V8 前置:开局(L1 depth3 双敌行集)", open_ok)
|
||
if not open_ok:
|
||
return
|
||
var enemies: Array = settlement._enemy_system.all_enemies()
|
||
var dual_ok: bool = enemies.size() >= 2
|
||
Kit.check("V8 前置:≥2 敌(两卡对照面)", dual_ok)
|
||
if not dual_ok:
|
||
return
|
||
var sc = load("res://core/status_container.gd")
|
||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||
Kit.check("V8 前置:A 敌受理削弱(B 敌不授)", settlement._status_effects.apply_effect("weaken", sc.owner_key_for(enemies[0]), src))
|
||
var snap: Dictionary = settlement.presentation_snapshot()
|
||
var e1_effects: Array = snap.enemies[0].effects
|
||
var e2_effects: Array = snap.enemies[1].effects
|
||
var e1_has := false
|
||
for e in e1_effects:
|
||
if String(e.effect_id) == "weaken":
|
||
e1_has = true
|
||
var e2_has := false
|
||
for e in e2_effects:
|
||
if String(e.effect_id) == "weaken":
|
||
e2_has = true
|
||
Kit.check("A 有削弱 B 无 → 两卡投影各现各的(A 卡现削弱、B 卡无同款)〔个体化批 V8〕", e1_has and not e2_has)
|