nodes/ground.pyΒΆ

Part of Clumsy Bird.

 1"""Scrolling ground: two side-by-side ground sprites that loop, plus a
 2solid collision strip the bird crashes into."""
 3
 4from config import (
 5    ASSETS,
 6    GROUND_HEIGHT,
 7    GROUND_SCROLL_SPEED,
 8    ground_top,
 9)
10
11from simvx.core import CharacterBody2D, Node2D, RectangleShape2D, Sprite2D, Vec2
12
13
14class Ground(Node2D):
15    """Visual + collision ground strip.
16
17    Two ground sprites tile horizontally; when one scrolls past the left edge
18    it teleports to the right of the other for a seamless loop. A single
19    static CharacterBody2D along the strip handles bird-vs-ground collisions
20    (the strip never moves, only the visuals scroll).
21
22    Each sprite is one viewport wide, so the pair covers the window at any
23    size and the loop keeps working through a resize.
24    """
25
26    def __init__(self, **kwargs):
27        super().__init__(**kwargs)
28        self.add_to_group("ground")
29        self._scroll = 0.0
30
31        self._sprites = [
32            self.add_child(Sprite2D(texture=str(ASSETS / "ground.png"), height=GROUND_HEIGHT, name=f"Ground{tag}"))
33            for tag in ("A", "B")
34        ]
35
36        # Static collision body covering the ground strip; never moves.
37        self.collider = self.add_child(
38            CharacterBody2D(
39                shape=RectangleShape2D(half_extents=Vec2(1.0, GROUND_HEIGHT / 2)),
40                name="GroundCollider",
41            )
42        )
43        self.collider.add_to_group("ground_collider")
44
45    def on_ready(self):
46        self._layout()
47
48    def on_update(self, dt: float):
49        # Scroll offset runs from 0 to one viewport width and wraps, so the
50        # two sprites hand over seamlessly however wide the window is.
51        self._scroll = (self._scroll + GROUND_SCROLL_SPEED * dt) % float(self.viewport_size.x)
52        self._layout()
53
54    def _layout(self):
55        width, height = self.viewport_size
56        strip_centre_y = ground_top(height) + GROUND_HEIGHT / 2
57
58        for index, spr in enumerate(self._sprites):
59            spr.width = width
60            spr.position = Vec2(width * (index + 0.5) - self._scroll, strip_centre_y)
61
62        # The collider spans well past both edges so a bird nudged off-screen
63        # still lands on it.
64        self.collider.shape.half_extents = Vec2(width, GROUND_HEIGHT / 2)
65        self.collider.position = Vec2(width / 2, strip_centre_y)