ajhahn.de
← Theria
GDScript 142 lines
extends GutTest
## Contracts for the per-team fog-of-war vision. Vision is pure data over a SimState — no engine,
## socket, or render coupling — so these run headless and deterministically, exactly like the
## simulation and map-data tests. They pin the two properties the netcode and the renderer lean on:
## a team always sees its own units, and an enemy is seen only when it stands inside a friendly
## sight source's radius — and that the rule is team-fair (mirror-symmetric).


## A bare world with the wave schedule off, so a test seats exactly the units it asserts on.
func _world() -> SimCore:
	var sim := SimCore.new()
	sim.spawn_creeps = false
	return sim


func test_own_team_is_always_visible_even_out_of_sight_range() -> void:
	var sim := _world()
	var near_hero := sim.add_hero(0, Vector2.ZERO, 320.0)
	# A second friendly well beyond the first's sight radius — own units are seen regardless.
	var far_hero := sim.add_hero(0, Vector2(Vision.HERO_SIGHT * 4.0, 0.0), 320.0)
	var visible := Vision.visible_ids(sim.state, 0)
	assert_true(visible.has(near_hero), "a team always sees its own hero")
	assert_true(visible.has(far_hero), "a team sees its own hero even out of every sight radius")


func test_an_enemy_is_seen_only_inside_a_sight_radius() -> void:
	var sim := _world()
	sim.add_hero(0, Vector2.ZERO, 320.0)
	var seen := sim.add_entity(1, Vector2(Vision.HERO_SIGHT - 1.0, 0.0), 320.0, 100)
	var hidden := sim.add_entity(1, Vector2(Vision.HERO_SIGHT + 1.0, 0.0), 320.0, 100)
	var visible := Vision.visible_ids(sim.state, 0)
	assert_true(visible.has(seen), "an enemy just inside a hero's sight is visible")
	assert_false(visible.has(hidden), "an enemy just outside every sight radius stays in fog")


func test_a_creep_grants_vision() -> void:
	var sim := _world()
	sim.add_creep(0, 0, Vector2.ZERO)
	var enemy := sim.add_entity(1, Vector2(Vision.CREEP_SIGHT - 1.0, 0.0), 320.0, 100)
	assert_true(Vision.visible_ids(sim.state, 0).has(enemy), "a lane creep lights its front")


func test_a_structure_grants_vision() -> void:
	var sim := _world()
	sim.add_structure(0, Vector2.ZERO, 1000, 0, 0.0, 0)
	var enemy := sim.add_entity(1, Vector2(Vision.STRUCTURE_SIGHT - 1.0, 0.0), 320.0, 100)
	assert_true(Vision.visible_ids(sim.state, 0).has(enemy), "a tower holds a ward over its approach")


func test_a_downed_hero_grants_no_vision() -> void:
	var sim := _world()
	var hero := sim.add_hero(0, Vector2.ZERO, 320.0)
	sim.state.get_entity(hero).respawn_ticks = 100  # downed and on the respawn clock
	var enemy := sim.add_entity(1, Vector2(Vision.HERO_SIGHT - 1.0, 0.0), 320.0, 100)
	var visible := Vision.visible_ids(sim.state, 0)
	assert_true(visible.has(hero), "a team still sees its own downed hero")
	assert_false(visible.has(enemy), "a dead hero's ward goes dark — the enemy by its body is unseen")
	assert_eq(Vision.sight_sources(sim.state, 0).size(), 0, "a downed hero is not a sight source")


func test_sight_sources_lists_living_friendly_units_only() -> void:
	var sim := _world()
	var living := sim.add_hero(0, Vector2(120.0, -40.0), 320.0)
	var downed := sim.add_hero(0, Vector2(900.0, 0.0), 320.0)
	sim.state.get_entity(downed).respawn_ticks = 100
	sim.add_hero(1, Vector2.ZERO, 320.0)  # an enemy — never our source
	var sources := Vision.sight_sources(sim.state, 0)
	assert_eq(sources.size(), 1, "only the living friendly hero is a source")
	var pos := sim.state.get_entity(living).position
	assert_eq(sources[0]["center"], pos, "the source sits on the unit")
	assert_eq(sources[0]["radius"], Vision.HERO_SIGHT, "a hero's source carries the sight radius")


