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)