From 084b663d63f520c9e96df6a548d4e21ef8b4778d Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 6 Oct 2026 19:55:19 +0200 Subject: [PATCH] feat(sites): playable bounds rule with typed status Co-Authored-By: Claude Haiku 4.5 --- .../application/sites/playable_bounds_rule.gd | 38 +++++++++++++++++ .../sites/playable_bounds_status.gd | 22 ++++++++++ scripts/quality_gate.sh | 10 +++++ tests/unit/run_site_application_tests.gd | 42 +++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 game/application/sites/playable_bounds_rule.gd create mode 100644 game/application/sites/playable_bounds_status.gd create mode 100644 tests/unit/run_site_application_tests.gd diff --git a/game/application/sites/playable_bounds_rule.gd b/game/application/sites/playable_bounds_rule.gd new file mode 100644 index 0000000..bbcfa04 --- /dev/null +++ b/game/application/sites/playable_bounds_rule.gd @@ -0,0 +1,38 @@ +class_name PlayableBoundsRule +extends RefCounted + +const PlayableBoundsStatus = preload( + "res://game/application/sites/playable_bounds_status.gd" +) + +var _bounds_m: Rect2 +var _warning_margin_m: float + + +func _init(bounds_m: Rect2, warning_margin_m: float = 40.0) -> void: + _bounds_m = bounds_m + _warning_margin_m = maxf(warning_margin_m, 0.0) + + +func evaluate(position_m: Vector3) -> PlayableBoundsStatus: + var point := Vector2(position_m.x, position_m.z) + var to_west := point.x - _bounds_m.position.x + var to_east := _bounds_m.end.x - point.x + var to_north := point.y - _bounds_m.position.y + var to_south := _bounds_m.end.y - point.y + var distance := minf(minf(to_west, to_east), minf(to_north, to_south)) + var state := PlayableBoundsStatus.State.INSIDE + if distance < 0.0: + state = PlayableBoundsStatus.State.OUTSIDE + elif distance < _warning_margin_m: + state = PlayableBoundsStatus.State.NEAR_EDGE + var direction := Vector3.ZERO + if state != PlayableBoundsStatus.State.INSIDE: + var centre := _bounds_m.get_center() + var toward := Vector2(centre.x - point.x, centre.y - point.y) + if distance == to_west or distance == to_east: + toward = Vector2(signf(centre.x - point.x), 0.0) + else: + toward = Vector2(0.0, signf(centre.y - point.y)) + direction = Vector3(toward.x, 0.0, toward.y) + return PlayableBoundsStatus.new(state, distance, direction) diff --git a/game/application/sites/playable_bounds_status.gd b/game/application/sites/playable_bounds_status.gd new file mode 100644 index 0000000..a694601 --- /dev/null +++ b/game/application/sites/playable_bounds_status.gd @@ -0,0 +1,22 @@ +class_name PlayableBoundsStatus +extends RefCounted + +enum State { + INSIDE, + NEAR_EDGE, + OUTSIDE, +} + +var state: State +var distance_to_edge_m: float +var return_direction: Vector3 + + +func _init(p_state: State, p_distance_to_edge_m: float, p_return_direction: Vector3) -> void: + state = p_state + distance_to_edge_m = p_distance_to_edge_m + return_direction = p_return_direction + + +func requires_warning() -> bool: + return state != State.INSIDE diff --git a/scripts/quality_gate.sh b/scripts/quality_gate.sh index 3b2aa1d..2ffbf89 100755 --- a/scripts/quality_gate.sh +++ b/scripts/quality_gate.sh @@ -113,6 +113,16 @@ check_godot_log \ "production site domain unit tests" \ "$QUALITY_TEMP_DIRECTORY/site-domain-unit-console.log" +printf 'Quality gate: production site application unit tests\n' +"$GODOT_EXECUTABLE" --headless \ + --log-file "$QUALITY_TEMP_DIRECTORY/site-application-unit.log" \ + --path . \ + --script tests/unit/run_site_application_tests.gd \ + 2>&1 | tee "$QUALITY_TEMP_DIRECTORY/site-application-unit-console.log" +check_godot_log \ + "production site application unit tests" \ + "$QUALITY_TEMP_DIRECTORY/site-application-unit-console.log" + printf 'Quality gate: production foundation integration\n' "$GODOT_EXECUTABLE" --headless \ --log-file "$QUALITY_TEMP_DIRECTORY/foundation-integration.log" \ diff --git a/tests/unit/run_site_application_tests.gd b/tests/unit/run_site_application_tests.gd new file mode 100644 index 0000000..b6a31d2 --- /dev/null +++ b/tests/unit/run_site_application_tests.gd @@ -0,0 +1,42 @@ +extends SceneTree + +const PlayableBoundsRule = preload("res://game/application/sites/playable_bounds_rule.gd") +const PlayableBoundsStatus = preload("res://game/application/sites/playable_bounds_status.gd") + +const TEST_COUNT := 9 + +var _failures: Array[String] = [] + + +func _init() -> void: + _test_bounds_rule() + + if _failures.is_empty(): + print("Site application unit tests: %d passed, 0 failed." % TEST_COUNT) + quit(0) + else: + for failure: String in _failures: + push_error(failure) + print("Site application unit tests: %d failed." % _failures.size()) + quit(1) + + +func _test_bounds_rule() -> void: + var rule := PlayableBoundsRule.new(Rect2(-300.0, -300.0, 600.0, 600.0), 40.0) + var centre := rule.evaluate(Vector3(0.0, -20.0, 0.0)) + _expect(centre.state == PlayableBoundsStatus.State.INSIDE, "centre is inside") + _expect(is_equal_approx(centre.distance_to_edge_m, 300.0), "distance to the nearest edge") + _expect(not centre.requires_warning(), "inside needs no warning") + var near := rule.evaluate(Vector3(275.0, -20.0, 10.0)) + _expect(near.state == PlayableBoundsStatus.State.NEAR_EDGE, "within the margin is near the edge") + _expect(is_equal_approx(near.distance_to_edge_m, 25.0), "near edge distance is measured to the east edge") + _expect(near.return_direction.is_equal_approx(Vector3.LEFT), "return direction points back toward the centre") + var outside := rule.evaluate(Vector3(0.0, -20.0, -340.0)) + _expect(outside.state == PlayableBoundsStatus.State.OUTSIDE, "beyond the edge is outside") + _expect(is_equal_approx(outside.distance_to_edge_m, -40.0), "outside distance is negative") + _expect(outside.return_direction.is_equal_approx(Vector3.BACK), "outside north returns south") + + +func _expect(condition: bool, label: String) -> void: + if not condition: + _failures.append(label)