extends SceneTree const DiveDifficultyProfile = preload("res://game/domain/rules/dive_difficulty_profile.gd") const DiveUpgradeEffects = preload("res://game/domain/rules/dive_upgrade_effects.gd") const DiveAccessibilityAdjustments = preload( "res://game/domain/rules/dive_accessibility_adjustments.gd" ) const DiveRulesResolver = preload("res://game/domain/rules/dive_rules_resolver.gd") const DiveSession = preload("res://game/domain/dive/dive_session.gd") const DiveTick = preload("res://game/domain/dive/dive_tick.gd") const DiveTelemetrySample = preload("res://game/domain/dive/dive_telemetry_sample.gd") const CollisionObservation = preload("res://game/domain/dive/collision_observation.gd") const ToolPowerDemand = preload("res://game/domain/dive/tool_power_demand.gd") const EjectionIntent = preload("res://game/domain/dive/ejection_intent.gd") const ReturnRouteSample = preload("res://game/domain/dive/return_route_sample.gd") const DiveStep = preload("res://game/domain/dive/dive_step.gd") const DiveAlertLevel = preload("res://game/domain/dive/dive_alert_level.gd") const DiveTerminalReason = preload("res://game/domain/dive/dive_terminal_reason.gd") const DiveEvent = preload("res://game/domain/dive/dive_event.gd") const ReturnEstimate = preload("res://game/domain/dive/return_estimate.gd") const SESSION_ID := &"unit-dive" const TEST_COUNT := 21 var _failures: Array[String] = [] func _init() -> void: _test_rules_resolve_difficulty_upgrades_and_accessibility() _test_initial_snapshot_has_explicit_units() _test_sixty_ticks_drain_one_second_of_oxygen() _test_idle_does_not_drain_battery() _test_propulsion_and_tool_power_are_summed() _test_certified_depth_is_safe() _test_excess_depth_damages_hull() _test_crush_grace_delays_catastrophic_damage() _test_collision_is_idempotent() _test_collisions_are_ordered_and_cooled_down() _test_invalid_sequence_is_rejected_without_mutation() _test_oversized_tick_is_rejected() _test_alert_transition_is_emitted_once() _test_empty_battery_is_not_terminal() _test_ejection_hold_resets_on_release() _test_ejection_terminates_after_three_seconds() _test_confirmed_ejection_terminates_immediately() _test_oxygen_depletion_terminates_dive() _test_hull_destruction_terminates_dive() _test_return_estimate_uses_real_kwh() _test_session_has_no_internal_clock() if _failures.is_empty(): print("Dive foundation unit tests: %d passed, 0 failed." % TEST_COUNT) quit(0) else: for failure: String in _failures: push_error(failure) print("Dive foundation unit tests: %d failed." % _failures.size()) quit(1) func _test_rules_resolve_difficulty_upgrades_and_accessibility() -> void: var resolver := DiveRulesResolver.new() var expert = resolver.resolve(DiveDifficultyProfile.expert()) _expect_near(expert.oxygen_consumption_l_per_min, 36.0, 0.001, "expert oxygen") _expect_near(expert.longitudinal_power_kw, 2070.0, 0.001, "expert battery") _expect_near(expert.pressure_damage_multiplier, 1.35, 0.001, "expert pressure") _expect_near(expert.crush_grace_seconds, 1.125, 0.001, "expert crush grace") var upgraded = resolver.resolve( DiveDifficultyProfile.standard(), DiveUpgradeEffects.new(25.0, 120.0, 20.0, 40.0), DiveAccessibilityAdjustments.new(0.5, 0.75, 0.5) ) _expect_near(upgraded.battery_capacity_kwh, 125.0, 0.001, "upgrade battery capacity") _expect_near(upgraded.oxygen_capacity_l, 600.0, 0.001, "upgrade oxygen capacity") _expect_near(upgraded.hull_max_integrity, 120.0, 0.001, "upgrade hull capacity") _expect_near(upgraded.certified_depth_m, 100.0, 0.001, "upgrade certified depth") _expect_near(upgraded.oxygen_consumption_l_per_min, 15.0, 0.001, "accessible oxygen") _expect_near(upgraded.longitudinal_power_kw, 1350.0, 0.001, "accessible battery") func _test_initial_snapshot_has_explicit_units() -> void: var snapshot = _session().current_snapshot() _expect_near(snapshot.battery.remaining_kwh, 100.0, 0.001, "initial battery kWh") _expect_near(snapshot.oxygen.remaining_l, 480.0, 0.001, "initial oxygen litres") _expect_near(snapshot.oxygen.consumption_l_per_min, 30.0, 0.001, "oxygen L/min") _expect_near(snapshot.hull.certified_pressure_bar, 7.0, 0.001, "certified pressure") _expect_near(snapshot.depth.certified_m, 60.0, 0.001, "certified depth") func _test_sixty_ticks_drain_one_second_of_oxygen() -> void: var session := _session() for sequence in 60: var update = session.advance(_step(sequence, 1.0 / 60.0)) _expect(update.accepted, "60 Hz oxygen step %d accepted" % sequence) _expect_near(session.current_snapshot().oxygen.remaining_l, 479.5, 0.001, "one second oxygen") _expect(session.current_snapshot().tick_sequence == 59, "last 60 Hz sequence is 59") func _test_idle_does_not_drain_battery() -> void: var session := _session() var update = session.advance(_step(0, 0.1)) _expect(update.accepted, "idle tick accepted") _expect_near(update.snapshot.battery.remaining_kwh, 100.0, 0.001, "idle battery") _expect_near(update.snapshot.battery.current_power_kw, 0.0, 0.001, "idle power") func _test_propulsion_and_tool_power_are_summed() -> void: var session := _session() var tools: Array[ToolPowerDemand] = [ToolPowerDemand.new(&"cutter", 30.0)] var update = session.advance(_step(0, 1.0 / 60.0, 24.0, 0.5, [], tools)) _expect(update.accepted, "powered tick accepted") _expect_near(update.snapshot.battery.current_power_kw, 930.0, 0.001, "combined power") _expect_near( update.snapshot.battery.remaining_kwh, 100.0 - 930.0 / 3600.0 / 60.0, 0.00001, "combined energy drain" ) func _test_certified_depth_is_safe() -> void: var session := _session() var update = session.advance(_step(0, 0.1, 60.0)) _expect_near(update.snapshot.hull.current_integrity, 100.0, 0.001, "certified hull") _expect_near(update.snapshot.hull.exterior_pressure_bar, 7.0, 0.001, "current pressure") func _test_excess_depth_damages_hull() -> void: var session := _session() var update = session.advance(_step(0, 0.1, 70.0)) _expect(update.snapshot.hull.current_integrity < 100.0, "excess depth damages hull") _expect(_event_count(update.events, DiveEvent.Kind.HULL_DAMAGED) == 1, "pressure damage event") func _test_crush_grace_delays_catastrophic_damage() -> void: var session := _session() var update for sequence in 14: update = session.advance(_step(sequence, 0.1, 80.0)) _expect_near(update.snapshot.hull.current_integrity, 100.0, 0.001, "crush grace protects hull") update = session.advance(_step(14, 0.1, 80.0)) _expect(update.snapshot.hull.current_integrity < 100.0, "crush damage starts after grace") func _test_collision_is_idempotent() -> void: var session := _session() var first_collisions: Array[CollisionObservation] = [ CollisionObservation.new(&"impact-a", 0, 0, 6.5), ] var first = session.advance(_step(0, 0.1, 20.0, 0.0, first_collisions)) _expect_near(first.snapshot.hull.current_integrity, 88.0, 0.001, "first collision damage") var repeated_collisions: Array[CollisionObservation] = [ CollisionObservation.new(&"impact-a", 1, 1, 6.5), ] var repeated = session.advance(_step(1, 0.1, 20.0, 0.0, repeated_collisions)) _expect(repeated.accepted, "repeated collision tick accepted") _expect_near(repeated.snapshot.hull.current_integrity, 88.0, 0.001, "collision idempotence") func _test_collisions_are_ordered_and_cooled_down() -> void: var session := _session() var collisions: Array[CollisionObservation] = [ CollisionObservation.new(&"late", 5, 0, 9.0), CollisionObservation.new(&"early", 1, 0, 4.5), ] var update = session.advance(_step(0, 0.1, 20.0, 0.0, collisions)) _expect_near(update.snapshot.hull.current_integrity, 96.0, 0.001, "ordered collision cooldown") _expect(_event_count(update.events, DiveEvent.Kind.HULL_DAMAGED) == 1, "one hull damage event") func _test_invalid_sequence_is_rejected_without_mutation() -> void: var session := _session() var before = session.current_snapshot() var update = session.advance(_step(2, 1.0 / 60.0)) _expect(not update.accepted, "out-of-order tick rejected") _expect(update.snapshot == before, "rejected tick preserves snapshot") _expect(update.snapshot.tick_sequence == -1, "rejected tick preserves sequence") func _test_oversized_tick_is_rejected() -> void: var session := _session() var update = session.advance(_step(0, 0.11)) _expect(not update.accepted, "oversized tick rejected") _expect_near(update.snapshot.oxygen.remaining_l, 480.0, 0.001, "oversized tick consumes nothing") func _test_alert_transition_is_emitted_once() -> void: var session := DiveSession.new(SESSION_ID, _standard_rules(), -1.0, 144.01) var first = session.advance(_step(0, 0.1)) _expect(first.snapshot.alerts.oxygen == DiveAlertLevel.Value.ATTENTION, "oxygen attention") _expect(_alert_event_count(first.events, &"oxygen") == 1, "oxygen alert transition emitted") var second = session.advance(_step(1, 0.1)) _expect(_alert_event_count(second.events, &"oxygen") == 0, "oxygen alert not repeated") func _test_empty_battery_is_not_terminal() -> void: var session := DiveSession.new(SESSION_ID, _standard_rules(), 0.0) var update = session.advance(_step(0, 0.1)) _expect(update.accepted, "empty battery tick accepted") _expect( update.snapshot.terminal_reason == DiveTerminalReason.Value.NONE, "empty battery not terminal" ) _expect(update.snapshot.alerts.battery == DiveAlertLevel.Value.FAILURE, "battery failure alert") _expect(update.snapshot.oxygen.remaining_l < 480.0, "oxygen continues with empty battery") func _test_ejection_hold_resets_on_release() -> void: var session := _session() for sequence in 20: session.advance(_step(sequence, 0.1, 20.0, 0.0, [], [], true)) var released = session.advance(_step(20, 0.1, 20.0, 0.0, [], [], false)) _expect_near(released.snapshot.ejection.held_seconds, 0.0, 0.001, "ejection resets") _expect(not released.snapshot.is_terminal(), "released ejection is not terminal") func _test_ejection_terminates_after_three_seconds() -> void: var session := _session() var update for sequence in 30: update = session.advance(_step(sequence, 0.1, 20.0, 0.0, [], [], true)) _expect( update.snapshot.terminal_reason == DiveTerminalReason.Value.VOLUNTARY_EJECTION, "three-second ejection terminates" ) _expect(_event_count(update.events, DiveEvent.Kind.DIVE_TERMINATED) == 1, "terminal event emitted") var rejected = session.advance(_step(30, 0.1)) _expect(not rejected.accepted, "terminal session rejects later ticks") func _test_confirmed_ejection_terminates_immediately() -> void: var session := _session() var step := _step(0, 0.1) step.ejection_intent = EjectionIntent.new(false, true) var update = session.advance(step) _expect( update.snapshot.terminal_reason == DiveTerminalReason.Value.VOLUNTARY_EJECTION, "confirmed pause-menu ejection terminates immediately" ) func _test_oxygen_depletion_terminates_dive() -> void: var session := DiveSession.new(SESSION_ID, _standard_rules(), -1.0, 0.01) var update = session.advance(_step(0, 0.1)) _expect_near(update.snapshot.oxygen.remaining_l, 0.0, 0.001, "oxygen reaches zero") _expect( update.snapshot.terminal_reason == DiveTerminalReason.Value.OXYGEN_DEPLETED, "oxygen terminal reason" ) func _test_hull_destruction_terminates_dive() -> void: var session := DiveSession.new(SESSION_ID, _standard_rules(), -1.0, -1.0, 1.0) var collisions: Array[CollisionObservation] = [ CollisionObservation.new(&"fatal-impact", 0, 0, 8.0), ] var update = session.advance(_step(0, 0.1, 20.0, 0.0, collisions)) _expect_near(update.snapshot.hull.current_integrity, 0.0, 0.001, "hull reaches zero") _expect( update.snapshot.terminal_reason == DiveTerminalReason.Value.HULL_DESTROYED, "hull terminal reason" ) func _test_return_estimate_uses_real_kwh() -> void: var session := _session() var route := ReturnRouteSample.new(true, 600.0, 0.0) var update = session.advance(_step(0, 0.1, 20.0, 0.0, [], [], false, route)) _expect_near(update.snapshot.return_estimate.required_kwh, 50.0, 0.001, "return kWh") _expect_near(update.snapshot.return_estimate.margin_kwh, 50.0, 0.001, "return margin") _expect(update.snapshot.return_estimate.status == ReturnEstimate.Status.SAFE, "return safe") func _test_session_has_no_internal_clock() -> void: var session := _session() var before = session.current_snapshot() var after = session.current_snapshot() _expect(after == before, "session cannot advance without an explicit DiveStep") _expect_near(after.oxygen.remaining_l, 480.0, 0.001, "no implicit oxygen drain") func _standard_rules(): return DiveRulesResolver.new().resolve(DiveDifficultyProfile.standard()) func _session() -> DiveSession: return DiveSession.new(SESSION_ID, _standard_rules()) func _step( sequence: int, delta_seconds: float, depth_m: float = 20.0, longitudinal_effort: float = 0.0, collisions: Array[CollisionObservation] = [], tools: Array[ToolPowerDemand] = [], ejection_active: bool = false, return_route: ReturnRouteSample = null ) -> DiveStep: return DiveStep.new( DiveTick.new(SESSION_ID, sequence, delta_seconds), DiveTelemetrySample.new(depth_m, longitudinal_effort), collisions, tools, EjectionIntent.new(ejection_active), return_route if return_route != null else ReturnRouteSample.new() ) func _event_count(events: Array[DiveEvent], kind: DiveEvent.Kind) -> int: var count := 0 for event: DiveEvent in events: if event.kind == kind: count += 1 return count func _alert_event_count(events: Array[DiveEvent], subject: StringName) -> int: var count := 0 for event: DiveEvent in events: if event.kind == DiveEvent.Kind.ALERT_LEVEL_CHANGED and event.subject == subject: count += 1 return count func _expect(condition: bool, label: String) -> void: if not condition: _failures.append(label) func _expect_near(actual: float, expected: float, tolerance: float, label: String) -> void: if absf(actual - expected) > tolerance: _failures.append("%s: expected %.5f, got %.5f" % [label, expected, actual])