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)