func test_a_wall_on_the_sight_line_hides_an_enemy_in_range() -> void:
	# Locate a real sight blocker and straddle it: a friendly source one side, the enemy the other,
	# close enough to be in range — the wall sitting between them must hide the enemy (the gank).
	var blockers := MapData.vision_blockers()
	assert_gt(blockers.size(), 0, "the map has sight-blocking walls")
	var wall: Dictionary = blockers[0]
	var center: Vector2 = wall["center"]
	var radius: float = wall["radius"]
	var sim := _world()
	sim.add_hero(0, center - Vector2.RIGHT * (radius + 30.0), 320.0)
	var enemy := sim.add_entity(1, center + Vector2.RIGHT * (radius + 30.0), 320.0, 100)
	assert_false(Vision.visible_ids(sim.state, 0).has(enemy), "a wall on the sight line hides it")


func test_a_clear_sight_line_sees_an_enemy_in_range() -> void:
	# Near a base fountain the walls keep their distance (WALL_SPAWN_CLEAR), so the sight line is
	# open — an in-range enemy with no wall between is seen, the counterpart to the occlusion above.
	var sim := _world()
	var base := MapData.spawn_for_team(0)
	sim.add_hero(0, base, 320.0)
	var enemy := sim.add_entity(1, base - base.normalized() * 150.0, 320.0, 100)
	assert_true(Vision.visible_ids(sim.state, 0).has(enemy), "a clear in-range enemy is seen")


func test_visible_ids_stays_cheap_over_a_heavy_world() -> void:
	# A slideshow tripwire, not a microbenchmark: the LOS occlusion scans the wall set, and the pass
	# runs a few times per tick (snapshot filter + render hide + minimap), so a full world must
	# resolve well inside the tick budget. The auto-pathing slice taught this lesson the hard way.
	var sim := _heavy_world()
	var start := Time.get_ticks_usec()
	for _i in 200:
		Vision.visible_ids(sim.state, 0)
	var per_call_us := float(Time.get_ticks_usec() - start) / 200.0
	print("visible_ids per call over a heavy world: %.1f us" % per_call_us)
	assert_lt(per_call_us, 2000.0, "visible_ids over a heavy world stays well under 2 ms/call")


func test_vision_is_team_fair_under_the_map_mirror() -> void:
	# The same encounter mirrored across the y = x axis and with the teams swapped must resolve the
	# same way — neither team sees farther, so fog never favours a side.
	var hero_pos := Vector2(0.0, 0.0)
	var enemy_pos := Vector2(500.0, 0.0)  # inside HERO_SIGHT

	var a := _world()
	a.add_hero(0, hero_pos, 320.0)
	var enemy_a := a.add_entity(1, enemy_pos, 320.0, 100)

	var b := _world()
	b.add_hero(1, MapData.mirror(hero_pos), 320.0)
	var enemy_b := b.add_entity(0, MapData.mirror(enemy_pos), 320.0, 100)

	assert_true(Vision.visible_ids(a.state, 0).has(enemy_a), "team 0 sees the enemy in range")
	assert_true(
		Vision.visible_ids(b.state, 1).has(enemy_b),
		"the mirrored encounter resolves identically for the swapped team",
	)


## A full mid-match world for the perf guard: both teams' structures, three heroes a side, and the
## opening creep waves on every lane (the first wave spawns on tick 0, so one step seeds it).
func _heavy_world() -> SimCore:
	var sim := SimCore.new()
	sim.spawn_structures()
	for team in 2:
		for i in 3:
			sim.add_hero(team, MapData.squad_spawn(team, i, 3), 320.0)
	sim.step({})
	return sim