nodes/overworld.pyΒΆ
Part of GDQuest Open RPG.
1"""Overworld: tilemap renderer + holds NPC list."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7import numpy as np
8
9from simvx.core import Controller, Node2D, Sprite2D
10from simvx.core.math.types import Vec2
11from simvx.core.ui import TouchController
12
13from . import sprites as art
14from .gameboard import (
15 GRASS,
16 PATH,
17 SAND,
18 SHRINE,
19 TILES,
20 TREE,
21 TRIGGERS,
22 WALL,
23 WATER,
24)
25from .settings import GRID_H, GRID_W, TILE
26
27if TYPE_CHECKING:
28 from .audio_fx import AudioFX
29
30
31def _build_tile_atlas() -> dict[int, np.ndarray]:
32 return {
33 GRASS: art.grass_tile(),
34 PATH: art.path_tile(),
35 WATER: art.water_tile(),
36 WALL: art.wall_tile(),
37 TREE: art.tree_tile(),
38 SAND: art.path_tile(),
39 SHRINE: art.shrine_tile(),
40 }
41
42
43class _TriggerOverlay(Node2D):
44 """Encounter / shrine cell highlights, drawn over the tiles via z_index."""
45
46 def on_ready(self) -> None:
47 self.z_index = 500
48
49 def on_draw(self, renderer) -> None:
50 for (cx, cy), trig in TRIGGERS.items():
51 if trig[0] == "encounter":
52 x = cx * TILE
53 y = cy * TILE
54 renderer.rect((x + 2, y + 2), (TILE - 4, TILE - 4), fill=(0.9, 0.3, 0.3, 0.30))
55 renderer.rect((x + 2, y + 2), (TILE - 4, TILE - 4), stroke=(1.0, 0.5, 0.4, 0.85))
56 elif trig[0] == "save":
57 x = cx * TILE
58 y = cy * TILE
59 renderer.rect((x + 4, y + 4), (TILE - 8, TILE - 8), fill=(1.0, 0.95, 0.40, 0.40))
60
61
62class _TileLayer(Node2D):
63 """Cached atlas of tile sprites; one Sprite2D per tile.
64
65 Built once at on_ready, then static. Z-index ensures it draws below
66 NPCs/Player.
67 """
68
69 def on_ready(self) -> None:
70 self.z_index = 0
71 atlas = _build_tile_atlas()
72 # Cache to avoid re-uploading: one Sprite2D per cell.
73 for y in range(GRID_H):
74 for x in range(GRID_W):
75 kind = TILES[y][x]
76 tex = atlas.get(kind, atlas[GRASS])
77 spr = Sprite2D(texture=tex)
78 spr.filter = "nearest"
79 spr.width = TILE
80 spr.height = TILE
81 spr.position = Vec2(x * TILE + TILE * 0.5, y * TILE + TILE * 0.5)
82 spr.z_index = -10
83 self.add_child(spr)
84
85
86class Overworld(Node2D):
87 """Container for the tile layer + NPCs + player. Player attaches separately."""
88
89 def __init__(self, audio: AudioFX | None = None) -> None:
90 super().__init__()
91 self.audio = audio
92 self.tile_layer = _TileLayer()
93 self.add_child(self.tile_layer)
94 self.trigger_overlay = _TriggerOverlay()
95 self.add_child(self.trigger_overlay)
96 # A thumb pad for walking, which is a controller: it publishes D-pad
97 # presses that the player's ``move`` binding already reads. A D-pad
98 # rather than a stick, because the world is a grid and an analogue
99 # reading has nowhere to go. It belongs to the overworld, so hiding the
100 # overworld for a battle takes it off the glass with everything else.
101 self.touch_pad = TouchController(layout="dpad_and_buttons", buttons=(Controller.A,))
102 self.add_child(self.touch_pad)