Files
settled-reach/client/tests/test_atlas_descend_entry.gd
T
jpmschweitzerandClaude Fable 5 d28d24fd26 feat(ui): step-canvas map component — RTT terrain + stepped zoom (D-255, T-1182)
The two-layer client rebuild per D-255(a)(b)(e), replacing the
_canvas.scale continuous-zoom model with one viewer, one path, all six
rungs:

- step_canvas_protocol.gd: StepCanvasRequest/Response codec against
  the T-1181 wire contract — incl. the discovered png_bytes subtlety
  (rmp_serde without serde_bytes emits a msgpack int-array, not bin;
  decode repacks via PackedByteArray before load_png_from_buffer) and
  the extent-echo rule (read the server-clamped extent, never assume
  the requested one).
- step_canvas/ component: transport (six-rung ladder, cursor-anchored
  scroll steps, edge-scroll/WASD pan with re-request on edge crossing,
  hard reset-to-Global), RTT terrain layer (Image.set_pixel colorize
  per the c1 measured ruling, texture.update reuse on step-cross,
  NEAREST coarse / LINEAR fine per rung), unscaled screen-space
  annotation sibling (courses + settlement markers at literal px),
  in-memory LRU cache (Tier 1; T-1183 layers the disk tiers beneath),
  request lifecycle (pending retry, staleness gate, extent echo).
- Full _canvas.scale retirement in the same change: the zoom-scaled
  canvas model, the _zs compensation family, select_rung /
  MAX_COVERAGE_M / compute_tile_grid, the orbital-mosaic-vs-window
  two-path split, _view_zoom/_canonical_fit_zoom — 10 source files
  deleted; their 14 test suites deleted with them (T-1157 dead-goldens
  rule; replacement visual-capture coverage is re-scoped T-1157).
- Surviving surfaces kept per the ticket: atlas_window_cache.gd's LRU
  shape (the ticket's named file atlas_window_tile_set.gd was the
  retiring orchestrator; the real LRU shape lives in
  atlas_window_cache.gd — cited in step_canvas_cache.gd), overlay
  colors, legend/overlay-bar chrome, AtlasViewer descend geometry.

Determinism boundary per D-255(e): the client interpolates only within
the closed server-supplied input set. 7 new gdUnit suites (164 cases)
incl. a real extent-echo bug caught by its own test during
implementation. Full client suite green (exit 0) with the live-gated
suites running against a worktree server build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 08:53:48 +02:00

