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 Node2D, Sprite2D
10from simvx.core.math.types import Vec2
11
12from . import sprites as art
13from .gameboard import (
14 GRASS,
15 PATH,
16 SAND,
17 SHRINE,
18 TILES,
19 TREE,
20 TRIGGERS,
21 WALL,
22 WATER,
23)
24from .settings import GRID_H, GRID_W, TILE
25
26if TYPE_CHECKING:
27 from .audio_fx import AudioFX
28
29
30def _build_tile_atlas() -> dict[int, np.ndarray]:
31 return {
32 GRASS: art.grass_tile(),
33 PATH: art.path_tile(),
34 WATER: art.water_tile(),
35 WALL: art.wall_tile(),
36 TREE: art.tree_tile(),
37 SAND: art.path_tile(),
38 SHRINE: art.shrine_tile(),
39 }
40
41
42class _TriggerOverlay(Node2D):
43 """Encounter / shrine cell highlights, drawn over the tiles via z_index."""
44
45 def on_ready(self) -> None:
46 self.z_index = 500
47
48 def on_draw(self, renderer) -> None:
49 for (cx, cy), trig in TRIGGERS.items():
50 if trig[0] == "encounter":
51 x = cx * TILE
52 y = cy * TILE
53 renderer.draw_rect((x + 2, y + 2), (TILE - 4, TILE - 4), colour=(0.9, 0.3, 0.3, 0.30), filled=True)
54 renderer.draw_rect((x + 2, y + 2), (TILE - 4, TILE - 4), colour=(1.0, 0.5, 0.4, 0.85), filled=False)
55 elif trig[0] == "save":
56 x = cx * TILE
57 y = cy * TILE
58 renderer.draw_rect((x + 4, y + 4), (TILE - 8, TILE - 8), colour=(1.0, 0.95, 0.40, 0.40), filled=True)
59
60
61class _TileLayer(Node2D):
62 """Cached atlas of tile sprites; one Sprite2D per tile.
63
64 Built once at on_ready, then static. Z-index ensures it draws below
65 NPCs/Player.
66 """
67
68 def on_ready(self) -> None:
69 self.z_index = 0
70 atlas = _build_tile_atlas()
71 # Cache to avoid re-uploading: one Sprite2D per cell.
72 for y in range(GRID_H):
73 for x in range(GRID_W):
74 kind = TILES[y][x]
75 tex = atlas.get(kind, atlas[GRASS])
76 spr = Sprite2D(texture=tex)
77 spr.filter = "nearest"
78 spr.width = TILE
79 spr.height = TILE
80 spr.position = Vec2(x * TILE + TILE * 0.5, y * TILE + TILE * 0.5)
81 spr.z_index = -10
82 self.add_child(spr)
83
84
85class Overworld(Node2D):
86 """Container for the tile layer + NPCs + player. Player attaches separately."""
87
88 def __init__(self, audio: AudioFX | None = None) -> None:
89 super().__init__()
90 self.audio = audio
91 self.tile_layer = _TileLayer()
92 self.add_child(self.tile_layer)
93 self.trigger_overlay = _TriggerOverlay()
94 self.add_child(self.trigger_overlay)