137 lines
6.0 KiB
GDScript
137 lines
6.0 KiB
GDScript
extends SceneTree
|
|
|
|
const BUFF_ID := &"time_anchor_attack_buff"
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
await process_frame
|
|
var bus: Node = load("res://autoload/event_bus.gd").new()
|
|
bus.name = "EventBus"
|
|
root.add_child(bus)
|
|
|
|
var actor := Node.new()
|
|
actor.name = "TestActor"
|
|
var container: Node = load("res://scenes/components/effect_container.gd").new()
|
|
container.name = "EffectContainer"
|
|
actor.add_child(container)
|
|
var buff_component: Node = load("res://scenes/components/attack_buff_component.gd").new()
|
|
buff_component.name = "AttackBuffComponent"
|
|
actor.add_child(buff_component)
|
|
root.add_child(actor)
|
|
await process_frame
|
|
|
|
var broadcasts: Array = []
|
|
bus.connect("attack_buff_changed", func(stacks: int, max_stacks: int) -> void:
|
|
broadcasts.append([stacks, max_stacks])
|
|
)
|
|
|
|
var stat_resolver: Script = load("res://scripts/resolvers/stat_resolver.gd")
|
|
|
|
var health: Node = load("res://scenes/components/health_component.gd").new()
|
|
health.name = "HealthComponent"
|
|
health.set("maximum", 1000)
|
|
health.set("current", 400)
|
|
actor.add_child(health)
|
|
await process_frame
|
|
|
|
# --- Perfect/Good held anchors add stacks; damage formula is 1 + 0.50 x stacks. ---
|
|
for _index: int in range(3):
|
|
bus.emit_signal("time_anchor_resolved", {}, true, {"label": "perfect"})
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), 3, "three held anchors should give three stacks")
|
|
var damage := float(stat_resolver.call("resolve_damage", 100.0, null, {"label": "perfect"}, container, null))
|
|
_expect_float(damage, 250.0, "damage should scale by 1 + 0.50 x stacks")
|
|
if not broadcasts.is_empty():
|
|
_expect_int(int(broadcasts[broadcasts.size() - 1][1]), 7, "broadcast should carry max stacks 7")
|
|
|
|
# --- Broken anchors do not add stacks. ---
|
|
bus.emit_signal("time_anchor_resolved", {}, false, {})
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), 3, "broken anchors must not add stacks")
|
|
|
|
# --- Bad anchors hold/heal but do not add attack-buff stacks. ---
|
|
bus.emit_signal("time_anchor_resolved", {"beat": 15}, true, {"label": "bad"})
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), 3, "bad held anchors must not add attack buff")
|
|
|
|
# --- Cap at 7; extra Perfect/Good holds keep the cap. ---
|
|
for _index: int in range(30):
|
|
bus.emit_signal("time_anchor_resolved", {}, true, {"label": "good"})
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), 7, "stacks should cap at 7")
|
|
damage = float(stat_resolver.call("resolve_damage", 100.0, null, {"label": "perfect"}, container, null))
|
|
_expect_float(damage, 450.0, "capped stacks should give +350% damage")
|
|
|
|
# --- Each actual phase switch subtracts two stacks, bottoming at zero. ---
|
|
var expected_reductions := [5, 3, 1, 0]
|
|
for expected: int in expected_reductions:
|
|
bus.emit_signal("time_phase_changed", &"past", &"future", &"time_anchor_broken")
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), expected, "phase switch should subtract stacks to %d" % expected)
|
|
var active_ids: Array = container.call("active_effect_ids")
|
|
_expect_bool(active_ids.has(BUFF_ID), false, "zero stacks should remove the buff effect")
|
|
|
|
# --- Effect stacks and component report stay equal. ---
|
|
bus.emit_signal("time_anchor_resolved", {}, true, {"label": "perfect"})
|
|
_expect_int(int(container.call("effect_stacks", BUFF_ID)), int(buff_component.call("attack_buff_stacks")), "effect stacks and component report should match")
|
|
|
|
# --- First held anchor per 4-beat window heals; low health doubles the heal. ---
|
|
health.call("set_values", 400, 1000)
|
|
bus.emit_signal("time_anchor_resolved", {"beat": 8}, true, {"label": "perfect"})
|
|
_expect_int(int(health.get("current")), 460, "perfect anchor should heal 60 while below half health")
|
|
bus.emit_signal("time_anchor_resolved", {"beat": 9}, true, {"label": "good"})
|
|
_expect_int(int(health.get("current")), 460, "second held anchor in the same 4-beat window should not heal")
|
|
bus.emit_signal("time_anchor_resolved", {"beat": 12}, true, {"label": "bad"})
|
|
_expect_int(int(health.get("current")), 480, "bad anchor in a new window should heal 20 while below half health")
|
|
health.call("set_values", 980, 1000)
|
|
bus.emit_signal("time_anchor_resolved", {"beat": 16}, true, {"label": "perfect"})
|
|
_expect_int(int(health.get("current")), 1000, "time-anchor healing should not exceed max health")
|
|
|
|
# --- chart_reset zeroes the buff. ---
|
|
bus.emit_signal("chart_reset", &"test")
|
|
_expect_int(int(buff_component.call("attack_buff_stacks")), 0, "chart_reset should zero the buff")
|
|
_expect_bool(container.call("active_effect_ids").has(BUFF_ID), false, "chart_reset should remove the buff effect")
|
|
|
|
# --- Regression: stack-aware aggregation keeps max_stacks = 1 behaviour. ---
|
|
var definition: Resource = load("res://resources/effects/effect_definition.gd").new()
|
|
definition.set("id", &"test_single_stack_add")
|
|
definition.set("duration_type", &"infinite")
|
|
definition.set("max_stacks", 1)
|
|
var modifier: Resource = load("res://resources/effects/stat_modifier.gd").new()
|
|
modifier.set("stat", &"damage_mult")
|
|
modifier.set("operation", "add")
|
|
modifier.set("value", 0.5)
|
|
var stat_modifiers: Array[Resource] = [modifier]
|
|
definition.set("stat_modifiers", stat_modifiers)
|
|
container.call("add_effect", definition)
|
|
container.call("add_effect", definition)
|
|
_expect_float(float(container.call("stat_multiplier", &"damage_mult")), 1.5, "max_stacks 1 add modifiers keep the migration baseline")
|
|
|
|
_finish()
|
|
|
|
|
|
func _expect_int(actual: int, expected: int, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
|
|
|
|
|
func _expect_float(actual: float, expected: float, label: String) -> void:
|
|
if not is_equal_approx(actual, expected):
|
|
failures.append("%s: expected %.3f, got %.3f" % [label, expected, actual])
|
|
|
|
|
|
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("PASS attack buff")
|
|
quit(0)
|
|
else:
|
|
for failure: String in failures:
|
|
push_error(failure)
|
|
quit(1)
|