524 lines
24 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## T-1138 (D-226 T-1124 amendment §5 entry revision): tests for the planetary
## AtlasViewer's click-through descent — the fixed view (no drag-pan/wheel-
## zoom), the pixel-to-DistrictPos inverse mapping (atlas_descend_geometry.gd),
## and the city-click-wins disambiguation rule.
class_name TestAtlasDescendEntry
extends GdUnitTestSuite
const AtlasDescendGeometry := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd")
# =============================================================================
# atlas_descend_geometry.gd — pure geometry (no scene tree needed)
# =============================================================================
## The default n=32 window's real footprint is 32 * 2.048 km = 65.536 km, so
## the label reads "~66 x 66 km" (rounded) — pins the number the amendment's
## "honest labeling" resolution depends on.
func test_reticle_label_extent_matches_district_window_default_n() -> void:
var label: Dictionary = AtlasDescendGeometry.reticle_label(Vector2(100.0, 100.0))
var expected_km: float = 32.0 * 2048.0 / 1000.0
assert_str(label["text"]).is_equal("~%.0f × %.0f km" % [expected_km, expected_km])
func test_reticle_label_position_offsets_from_center() -> void:
var center := Vector2(50.0, 60.0)
var label: Dictionary = AtlasDescendGeometry.reticle_label(center)
var pos: Vector2 = label["position"]
assert_float(pos.x).is_greater(center.x) # offset to the right, per §5's resolution
## Eight segments (four L-shaped bracket corners, two arms each) — every
## segment's "from" endpoint is exactly one of the four corner anchors, and no
## segment has zero length (a degenerate reticle would be invisible).
func test_reticle_segments_has_eight_nonzero_segments() -> void:
var segments: Array = AtlasDescendGeometry.reticle_segments(Vector2(200.0, 150.0))
assert_int(segments.size()).is_equal(8)
for seg: Array in segments:
assert_that(seg[0]).override_failure_message(
"a reticle segment must not be zero-length"
).is_not_equal(seg[1])
## The reticle's overall bounding box is centered on `center` and sized by
## DESCEND_RETICLE_SIZE — a regression guard against an off-center or
## mis-scaled bracket.
func test_reticle_segments_centered_on_input_point() -> void:
var center := Vector2(300.0, 300.0)
var segments: Array = AtlasDescendGeometry.reticle_segments(center)
var min_pt := Vector2(INF, INF)
var max_pt := Vector2(-INF, -INF)
for seg: Array in segments:
for p: Vector2 in seg:
min_pt.x = minf(min_pt.x, p.x)
min_pt.y = minf(min_pt.y, p.y)
max_pt.x = maxf(max_pt.x, p.x)
max_pt.y = maxf(max_pt.y, p.y)
var bbox_center: Vector2 = (min_pt + max_pt) * 0.5
assert_float(bbox_center.x).is_equal_approx(center.x, 0.01)
assert_float(bbox_center.y).is_equal_approx(center.y, 0.01)
# =============================================================================
# district_pos_at — pixel-to-DistrictPos inverse mapping
# =============================================================================
## No radius (tiny test body fallback, matches the server's own derive_district
## fallback): the district grid IS the heightmap grid 1:1, so a canvas point
## maps to the nearest integer district coordinate directly.
func test_district_pos_at_no_radius_is_1to1_pixel_mapping() -> void:
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(12.4, 7.6), 100.0, 100.0, 0.0
)
assert_that(pos).is_equal(Vector2i(12, 8))
func test_district_pos_at_zero_texture_size_is_safe() -> void:
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(Vector2(10.0, 10.0), 0.0, 0.0, 6371.0)
assert_that(pos).is_equal(Vector2i.ZERO)
## The equatorial center of the texture (px = tex_w/2) is district column
## district_cols/2 on a body with a radius (equator wraps at district (0,0)
## per the server's own doc: "district (0,0) sits at lon 0 / the equator").
##
## The TRUE equator pixel row is `(tex_h - 1) * 0.5`, NOT `tex_h * 0.5` (PR
## #187 review, Hoshe): the server's forward map (district_profile.rs:1436)
## divides by `ta.h.saturating_sub(1)`, so row 0 and row (h-1) are the real
## clamped pole endpoints and the midpoint between them is `(h-1)/2`. This
## test originally sampled `tex_h * 0.5`, which is NOT that midpoint for any
## finite tex_h — it happened to still round to district row 0 under BOTH the
## pre-fix buggy formula and the fix, at this specific radius/tex_h pair,
## which is exactly why this equator-only sample masked the ÷tex_h bug
## instead of catching it (see the off-equator round-trip tests below for the
## real regression coverage). Corrected here to the genuine equator pixel so
## this test asserts something true about the fixed formula, not a
## coincidence of the old one.
func test_district_pos_at_center_of_texture_is_near_equator_row_zero() -> void:
var radius_km := 6371.0
var tex_w := 1024.0
var tex_h := 512.0
var true_equator_py: float = (tex_h - 1.0) * 0.5
var pos: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(0.0, true_equator_py), tex_w, tex_h, radius_km
)
assert_int(pos.y).override_failure_message(
"the true equator pixel ((tex_h-1)/2) must map to row 0"
).is_equal(0)
## Panning across the texture's full width sweeps through the FULL district
## column range (not clamped to a tiny sub-range) — a coarse sanity check that
## district_cols is actually being derived from the body's circumference, not
## left at some degenerate default.
func test_district_pos_at_sweeps_full_column_range_across_texture_width() -> void:
var radius_km := 6371.0
var tex_w := 1024.0
var tex_h := 512.0
var left: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(0.0, tex_h * 0.5), tex_w, tex_h, radius_km
)
var right: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(tex_w - 1.0, tex_h * 0.5), tex_w, tex_h, radius_km
)
# An Earth-radius body has thousands of equatorial districts (circumference
# ~40,075 km / 2.048 km per district ≈ 19,568) — near-full-width should
# sweep a large fraction of that, not a handful of columns.
assert_int(absi(right.x - left.x)).is_greater(1000)
# =============================================================================
# Off-equator round-trip (PR #187 review, Hoshe — BLOCKING, live-repro'd bug):
# district_pos_at's row inverse divided by tex_h instead of (tex_h - 1),
# matching the WRONG symmetry with the column inverse (which correctly
# divides by the plain tex_w, since longitude wraps and has no edge case).
# The server's forward map (district_profile.rs:1436) uses
# ta.h.saturating_sub(1) for the SAME reason the ground-truth inverse
# (aliveness_probe.rs:511, `row / (ta_h - 1) - 0.5`) does: latitude CLAMPS at
# the poles, so row 0 / row (h-1) are real endpoints the division must land
# on exactly. This class of bug — a client-side twin of a server coordinate
# mapping silently drifting out of sync — is exactly what Tyre's C2 review
# criterion requires a round-trip drift guard for; that is what every test in
# this section is. The suite previously missed it because the only
# real-radius latitude sample was py = tex_h*0.5 (test above), the ONE point
## where the buggy (÷tex_h) and correct (÷(tex_h-1)) formulas round to the
# same integer district row — never exercising the asymmetry the fix targets.
# =============================================================================
## Forward-maps a district row to its heightmap pixel row using the server's
## OWN formula, transcribed here (district_profile.rs:1436):
## let lat_frac = (dy * DISTRICT_M / meridian_m).clamp(-0.5, 0.5);
## let py = (0.5 + lat_frac) * ta.h.saturating_sub(1);
## `dy` is a DistrictPos.y component (not a pixel) — the caller supplies the
## same `district_rows_half`-scaled value district_pos_at()'s inverse would
## need to recover, so this function and district_pos_at() are meant to be
## exact inverses of one another for any row within the clamped range.
static func _server_forward_map_row(district_row: int, tex_h: float, radius_km: float) -> float:
var meridian_m: float = PI * radius_km * 1000.0
var district_m: float = AtlasDescendGeometry.DISTRICT_M
var lat_frac: float = clampf((float(district_row) * district_m) / meridian_m, -0.5, 0.5)
return (0.5 + lat_frac) * (tex_h - 1.0)
## The actual regression: forward-map three off-equator district rows (one
## per hemisphere, plus a near-pole extreme) to pixel rows via the server's
## transcribed formula, then assert district_pos_at() recovers each EXACT
## row from that pixel. Fails outright under the pre-fix ÷tex_h formula for
## every row here except (coincidentally) very near the equator.
func test_district_pos_at_round_trips_off_equator_rows_against_server_forward_map() -> void:
var radius_km := 6238.4 # GJ380c (server/data/systems.db) — a real body, not a round number
var tex_w := 1024.0
var tex_h := 512.0
var col := 400 # arbitrary — this test is about the row axis only
# One row per hemisphere (moderate latitude) + one extreme near-pole row.
# meridian_m / DISTRICT_M ≈ 9,569 for this radius, so district_rows_half
# ≈ 4,785 — these rows sit well inside that range without saturating the
# clamp (except the "near pole" case, deliberately close to the edge).
var test_rows: Array = [-1200, 900, -4700]
for district_row: int in test_rows:
var forward_py: float = _server_forward_map_row(district_row, tex_h, radius_km)
var recovered: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(float(col), forward_py), tex_w, tex_h, radius_km
)
assert_int(recovered.y).override_failure_message(
(
"row round-trip failed: district_row=%d -> forward pixel py=%.4f -> "
+ "district_pos_at recovered row=%d (drift=%d)"
) % [district_row, forward_py, recovered.y, recovered.y - district_row]
).is_equal(district_row)
## Same round-trip, restated as an explicit non-equator drift guard: the
## pre-fix bug produced a WRONG but still-integer row (Hoshe's live repro:
## forward row 50, buggy inverse returned 45 — a 5-district, ~10.2 km drift)
## — a coarse "is it roughly right" tolerance would have passed that. This
## asserts EXACT equality specifically at a moderate off-equator row, not an
## approximate one, so a reintroduced ÷tex_h regression fails loudly again.
func test_district_pos_at_off_equator_row_is_not_off_by_a_few_districts() -> void:
var radius_km := 6238.4
var tex_w := 1024.0
var tex_h := 512.0
var district_row := 2500
var forward_py: float = _server_forward_map_row(district_row, tex_h, radius_km)
var recovered: Vector2i = AtlasDescendGeometry.district_pos_at(
Vector2(512.0, forward_py), tex_w, tex_h, radius_km
)
assert_int(recovered.y).is_equal(district_row)
# =============================================================================
# T-1142 item 1: is_on_texture() — the letterbox bounds gate
# =============================================================================
func test_is_on_texture_true_for_a_point_inside_the_texture() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, 250.0), 1024.0, 512.0)).is_true()
func test_is_on_texture_true_at_the_top_left_origin() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2.ZERO, 1024.0, 512.0)).is_true()
## Half-open range [0, tex_w) x [0, tex_h) — the last valid pixel is tex_w-1 /
## tex_h-1, NOT tex_w/tex_h themselves (canvas_pt.x == tex_w is one pixel
## PAST the texture, the classic off-by-one a naive <= bound would miss).
func test_is_on_texture_false_exactly_at_the_texture_width_bound() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(1024.0, 250.0), 1024.0, 512.0)).is_false()
func test_is_on_texture_false_exactly_at_the_texture_height_bound() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, 512.0), 1024.0, 512.0)).is_false()
## Jeroen's exact repro shape: a letterbox click lands far PAST the texture
## width in canvas space (a wide viewport around a 2:1-fitted heightmap).
func test_is_on_texture_false_for_a_letterbox_point_past_texture_width() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(1400.0, 250.0), 1024.0, 512.0)).is_false()
func test_is_on_texture_false_for_negative_coordinates() -> void:
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(-10.0, 250.0), 1024.0, 512.0)).is_false()
assert_bool(AtlasDescendGeometry.is_on_texture(Vector2(500.0, -10.0), 1024.0, 512.0)).is_false()
# =============================================================================
# T-1142 item 6a: canonicalize_district_center() — column wraps, row clamps
# =============================================================================
## Jeroen's own repro column (12276) on a body whose circumference works out
## to ~11236 districts (body_radius_km chosen so district_extent().cols ==
## 11236 as closely as the rounding allows) wraps down into range — the exact
## scenario the letterbox click hit before the bounds gate (item 1) made it
## unreachable via the UI, but canonicalization is still the correct backstop
## for any out-of-range center this or a future path constructs.
func test_canonicalize_wraps_a_column_past_the_circumference() -> void:
var radius_km := 6371.0 # -> district_extent().cols ~= 19,568 (Earth-like)
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var out_of_range := Vector2i(cols + 100, 0)
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
out_of_range, radius_km
)
assert_int(canonical.x).is_equal(100)
assert_int(canonical.y).is_equal(0)
## A column ONE past the wrap seam (cols) canonicalizes to column 0 — its
## "twin" on the other side of the antimeridian. This is the exact identity
## AtlasWindowCache.make_key() depends on for item 6b (a full-circumnavigation
## pan hits cache, not a fresh derive) — tested directly on the cache in
## test_atlas_window_viewer.gd; this pins the canonicalization half alone.
func test_canonicalize_one_column_past_the_seam_matches_its_twin() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var past_seam: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(cols, 50), radius_km
)
var at_seam: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, 50), radius_km
)
assert_that(past_seam).override_failure_message(
"column `cols` and column 0 are the same antimeridian-adjacent point"
).is_equal(at_seam)
## Negative columns wrap too (Euclidean, not truncating) — a pan that crosses
## the seam going WEST must land in [0, cols), not go negative.
func test_canonicalize_wraps_a_negative_column() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var cols: int = int(extent["cols"])
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(-5, 0), radius_km
)
assert_int(canonical.x).is_equal(cols - 5)
## Rows CLAMP, never wrap — a row past +rows_half pins to +rows_half exactly
## (the pole), matching the server's normalize_window_center() clamp
## disposition (latitude terminates, it does not wrap around).
func test_canonicalize_clamps_a_row_past_the_pole() -> void:
var radius_km := 6371.0
var extent: Dictionary = AtlasDescendGeometry.district_extent(radius_km)
var rows_half: int = int(extent["rows_half"])
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, rows_half + 500), radius_km
)
assert_int(canonical.y).is_equal(rows_half)
var canonical_south: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
Vector2i(0, -rows_half - 500), radius_km
)
assert_int(canonical_south.y).is_equal(-rows_half)
## An already-in-range center is a no-op (identity) — canonicalization must
## never perturb a legitimate, already-valid request.
func test_canonicalize_is_identity_for_an_in_range_center() -> void:
var radius_km := 6371.0
var in_range := Vector2i(500, 100)
var canonical: Vector2i = AtlasDescendGeometry.canonicalize_district_center(
in_range, radius_km
)
assert_that(canonical).is_equal(in_range)
## No-radius bodies (tiny test bodies) are identity — matching
## normalize_window_center()'s own no-radius disposition (no periodicity
## concept at the DistrictPos level for a body with no radius).
func test_canonicalize_no_radius_is_identity() -> void:
var anything := Vector2i(99999, -99999)
assert_that(AtlasDescendGeometry.canonicalize_district_center(anything, 0.0)).is_equal(anything)
# =============================================================================
# AtlasViewer — fixed view (no drag-pan/wheel-zoom) + click-through descent
# =============================================================================
## T-1120 capture API (set_view/get_view_offset/get_view_zoom) must survive
## the removal of user pan/zoom — this is the ticket's own explicit note, and
## the visual-golden harness depends on it.
func test_set_view_still_works_after_pan_zoom_removal() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v.set_view(3.0, Vector2(50.0, -20.0))
assert_that(v.get_view_zoom()).is_equal_approx(3.0, 0.001)
assert_that(v.get_view_offset()).is_equal(Vector2(50.0, -20.0))
## Clicking (with no heightmap loaded — the guard AtlasViewer's _gui_input
## checks first) must not emit a descend request — there is nothing to
## descend into yet.
func test_no_descend_signal_without_a_loaded_heightmap() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
# _heightmap_texture stays null (no show_body() call) — _gui_input's
# early-return guard should prevent any click handling at all.
var mb := InputEventMouseButton.new()
mb.button_index = MOUSE_BUTTON_LEFT
mb.pressed = true
mb.position = Vector2(100.0, 100.0)
v._gui_input(mb)
assert_int(received.size()).is_equal(0)
## T-1182 (D-255 stepped Atlas ladder): RegionalScreen no longer wraps
## AtlasViewer or forwards district_descend_requested — the "regional" nav
## entry opens the stepped six-rung ladder (StepCanvasViewer) directly at the
## Global opener (rung 0) — see regional_screen.gd's own doc. This
## regression-guards the wiring: entering "regional" reaches
## StepCanvasViewer, not AtlasViewer.
func test_regional_screen_wraps_step_canvas_viewer_not_atlas_viewer() -> void:
var screen: RegionalScreen = auto_free(RegionalScreen.new())
add_child(screen)
assert_object(screen._viewer).override_failure_message(
"RegionalScreen must wrap StepCanvasViewer (the stepped ladder) since T-1182,"
+ " not the retired AtlasViewer heightmap-texture display"
).is_instanceof(StepCanvasViewer)
# =============================================================================
# PR #187 review (Araminta — broken promise): the descend reticle must be
# hidden while hovering a city marker — _hovered_city already flares white as
# the "click here for city data" signal, and _try_click_city wins the click
# over descent, so drawing the reticle at the same time promised a descent
# the click would never perform. State-level tests (per the review's own
# "state-level is fine" allowance): assert _should_draw_descend_reticle()'s
# condition directly against _hover_active/_hovered_city/_heightmap_texture,
# rather than a pixel-diff on the actual draw call.
# =============================================================================
static func _one_pixel_texture() -> ImageTexture:
var img := Image.create(1, 1, false, Image.FORMAT_RGB8)
return ImageTexture.create_from_image(img)
## Hovering EMPTY map (no city under the cursor) -> reticle SHOWS.
func test_descend_reticle_shows_when_hovering_empty_map() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must show when hovering the open map (no city under cursor)"
).is_true()
## Hovering a CITY marker -> reticle HIDES, even though _hover_active is true
## (this is the exact bug: pre-fix, _hovered_city was never consulted here).
func test_descend_reticle_hides_when_hovering_a_city() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {"name": "Ridgeback", "pos": [10, 20]}
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must hide while hovering a city — the click would open city"
+ " data (_try_click_city wins), not descend"
).is_false()
## Not hovering at all (_hover_active false) -> reticle HIDES regardless of
## _hovered_city — the pre-existing base condition, guarded here so the city
## fix can't accidentally invert it.
func test_descend_reticle_hides_when_not_hovering() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = false
v._hovered_city = {}
assert_bool(v._should_draw_descend_reticle()).is_false()
# =============================================================================
# T-1142 item 1: the letterbox bounds gate, wired into AtlasViewer's own
# reticle guard and click fall-through (fields default to _tex_w=1024/
# _tex_h=512, _view_zoom=1.0, _view_offset=ZERO — screen_to_canvas() is
# therefore the identity transform in these tests, so a screen point maps
# 1:1 to the same canvas point, matching this file's own established
## _one_pixel_texture() convention above (the LOADED texture's real
# dimensions don't matter here — only _tex_w/_tex_h do).
# =============================================================================
## An on-map screen point (well inside [0, 1024) x [0, 512)) shows the
## reticle — the ordinary, expected case.
func test_descend_reticle_shows_for_an_on_texture_point() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(500.0, 250.0)
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must show for a point on the heightmap texture"
).is_true()
## Jeroen's exact repro shape: a letterbox point (canvas x >= tex_w, i.e. past
## the right edge of a fitted 2:1 heightmap in a wider viewport) HIDES the
## reticle — the affordance must never promise a descent the click can't
## honestly perform.
func test_descend_reticle_hides_for_a_letterbox_point_past_texture_width() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(1400.0, 250.0) # past _tex_w=1024
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must hide for a letterbox point past the texture's right edge"
).is_false()
## Same shape, Y axis — a letterbox point above/below a fitted heightmap
## (narrow-viewport case) must also hide the reticle.
func test_descend_reticle_hides_for_a_letterbox_point_past_texture_height() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
v._heightmap_texture = _one_pixel_texture()
v._hover_active = true
v._hovered_city = {}
v._hover_screen_pos = Vector2(500.0, 900.0) # past _tex_h=512
assert_bool(v._should_draw_descend_reticle()).override_failure_message(
"reticle must hide for a letterbox point past the texture's bottom edge"
).is_false()
## The click fall-through mirrors the reticle exactly (item 1's "one truth"
## requirement) — an on-texture click DOES emit district_descend_requested.
func test_descend_at_emits_for_an_on_texture_click() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
v._descend_at(Vector2(500.0, 250.0))
assert_int(received.size()).override_failure_message(
"an on-texture click must emit district_descend_requested"
).is_equal(1)
## A letterbox click is INERT — no signal at all, matching the reticle never
## having shown a promise there. This is Jeroen's exact repro: a letterbox
## click must never derive a DistrictPos, on-texture or off.
func test_descend_at_is_inert_for_a_letterbox_click() -> void:
var v: AtlasViewer = auto_free(AtlasViewer.new())
add_child(v)
var received: Array = []
v.district_descend_requested.connect(func(c: Vector2i) -> void: received.append(c))
v._descend_at(Vector2(1400.0, 250.0))
assert_int(received.size()).override_failure_message(
"a letterbox click must be inert — no district_descend_requested at all"
).is_equal(0)