_draw() splits into two independent gates: the Layer-1-gated skeleton path (Region chords, clip retained) and the NEW DistrictWindowLayer- gated course path — build_course_render_plan() (pure, render-free- testable) consumed by draw_polyline with _zs-compensated widths and opacities from the Araminta revisit tables; mouth double-rings at Mouth termini only (EdgeDrain/ContinuesBeyondWindow/None: three meanings, one presentation — draw to last point, stop, documented); zero water clip on the course path by construction (courses carry rung-consistent termini). CourseTerminus wire vocabulary kept re- pointable pending A2's real serde names; synthetic Ruling-3h fixtures mean the suites need zero changes when the server payload lands. Real gap found and fixed: window arrival never redrew the nature overlay after the first fit (one line in _on_window_ready — courses would miss every window swap post-pan). Water-clip header rewritten to RESTRUCTURED status (retired on course rungs; permanent at Region until Region goes windowed, T-1143 ruling 2). Revert-verified (visibility-gate bypass -> 4 named failures). geometry-nature 112/112, nature-overlay 58/58, viewer 78/78, zero collateral; full sweep 3928/3928 after the full-import bootstrap; gdlint clean (viewer 1016->1017, pre-existing overage rides T-1158). Tickets: T-1170 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
580 lines
26 KiB
GDScript
580 lines
26 KiB
GDScript
## T-1156 wave 1: tests for AtlasWindowNatureOverlay — the whole-body
|
|
## Layer-1 (river/basin/attractor) draw node on the zoom ladder. Covers
|
|
## request/response lifecycle (idempotent-per-body, staleness guard, decode
|
|
## tolerance for a missing river_class array) and the draw-gate wiring
|
|
## (rung/overlay-bar double-gate), NOT pixel-level draw output — the
|
|
## coordinate math itself is covered directly in
|
|
## test_atlas_window_geometry_nature.gd, matching test_atlas_window_overlay.gd's
|
|
## own "cache/lifecycle here, colorizer pixels elsewhere" split.
|
|
class_name TestAtlasWindowNatureOverlay
|
|
extends GdUnitTestSuite
|
|
|
|
const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
|
|
|
|
|
|
## Minimal viewer stub — AtlasWindowNatureOverlay only reaches the viewer
|
|
## through get_held_granularity_v2()/get_body_radius_km()/get_held_center()/
|
|
## get_held_n()/get_cell_pixel_size()/get_view_zoom()/is_overlay_visible(),
|
|
## the same duck-typed-viewer precedent test_atlas_window_overlay.gd's
|
|
## _ViewerStub already establishes for AtlasWindowOverlay. get_view_zoom()
|
|
## added post-live-eyeball (coordinator finding, 2026-07-23): _draw() now
|
|
## reads it for the zoom-compensated marker-size fix. is_tile_mode()/
|
|
## get_district_window()/get_tile_set() added for T-1172 (the water clip) —
|
|
## district_window/tile_set default to null/an empty stub, matching "no
|
|
## arrived composite data yet" (the fail-open case) unless a test sets them.
|
|
class _ViewerStub:
|
|
var held_granularity_v2: String = "Region"
|
|
var body_radius_km: float = 6371.0
|
|
var held_center: Vector2i = Vector2i.ZERO
|
|
var held_n: int = 64
|
|
var view_zoom: float = 1.0
|
|
var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": false, "gen_attractors": false}
|
|
var tile_mode: bool = false
|
|
var district_window: Variant = null
|
|
var tile_set: Variant = null
|
|
|
|
func get_held_granularity_v2() -> String:
|
|
return held_granularity_v2
|
|
|
|
func get_body_radius_km() -> float:
|
|
return body_radius_km
|
|
|
|
func get_held_center() -> Vector2i:
|
|
return held_center
|
|
|
|
func get_held_n() -> int:
|
|
return held_n
|
|
|
|
func get_cell_pixel_size() -> float:
|
|
return 16.0
|
|
|
|
func get_view_zoom() -> float:
|
|
return view_zoom
|
|
|
|
func is_overlay_visible(overlay_id: String) -> bool:
|
|
return bool(overlay_visibility.get(overlay_id, false))
|
|
|
|
func is_tile_mode() -> bool:
|
|
return tile_mode
|
|
|
|
func get_district_window() -> Variant:
|
|
return district_window
|
|
|
|
func get_tile_set() -> Variant:
|
|
return tile_set
|
|
|
|
|
|
## Bare tile-set stub — AtlasWindowNatureOverlay's water-clip lookup only
|
|
## reaches it through get_tiles(), matching AtlasWindowTileSet's own public
|
|
## surface (an Array of {"center": Vector2i, "window": Variant}).
|
|
class _TileSetStub:
|
|
var tiles: Array = []
|
|
|
|
func get_tiles() -> Array:
|
|
return tiles
|
|
|
|
|
|
static func _mock_layer1(river_class: Variant = null) -> Dictionary:
|
|
var rn: Dictionary = {
|
|
"river_cells": [[10, 10], [20, 20], [30, 30]],
|
|
"confluences": [[15, 15]],
|
|
"mouths": [[40, 40]],
|
|
}
|
|
if river_class != null:
|
|
rn["river_class"] = river_class
|
|
return {
|
|
"river_network": rn,
|
|
"drainage_basins": [{"basin_id": 1, "boundary": [[0, 0], [0, 10], [10, 10], [10, 0]]}],
|
|
"attractors": [{"position": [10, 10], "strength": 0.5, "attractor_type": "Oasis", "sub_biome": ""}],
|
|
"grid_w": 256,
|
|
"grid_h": 128,
|
|
}
|
|
|
|
|
|
static func _mock_response(body_id: String, layer1: Variant) -> Dictionary:
|
|
return {"body_id": body_id, "status": "Ready", "layer1": layer1}
|
|
|
|
|
|
## Window-only responses (the OTHER shape SimBridge.atlas_layers_received
|
|
## carries, per AtlasWindowRequest's own test conventions) must be ignored —
|
|
## `layer1` is null on that envelope, matching atlas_response_from_raw()'s
|
|
## "only one of layer1/district_window populated per response" contract.
|
|
static func _mock_window_response(body_id: String) -> Dictionary:
|
|
return {"body_id": body_id, "status": "Ready", "district_window": {"n": 32}, "layer1": null}
|
|
|
|
|
|
func _make_overlay(viewer: Variant = null) -> Variant:
|
|
var o = auto_free(AtlasWindowNatureOverlay.new(viewer if viewer != null else _ViewerStub.new()))
|
|
add_child(o)
|
|
return o
|
|
|
|
|
|
# =============================================================================
|
|
# Request lifecycle
|
|
# =============================================================================
|
|
|
|
|
|
func test_no_layer1_before_any_request() -> void:
|
|
var o = _make_overlay()
|
|
assert_that(o.get_layer1()).is_null()
|
|
|
|
|
|
func test_response_for_requested_body_is_adopted() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
var layer1: Dictionary = _mock_layer1()
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
|
|
assert_that(o.get_layer1()).is_equal(layer1)
|
|
|
|
|
|
## Staleness guard: a response for a body this node never asked for (or
|
|
## navigated away from) must be ignored — same posture
|
|
## AtlasGenerationProxy.on_response()'s own body_id guard establishes.
|
|
func test_response_for_a_different_body_is_ignored() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
SimBridge.atlas_layers_received.emit(_mock_response("SomeOtherBody", _mock_layer1()))
|
|
assert_that(o.get_layer1()).is_null()
|
|
|
|
|
|
## A window-shaped response (the windowed DistrictWindowLayer envelope,
|
|
## `layer1` null) must be ignored outright — this node only ever adopts the
|
|
## whole-body Layer-1 envelope.
|
|
func test_window_only_response_is_ignored() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
SimBridge.atlas_layers_received.emit(_mock_window_response("GJ380c"))
|
|
assert_that(o.get_layer1()).is_null()
|
|
|
|
|
|
## request_layer1() for the SAME body id, after data has already arrived,
|
|
## must NOT clear the held data — a re-entrant enter_orbital() on the body
|
|
## already showing keeps drawing rivers instead of flashing them away.
|
|
func test_request_layer1_same_body_after_arrival_keeps_held_data() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
var layer1: Dictionary = _mock_layer1()
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
|
|
o.request_layer1("GJ380c")
|
|
assert_that(o.get_layer1()).is_equal(layer1)
|
|
|
|
|
|
## request_layer1() for a DIFFERENT body id must clear the previous body's
|
|
## held data immediately — the old body's rivers must never draw over the
|
|
## new body's terrain during the in-flight gap.
|
|
func test_request_layer1_different_body_clears_stale_data() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", _mock_layer1()))
|
|
o.request_layer1("AnotherBody")
|
|
assert_that(o.get_layer1()).override_failure_message(
|
|
"switching bodies must clear the previous body's layer1 data immediately,"
|
|
+ " not just leave it drawn until the new response arrives"
|
|
).is_null()
|
|
|
|
|
|
func test_request_layer1_empty_body_id_is_a_noop() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("")
|
|
SimBridge.atlas_layers_received.emit(_mock_response("", _mock_layer1()))
|
|
assert_that(o.get_layer1()).override_failure_message(
|
|
"an empty body_id must never be requested/adopted"
|
|
).is_null()
|
|
|
|
|
|
# =============================================================================
|
|
# Decode tolerance — layer1 without river_class (pre-T-1156 payload / the
|
|
# graceful-fallback empty-array case, Dudley's #[serde(default)] contract).
|
|
# =============================================================================
|
|
|
|
|
|
## A response with NO river_class key at all (river_network dict omits it —
|
|
## the msgpack-decode equivalent of Dudley's serde default producing an
|
|
## empty Vec) must still be adopted without error; per-cell class then falls
|
|
## back to RIVER_CLASS_FALLBACK (TRUNK) at draw time, not a crash/decode failure.
|
|
func test_layer1_without_river_class_key_is_still_adopted() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
var layer1: Dictionary = _mock_layer1() # no river_class arg -> key absent
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
|
|
assert_that(o.get_layer1()).is_equal(layer1)
|
|
assert_bool((o.get_layer1()["river_network"] as Dictionary).has("river_class")).is_false()
|
|
|
|
|
|
## An explicitly EMPTY river_class array (the actual wire shape Dudley's
|
|
## `#[serde(default)]` produces for a pre-T-1156 payload) must also decode
|
|
## without error and be adopted — the per-cell RIVER_CLASS_FALLBACK
|
|
## resolution this enables is exercised at draw time by
|
|
## test_atlas_window_nature_overlay_draw_smoke.gd (real-render smoke suite;
|
|
## draw_circle()/draw_rect() calls require a live render pass under this
|
|
## engine version — confirmed directly, matching
|
|
## test_atlas_window_overlay_draw_smoke.gd's own header doc on why a plain
|
|
## unit test cannot call `_draw()` outside one).
|
|
func test_layer1_with_empty_river_class_array_is_adopted() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
var layer1: Dictionary = _mock_layer1(PackedByteArray([]))
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
|
|
assert_that(o.get_layer1()).is_equal(layer1)
|
|
|
|
|
|
## No viewer at all (viewer == null, matching AtlasWindowOverlay's own
|
|
## "viewer == null -> return" guard convention) is a legal, inert state —
|
|
## _draw()'s null-viewer early-return (BEFORE any draw_*() call) is safe to
|
|
## call directly since it never reaches the engine's draw-context requirement.
|
|
func test_draw_with_null_viewer_is_a_noop() -> void:
|
|
var o = auto_free(AtlasWindowNatureOverlay.new(null))
|
|
add_child(o)
|
|
o._draw()
|
|
assert_that(o.get_layer1()).is_null()
|
|
|
|
|
|
## grid_w/grid_h missing or zero (a malformed/degenerate layer1) must decode
|
|
## and adopt cleanly — _draw()'s own grid_w<=0/grid_h<=0 early-return (also
|
|
## BEFORE any draw_*() call) is exercised the same direct way.
|
|
func test_draw_with_zero_grid_dims_returns_before_any_draw_call() -> void:
|
|
var o = _make_overlay()
|
|
o.request_layer1("GJ380c")
|
|
var layer1: Dictionary = _mock_layer1()
|
|
layer1["grid_w"] = 0
|
|
layer1["grid_h"] = 0
|
|
SimBridge.atlas_layers_received.emit(_mock_response("GJ380c", layer1))
|
|
o._draw() # grid_w<=0 -> returns before touching the canvas — safe to call directly
|
|
assert_that(o.get_layer1()).is_not_null()
|
|
|
|
|
|
## PR #195 review (Tyre I1) regression pin: every stroke-WIDTH argument in
|
|
## _draw_attractor_shape() must route through the pre-compensated `px_w`
|
|
## param, never a raw numeric literal — Godot multiplies stroke widths by the
|
|
## canvas scale exactly like radii, so a raw `2.0` rasterizes at ~0.01px at
|
|
## the Region orbital fit zoom (the identical sub-pixel failure the dot/ring
|
|
## zoom compensation fixed, missed on glyph outlines in the first pass). The
|
|
## draw-smoke suite cannot gate this (its own header documents the vacuous-
|
|
## pass mode under X11 BadMatch), so this is a SOURCE-SCAN pin: parse the
|
|
## overlay script's _draw_attractor_shape body and assert no draw_arc/
|
|
## draw_line call carries a bare numeric width literal. Crude but
|
|
## environment-independent, and it pins the exact regression class (someone
|
|
## reintroducing a literal width in a new glyph arm).
|
|
func test_attractor_shape_stroke_widths_are_never_raw_literals() -> void:
|
|
var src: String = (
|
|
FileAccess.get_file_as_string("res://ui/implant/apps/atlas/atlas_window_nature_overlay.gd")
|
|
)
|
|
var fn_start := src.find("func _draw_attractor_shape(")
|
|
assert_that(fn_start).override_failure_message(
|
|
"_draw_attractor_shape must exist in atlas_window_nature_overlay.gd"
|
|
).is_not_equal(-1)
|
|
var next_fn := src.find("\nfunc ", fn_start + 1)
|
|
var body := src.substr(fn_start, (next_fn - fn_start) if next_fn != -1 else -1)
|
|
var stroke_re := RegEx.new()
|
|
# A draw_arc/draw_line call whose FINAL (width) argument is a bare numeric
|
|
# literal: `, <digits[.digits]>)` at call end. px_w-scaled forms
|
|
# (`px_w`, `2.0 * px_w`) do not match.
|
|
stroke_re.compile("draw_(arc|line)\\([^\\n]*,\\s*\\d+(\\.\\d+)?\\s*\\)")
|
|
var hits := stroke_re.search_all(body)
|
|
var offenders: Array[String] = []
|
|
for hit in hits:
|
|
offenders.append(hit.get_string())
|
|
assert_array(offenders).override_failure_message(
|
|
"raw numeric stroke width(s) in _draw_attractor_shape — route through"
|
|
+ " px_w (PR #195 Tyre I1): %s" % [offenders]
|
|
).is_empty()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1172 — the two-waterline clip. _is_drawn_water()/_district() are called
|
|
# directly (both are pure lookups with NO draw_*() call of their own — the
|
|
# _draw()-requires-a-live-render-pass constraint the smoke suite exists for
|
|
# does not apply to them), matching this file's own "call private helpers
|
|
# directly when they're the load-bearing unit" precedent
|
|
# (test_draw_with_zero_grid_dims_returns_before_any_draw_call() above already
|
|
# calls _draw() itself specifically because its early-return is BEFORE any
|
|
# draw call — same reasoning here, one level down).
|
|
# =============================================================================
|
|
|
|
|
|
## A 4x4 District window (n=4, grid_side=4) centered on district (0,0),
|
|
## spanning [-2, 2) on both axes — cell (0,0) is water, everything else land.
|
|
## Mirrors test_atlas_window_water_clip.gd's own _mock_4x4_window() fixture
|
|
## shape (kept local here rather than shared — no cross-test-file import
|
|
## precedent in this cluster).
|
|
static func _mock_4x4_water_corner_window() -> Dictionary:
|
|
var morphology := PackedByteArray()
|
|
morphology.resize(16)
|
|
for i in range(16):
|
|
morphology[i] = 8 # AlluvialPlain — land
|
|
morphology[0] = 0 # OpenOcean — the single water cell, row 0 col 0
|
|
return {"center": [0, 0], "n": 4, "granularity_v2": "District", "morphology": morphology}
|
|
|
|
|
|
func _ctx_for(viewer_stub: _ViewerStub) -> Dictionary:
|
|
return {
|
|
"grid_w": 256.0,
|
|
"grid_h": 128.0,
|
|
"radius_km": 0.0, # no-radius: 1 heightmap pixel = 1 district metre (simplest math)
|
|
"held_center": viewer_stub.held_center,
|
|
"held_n": viewer_stub.held_n,
|
|
"cell_px": 16.0,
|
|
"cols": 0,
|
|
"granularity_v2": viewer_stub.held_granularity_v2,
|
|
"view_zoom": viewer_stub.view_zoom,
|
|
}
|
|
|
|
|
|
## Single-window mode: a district position resolving to a LAND cell must not
|
|
## be clipped (_is_drawn_water() returns false — the dot draws).
|
|
func test_is_drawn_water_false_for_a_land_cell_single_window() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message(
|
|
"a district position over a LAND cell must not be clipped"
|
|
).is_false()
|
|
|
|
|
|
## Single-window mode: a district position resolving to a WATER cell must be
|
|
## clipped (_is_drawn_water() returns true — the caller skips drawing).
|
|
func test_is_drawn_water_true_for_a_water_cell_single_window() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message(
|
|
"a district position over a WATER (OpenOcean) cell must be clipped"
|
|
).is_true()
|
|
|
|
|
|
## Tile mode: same land/water split, but the composite data arrives via
|
|
## get_tile_set().get_tiles() instead of get_district_window() — proving the
|
|
## clip predicate reaches BOTH path shapes, per the coordinator's explicit
|
|
## "both path shapes exercised" ask.
|
|
func test_is_drawn_water_works_in_tile_mode_land() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.tile_mode = true
|
|
var ts := _TileSetStub.new()
|
|
ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}]
|
|
stub.tile_set = ts
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message(
|
|
"tile mode: a district position over a LAND cell must not be clipped"
|
|
).is_false()
|
|
|
|
|
|
func test_is_drawn_water_works_in_tile_mode_water() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.tile_mode = true
|
|
var ts := _TileSetStub.new()
|
|
ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}]
|
|
stub.tile_set = ts
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message(
|
|
"tile mode: a district position over a WATER cell must be clipped"
|
|
).is_true()
|
|
|
|
|
|
## Tyre's rule 5: NO arrived composite data at the queried position (single-
|
|
## window mode, window is null — the pre-arrival state) must FAIL OPEN — the
|
|
## clip is a presentation refinement, never a data gate.
|
|
func test_is_drawn_water_fails_open_with_no_composite_data_single_window() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = null # nothing arrived yet
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message(
|
|
"no arrived composite data must fail OPEN (draw the dot), never clip"
|
|
).is_false()
|
|
|
|
|
|
## Same fail-open guarantee in tile mode: no tile set at all (get_tile_set()
|
|
## returns null, matching the viewer's own pre-enter_orbital() state).
|
|
func test_is_drawn_water_fails_open_with_no_tile_set() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.tile_mode = true
|
|
stub.tile_set = null
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message(
|
|
"no tile set at all must fail OPEN (draw the dot), never clip"
|
|
).is_false()
|
|
|
|
|
|
## Fail-open ALSO covers "a tile set exists but no tile covers this position
|
|
## yet" (mid-progressive-arrival) — the coordinator's own "briefly-unclipped
|
|
## dot during progressive arrival is fine and self-heals" framing.
|
|
func test_is_drawn_water_fails_open_when_no_tile_covers_the_position() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.tile_mode = true
|
|
var ts := _TileSetStub.new()
|
|
ts.tiles = [{"center": Vector2i(500, 500), "window": null}] # far away, unarrived
|
|
stub.tile_set = ts
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).is_false()
|
|
|
|
|
|
## Mouths must be SUPPRESSED (not snapped, not dimmed) on drawn water and
|
|
## render exactly as today on drawn land — this suite covers the SHARED
|
|
## predicate _draw_rivers() calls for river cells/confluences/mouths alike
|
|
## (_is_drawn_water() itself has no notion of "which feature type" — that's
|
|
## by design, per the ruling's "same predicate" wording for rule 3). A
|
|
## dedicated assertion here pins the WORDING intent (mouth-specific rule 3)
|
|
## even though the underlying mechanism is identical to the river-cell tests
|
|
## above — a future refactor that special-cases mouths differently should
|
|
## still trip this.
|
|
func test_mouth_position_on_water_is_suppressed_same_predicate_as_rivers() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(-1.9, -1.9), ctx)).override_failure_message(
|
|
"a mouth position over drawn water must resolve as clipped, via the SAME"
|
|
+ " predicate river cells/confluences use — no separate snap/dim path"
|
|
).is_true()
|
|
|
|
|
|
func test_mouth_position_on_land_is_not_suppressed() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(o._is_drawn_water(Vector2(1.9, 1.9), ctx)).override_failure_message(
|
|
"a mouth position over drawn land must render exactly as today (not clipped)"
|
|
).is_false()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1170 Ruling 3g/5a: _segment_touches_drawn_water() — the CHORD SEGMENT
|
|
# clip rule (both endpoints + midpoint), replacing the old per-point-only
|
|
# clip for the skeleton-chord draw path. The water cell is (0,0) in district
|
|
# space, per _mock_4x4_water_corner_window()'s own doc — spans roughly
|
|
# [-0.5, 0.5) x [-0.5, 0.5) at this fixture's district granularity.
|
|
# =============================================================================
|
|
|
|
|
|
## Both endpoints on land, entirely away from the water cell — no clip.
|
|
func test_segment_touches_drawn_water_false_when_fully_on_land() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(
|
|
o._segment_touches_drawn_water(Vector2(1.0, 1.0), Vector2(1.9, 1.9), ctx)
|
|
).override_failure_message(
|
|
"a segment entirely on land (both endpoints, and therefore its"
|
|
+ " midpoint) must not be clipped"
|
|
).is_false()
|
|
|
|
|
|
## Either endpoint alone on water clips the whole segment.
|
|
func test_segment_touches_drawn_water_true_when_an_endpoint_is_on_water() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
assert_bool(
|
|
o._segment_touches_drawn_water(Vector2(-1.9, -1.9), Vector2(1.9, 1.9), ctx)
|
|
).override_failure_message(
|
|
"a segment with EITHER endpoint over drawn water must be clipped"
|
|
).is_true()
|
|
|
|
|
|
## The decision this rule specifically exists to catch (Ruling 3g's ask, "pick
|
|
## the visually cleaner rule, document it, test it"): BOTH endpoints on land,
|
|
## on opposite sides of the water cell, with the MIDPOINT landing inside it —
|
|
## an endpoints-only rule would miss this entirely (a chord visibly crossing
|
|
## open water with neither end clipped). The midpoint sample must catch it.
|
|
func test_segment_touches_drawn_water_true_when_only_midpoint_is_on_water() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = _mock_4x4_water_corner_window()
|
|
var o = _make_overlay(stub)
|
|
var ctx: Dictionary = _ctx_for(stub)
|
|
# The water cell is (col=0, row=0), spanning district [-2,-1) x [-2,-1) in
|
|
# this n=4/grid_side=4 fixture (1:1 district-to-cell mapping). Pick
|
|
# endpoints that EACH resolve to a DIFFERENT LAND cell adjacent to the
|
|
# water corner — (-1.99, -0.9) resolves to (col=0, row=1), land; (-0.9,
|
|
# -1.99) resolves to (col=1, row=0), land — but their MIDPOINT
|
|
# (-1.445, -1.445) falls squarely inside the water cell (col=0, row=0).
|
|
# Verified numerically, not eyeballed (see the two sanity asserts below).
|
|
var from_district := Vector2(-1.99, -0.9)
|
|
var to_district := Vector2(-0.9, -1.99)
|
|
# Sanity: neither endpoint alone is clipped (both resolve to LAND cells)
|
|
# — isolates the midpoint as the ONLY reason the segment clips below.
|
|
assert_bool(o._is_drawn_water(from_district, ctx)).override_failure_message(
|
|
"test setup invariant: the FROM endpoint alone must resolve to land"
|
|
).is_false()
|
|
assert_bool(o._is_drawn_water(to_district, ctx)).override_failure_message(
|
|
"test setup invariant: the TO endpoint alone must resolve to land"
|
|
).is_false()
|
|
assert_bool(o._segment_touches_drawn_water(from_district, to_district, ctx)).override_failure_message(
|
|
"a segment whose ENDPOINTS are both on land but whose MIDPOINT lands"
|
|
+ " on drawn water must still be clipped — this is the exact failure"
|
|
+ " mode an endpoints-only rule would miss (Ruling 3g's ask)"
|
|
).is_true()
|
|
|
|
|
|
# =============================================================================
|
|
# T-1170 Ruling 5b (B3): _draw_course_path() early-return gating — the SAME
|
|
# "call the function directly when its early-return happens BEFORE any
|
|
# draw_*() call" precedent test_draw_with_null_viewer_is_a_noop() and
|
|
# test_draw_with_zero_grid_dims_returns_before_any_draw_call() already
|
|
# establish. Every case below returns before _draw_one_course() is ever
|
|
# reached, so calling _draw_course_path() directly (no SubViewport/render
|
|
# context) is safe. This is a SEPARATE data source/gate from the Layer-1
|
|
# skeleton path above — none of these tests touch _layer1 at all.
|
|
# =============================================================================
|
|
|
|
|
|
## The overlay-bar "gen_rivers" toggle gates the course path too — the SAME
|
|
## toggle the skeleton path uses (one player-facing "rivers" control covers
|
|
## both presentation surfaces, per the ruling).
|
|
func test_draw_course_path_returns_before_any_draw_when_gen_rivers_is_off() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.overlay_visibility["gen_rivers"] = false
|
|
stub.district_window = {
|
|
"n": 64, "granularity_v2": "District",
|
|
"courses": [{"class": 2, "points": [[0, 0], [100, 0]], "terminus": "None"}],
|
|
}
|
|
var o = _make_overlay(stub)
|
|
o._draw_course_path() # must return before draw_polyline() — no crash outside a render context
|
|
|
|
|
|
## No district window at all (single-window mode hasn't arrived yet) — the
|
|
## course path must return cleanly, not crash on a null window read.
|
|
func test_draw_course_path_returns_before_any_draw_when_no_window() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = null
|
|
var o = _make_overlay(stub)
|
|
o._draw_course_path()
|
|
|
|
|
|
## Ruling 3h decode tolerance: a window WITHOUT a `courses` key at all (the
|
|
## old/pre-A2 payload shape) must draw NOTHING at District/Quarter except
|
|
## mouths-on-land from the skeleton (that's the OTHER path's job) — this
|
|
## path itself must simply return, not error or fall back to a dot-scatter.
|
|
func test_draw_course_path_missing_courses_field_is_tolerated() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = {"n": 64, "granularity_v2": "District"} # no "courses" key
|
|
var o = _make_overlay(stub)
|
|
o._draw_course_path()
|
|
|
|
|
|
## An explicitly present but EMPTY courses array must also be tolerated
|
|
## cleanly (the loop simply iterates zero times).
|
|
func test_draw_course_path_empty_courses_array_is_tolerated() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": []}
|
|
var o = _make_overlay(stub)
|
|
o._draw_course_path()
|
|
|
|
|
|
## A `courses` field that is present but the WRONG TYPE (not an Array — e.g.
|
|
## a malformed/corrupted payload) must be tolerated the same way as a
|
|
## missing field, not crash attempting to iterate a non-Array.
|
|
func test_draw_course_path_non_array_courses_field_is_tolerated() -> void:
|
|
var stub := _ViewerStub.new()
|
|
stub.district_window = {"n": 64, "granularity_v2": "District", "courses": "not an array"}
|
|
var o = _make_overlay(stub)
|
|
o._draw_course_path()
|