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)