nodes/sprites.pyΒΆ
Part of GDQuest Open RPG.
1"""Procedural pixel-art sprite generation (no PNG assets).
2
3Every sprite returned is an HxWx4 uint8 RGBA ndarray. Sized for 32-px tiles.
4Caller composites into Sprite2D via `texture=ndarray` (engine accepts ndarrays).
5"""
6
7from __future__ import annotations
8
9import numpy as np
10
11PX = 32
12
13# ---- helpers -------------------------------------------------------------
14
15
16def _rgba(r: float, g: float, b: float, a: float = 1.0) -> tuple[int, int, int, int]:
17 return (int(r * 255), int(g * 255), int(b * 255), int(a * 255))
18
19
20def _blank(w: int, h: int, rgba=(0, 0, 0, 0)) -> np.ndarray:
21 arr = np.zeros((h, w, 4), dtype=np.uint8)
22 arr[:] = rgba
23 return arr
24
25
26def _fill_rect(arr: np.ndarray, x: int, y: int, w: int, h: int, rgba) -> None:
27 arr[y : y + h, x : x + w] = rgba
28
29
30def _circle(arr: np.ndarray, cx: int, cy: int, r: int, rgba) -> None:
31 h, w = arr.shape[:2]
32 yy, xx = np.ogrid[:h, :w]
33 mask = (xx - cx) ** 2 + (yy - cy) ** 2 <= r * r
34 arr[mask] = rgba
35
36
37def _outline(arr: np.ndarray, rgba=(0, 0, 0, 255)) -> None:
38 """Add a 1-px outline where alpha goes from 0 to non-zero."""
39 h, w, _ = arr.shape
40 alpha = arr[..., 3]
41 out = arr.copy()
42 for dy in (-1, 0, 1):
43 for dx in (-1, 0, 1):
44 if dy == 0 and dx == 0:
45 continue
46 ys = slice(max(0, dy), h + min(0, dy))
47 xs = slice(max(0, dx), w + min(0, dx))
48 ydst = slice(max(0, -dy), h + min(0, -dy))
49 xdst = slice(max(0, -dx), w + min(0, -dx))
50 mask = (alpha[ys, xs] > 0) & (out[ydst, xdst, 3] == 0)
51 out[ydst, xdst][mask] = rgba
52 arr[:] = out
53
54
55# ---- tiles ---------------------------------------------------------------
56
57
58def grass_tile(rng: np.random.Generator | None = None) -> np.ndarray:
59 arr = _blank(PX, PX, _rgba(0.30, 0.62, 0.30))
60 rng = rng or np.random.default_rng(0)
61 for _ in range(8):
62 x, y = rng.integers(0, PX, 2)
63 arr[y, x] = _rgba(0.22, 0.52, 0.24)
64 for _ in range(3):
65 x, y = rng.integers(2, PX - 2, 2)
66 # tiny 2x2 darker patch
67 arr[y : y + 2, x : x + 2] = _rgba(0.18, 0.45, 0.22)
68 return arr
69
70
71def path_tile(rng: np.random.Generator | None = None) -> np.ndarray:
72 arr = _blank(PX, PX, _rgba(0.70, 0.60, 0.40))
73 rng = rng or np.random.default_rng(0)
74 for _ in range(6):
75 x, y = rng.integers(0, PX, 2)
76 arr[y, x] = _rgba(0.55, 0.45, 0.30)
77 return arr
78
79
80def water_tile() -> np.ndarray:
81 arr = _blank(PX, PX, _rgba(0.18, 0.40, 0.66))
82 for y in range(0, PX, 6):
83 for x in range(0, PX, 8):
84 arr[y, x : x + 4] = _rgba(0.40, 0.60, 0.85)
85 return arr
86
87
88def wall_tile() -> np.ndarray:
89 arr = _blank(PX, PX, _rgba(0.36, 0.30, 0.22))
90 for y in range(0, PX, 8):
91 arr[y, :] = _rgba(0.20, 0.16, 0.12)
92 for x in (0, 16):
93 for y in range(0, PX, 16):
94 arr[y : y + 8, x : x + 1] = _rgba(0.20, 0.16, 0.12)
95 return arr
96
97
98def tree_tile() -> np.ndarray:
99 arr = _blank(PX, PX, _rgba(0.30, 0.62, 0.30))
100 # trunk
101 _fill_rect(arr, PX // 2 - 2, PX - 8, 4, 8, _rgba(0.30, 0.20, 0.10))
102 # crown
103 _circle(arr, PX // 2, PX // 2 - 2, 11, _rgba(0.16, 0.42, 0.18))
104 _circle(arr, PX // 2 - 5, PX // 2 - 5, 6, _rgba(0.10, 0.32, 0.13))
105 _circle(arr, PX // 2 + 4, PX // 2 - 1, 5, _rgba(0.22, 0.55, 0.22))
106 return arr
107
108
109def shrine_tile() -> np.ndarray:
110 arr = _blank(PX, PX, _rgba(0.30, 0.62, 0.30))
111 # base
112 _fill_rect(arr, 6, 22, 20, 8, _rgba(0.55, 0.55, 0.55))
113 _fill_rect(arr, 8, 8, 16, 16, _rgba(0.85, 0.78, 0.55))
114 # crystal
115 _fill_rect(arr, 13, 4, 6, 14, _rgba(0.75, 0.95, 1.00, 0.86))
116 _fill_rect(arr, 15, 1, 2, 4, _rgba(1.00, 1.00, 1.00, 0.86))
117 return arr
118
119
120# ---- characters: 4-direction, 2-frame walk -------------------------------
121
122
123def _humanoid(body: tuple, head: tuple, hat: tuple | None = None) -> np.ndarray:
124 """Single 32x32 idle-frame humanoid (front-facing)."""
125 arr = _blank(PX, PX)
126 # legs
127 _fill_rect(arr, 11, 22, 4, 8, body)
128 _fill_rect(arr, 17, 22, 4, 8, body)
129 # body
130 _fill_rect(arr, 9, 12, 14, 12, body)
131 # head
132 _circle(arr, 16, 9, 5, head)
133 # hat
134 if hat is not None:
135 _fill_rect(arr, 11, 3, 10, 3, hat)
136 _fill_rect(arr, 13, 6, 6, 1, hat)
137 # eyes
138 arr[8, 14] = _rgba(0, 0, 0)
139 arr[8, 18] = _rgba(0, 0, 0)
140 _outline(arr)
141 return arr
142
143
144def knight_sprite() -> np.ndarray:
145 return _humanoid(_rgba(0.55, 0.55, 0.65), _rgba(0.95, 0.80, 0.62), hat=_rgba(0.75, 0.75, 0.85))
146
147
148def wizard_sprite() -> np.ndarray:
149 return _humanoid(_rgba(0.30, 0.30, 0.65), _rgba(0.95, 0.80, 0.62), hat=_rgba(0.20, 0.20, 0.50))
150
151
152def squirrel_sprite() -> np.ndarray:
153 arr = _blank(PX, PX)
154 # body
155 _circle(arr, 16, 20, 8, _rgba(0.65, 0.40, 0.18))
156 # head
157 _circle(arr, 16, 11, 6, _rgba(0.78, 0.48, 0.20))
158 # tail
159 for r in (8, 6, 4):
160 _circle(arr, 26, 18 - (8 - r), r, _rgba(0.78, 0.48, 0.20))
161 # ears
162 _circle(arr, 13, 6, 2, _rgba(0.45, 0.28, 0.12))
163 _circle(arr, 19, 6, 2, _rgba(0.45, 0.28, 0.12))
164 # eyes
165 arr[10, 13] = _rgba(0, 0, 0)
166 arr[10, 19] = _rgba(0, 0, 0)
167 _outline(arr)
168 return arr
169
170
171def bear_sprite() -> np.ndarray:
172 arr = _blank(PX, PX)
173 _circle(arr, 16, 20, 11, _rgba(0.30, 0.18, 0.10))
174 _circle(arr, 16, 10, 8, _rgba(0.40, 0.24, 0.14))
175 _circle(arr, 10, 5, 3, _rgba(0.30, 0.18, 0.10))
176 _circle(arr, 22, 5, 3, _rgba(0.30, 0.18, 0.10))
177 _circle(arr, 16, 12, 2, _rgba(0.10, 0.06, 0.04))
178 arr[8, 12] = _rgba(1, 1, 1)
179 arr[8, 20] = _rgba(1, 1, 1)
180 _outline(arr)
181 return arr
182
183
184def bugcat_sprite() -> np.ndarray:
185 arr = _blank(PX, PX)
186 _circle(arr, 16, 18, 9, _rgba(0.85, 0.65, 0.30))
187 # ears
188 pts1 = [(11, 7), (13, 12), (10, 13)]
189 pts2 = [(21, 7), (19, 12), (22, 13)]
190 for x, y in pts1 + pts2:
191 _circle(arr, x, y, 1, _rgba(0.70, 0.50, 0.20))
192 # head
193 _circle(arr, 16, 11, 6, _rgba(0.95, 0.78, 0.45))
194 # antennae
195 for x in (12, 20):
196 for y in range(2, 7):
197 arr[y, x] = _rgba(0.30, 0.20, 0.10)
198 _circle(arr, x, 1, 2, _rgba(0.50, 0.30, 0.30))
199 # eyes
200 _circle(arr, 13, 11, 2, _rgba(0.10, 0.06, 0.04))
201 _circle(arr, 19, 11, 2, _rgba(0.10, 0.06, 0.04))
202 arr[10, 13] = _rgba(1, 1, 1)
203 arr[10, 19] = _rgba(1, 1, 1)
204 _outline(arr)
205 return arr
206
207
208def wolf_sprite() -> np.ndarray:
209 arr = _blank(PX, PX)
210 _circle(arr, 16, 19, 10, _rgba(0.42, 0.42, 0.50))
211 _circle(arr, 16, 10, 6, _rgba(0.55, 0.55, 0.65))
212 # ears (triangular-ish)
213 for dy in range(0, 5):
214 arr[3 + dy, 11 - dy : 13] = _rgba(0.30, 0.30, 0.40)
215 arr[3 + dy, 19 : 21 + dy] = _rgba(0.30, 0.30, 0.40)
216 # eyes
217 arr[10, 13] = _rgba(1, 0.85, 0.20)
218 arr[10, 19] = _rgba(1, 0.85, 0.20)
219 _outline(arr)
220 return arr