class_name Movement extends RefCounted ## Pure movement helpers. These run on the server (authoritative) and on the ## client (prediction). They must stay side-effect free so a client can replay ## them over a history of inputs and land on the same result as the server. ## Clamp a raw analogue/keyboard vector to a unit disc. static func sanitize_move(raw: Vector2) -> Vector2: if raw.length_squared() > 1.0: return raw.normalized() return raw ## One tick of player locomotion. Returns the new position. ## ## Collision resolution lives in [MapGrid.slide_circle], and the client runs ## this exact call during prediction and reconciliation -- so the map the client ## holds has to match the server's, which is why maps are replicated in full ## even where fog hides them. static func step_player(pos: Vector2, move: Vector2, speed: float, map: MapGrid, radius: float = SimConfig.PLAYER_RADIUS) -> Vector2: var delta := sanitize_move(move) * speed * SimConfig.TICK_DELTA return map.slide_circle(pos, delta, radius) ## Circle-vs-circle overlap test used for every hit in the game. static func circles_overlap(a: Vector2, ar: float, b: Vector2, br: float) -> bool: var r := ar + br return a.distance_squared_to(b) <= r * r ## Outside the map's extent. Derivable on both sides from the map's dimensions ## alone, so it needs no announcement even when tiles have not been streamed. static func outside_map(p: Vector2, map: MapGrid) -> bool: if map == null: return false return not map.world_rect().grow(SimConfig.BULLET_CULL_MARGIN).has_point(p) ## Outside the map, or stopped by geometry. static func bullet_stopped(p: Vector2, map: MapGrid) -> bool: if map == null: return false return outside_map(p, map) or map.bullet_blocked(p)