extends GutTest ## Interest management: what the server declines to send. ## ## Fog is a rendering rule and stops nothing — a modified client draws whatever ## it was handed. These tests cover the part that is actually a defence: actors ## and bullet spawns the server never puts on the wire. var world: SimWorld const ME := 1 const OTHER := 2 func before_each() -> void: var map := MapGrid.new(200, 60, MapGrid.Kind.WALL) map.fill_rect(Rect2i(1, 1, 198, 58), MapGrid.Kind.FLOOR) map.centre_on_origin() world = SimWorld.new(1) world.set_map(map) var me := world.add_player(ME, "me") me.pos = Vector2.ZERO func _snap_for(peer: int) -> Dictionary: return NetCodec.decode_snapshot( NetCodec.encode_snapshot(world, Protocol.COUNTDOWN_NONE, peer)) func test_a_distant_enemy_is_not_sent_at_all() -> void: world.spawn_enemy(Content.turret(), Vector2(SimConfig.ACTOR_INTEREST_RADIUS + 300.0, 0.0)) assert_eq((_snap_for(ME)["enemies"] as Array).size(), 0, "an enemy across the map must never reach the client") func test_a_nearby_enemy_is_sent() -> void: world.spawn_enemy(Content.turret(), Vector2(200.0, 0.0)) assert_eq((_snap_for(ME)["enemies"] as Array).size(), 1) func test_enemies_are_visible_well_beyond_what_can_be_seen() -> void: # The interest radius has to exceed the fog radius, or enemies would appear # out of nowhere at the exact edge of sight. assert_gt(SimConfig.ACTOR_INTEREST_RADIUS, SimConfig.FOG_VIEW_RADIUS) world.spawn_enemy(Content.turret(), Vector2(SimConfig.FOG_VIEW_RADIUS + 50.0, 0.0)) assert_eq((_snap_for(ME)["enemies"] as Array).size(), 1, "just out of sight must still be known, so it does not pop in") func test_a_distant_player_is_not_sent() -> void: var them := world.add_player(OTHER, "them") them.pos = Vector2(SimConfig.ACTOR_INTEREST_RADIUS + 400.0, 0.0) var peers: Array = _snap_for(ME)["players"] assert_eq(peers.size(), 1) assert_eq(int(peers[0]["peer"]), ME) ## The one record that can never be filtered: the client reconciles its own ## prediction against it, so dropping it would break the player's own movement ## rather than merely hiding someone. func test_your_own_player_is_always_sent_however_far_out_you_are() -> void: world.players[ME].pos = Vector2(1.0e5, 1.0e5) var peers: Array = _snap_for(ME)["players"] assert_eq(peers.size(), 1) assert_eq(int(peers[0]["peer"]), ME) func test_a_distant_boss_is_not_sent() -> void: var boss := world.spawn_boss(Content.warden()) boss.pos = Vector2(SimConfig.ACTOR_INTEREST_RADIUS + 500.0, 0.0) assert_null(_snap_for(ME)["boss"], "you should not see a boss room you are nowhere near") boss.pos = Vector2(100.0, 0.0) assert_not_null(_snap_for(ME)["boss"]) func test_omitting_the_observer_encodes_the_whole_world() -> void: # Peer 0 means "no filtering" — used by tests and as the safe fallback when # the observer cannot be found. world.spawn_enemy(Content.turret(), Vector2(9000.0, 0.0)) var snap := NetCodec.decode_snapshot(NetCodec.encode_snapshot(world)) assert_eq((snap["enemies"] as Array).size(), 1) ## The radius that stops bullets winking into existence, checked against the ## real content rather than guessed. Adding a faster or longer-lived bullet ## should fail here rather than produce invisible damage in play. func test_the_bullet_radius_covers_the_longest_shot_in_the_game() -> void: # The player's shot is the longest-travelling bullet in the game once # upgrades are involved, so the worst case is measured at the speed ceiling # rather than at the base speed. test_interest used to model only static # content, which upgrades quietly invalidated. var worst := SimConfig.MAX_BULLET_SPEED \ * float(SimConfig.PLAYER_BULLET_LIFETIME) * SimConfig.TICK_DELTA var emitters: Array[BulletEmitter] = [] for id in Content.ALL_ENEMIES: emitters.append_array(Content.enemy(id).emitters) for boss_id in Content.ALL_BOSSES: for phase in Content.boss(boss_id).phases: emitters.append_array(phase.emitters) for e in emitters: worst = maxf(worst, e.speed * float(e.lifetime) * SimConfig.TICK_DELTA) assert_gte(SimConfig.BULLET_INTEREST_RADIUS, worst + SimConfig.FOG_VIEW_RADIUS, "a bullet spawned just outside the radius must not be able to reach " + "ground the player can see before it expires (worst travel %.0f)" % worst) ## The ceiling is what makes the radius above computable at all. Without it a ## stack of Snipers would push bullet travel past whatever radius was chosen, ## and the failure would be invisible damage rather than a failing test. func test_no_stack_of_upgrades_can_outrun_the_bullet_radius() -> void: var ids: Array[StringName] = [] for _i in 8: ids.append(Upgrades.SNIPER) var travel := PlayerStats.build(ids).bullet_speed \ * float(SimConfig.PLAYER_BULLET_LIFETIME) * SimConfig.TICK_DELTA assert_gte(SimConfig.BULLET_INTEREST_RADIUS, travel + SimConfig.FOG_VIEW_RADIUS)