nextrpg.draw_on_screen

Drawable on screen.

  1"""
  2Drawable on screen.
  3"""
  4
  5from __future__ import annotations
  6
  7from abc import ABC, abstractmethod
  8from dataclasses import KW_ONLY, field
  9from functools import cached_property, singledispatchmethod
 10from itertools import product
 11from math import ceil, sqrt
 12from pathlib import Path
 13from typing import override
 14
 15from pygame import Mask, SRCALPHA, Surface
 16from pygame.draw import polygon
 17from pygame.image import load
 18from pygame.mask import from_surface
 19
 20from nextrpg.config import config
 21from nextrpg.core import Direction, DirectionalOffset, Pixel, Rgba, Size
 22from nextrpg.model import Model, cached, internal_field
 23
 24
 25class Coordinate(Model):
 26    """
 27    Represents a 2D coordinate with immutability and provides methods
 28    for scaling and shifting coordinates.
 29
 30    Attributes:
 31        `left`: The horizontal position of the coordinate, measured by
 32            the number of pixels from the left edge of the game window.
 33
 34        `top`: The vertical position of the coordinate, measured by
 35            the number of pixels from the top edge of the game window.
 36    """
 37
 38    left: Pixel
 39    top: Pixel
 40
 41    def __mul__(self, scale: float) -> Coordinate:
 42        """
 43        Scales the current `Coordinate` values (left and top) by a given factor
 44        and returns a new `Coordinate` with the scaled values rounded up to the
 45        nearest integer.
 46
 47        Round up so that drawings won't leave tiny, black gaps after scaled.
 48
 49        Args:
 50            `scale`: The scaling factor to multiply the left and
 51                top values of the `Coordinate`.
 52
 53        Returns:
 54            `Coordinate`: A new `Coordinate` with the scaled and rounded values.
 55        """
 56        return Coordinate(ceil(self.left * scale), ceil(self.top * scale))
 57
 58    def __sub__(self, other: Coordinate) -> Coordinate:
 59        return Coordinate(self.left - other.left, self.top - other.top)
 60
 61    @singledispatchmethod
 62    def __add__(self, offset: DirectionalOffset | Coordinate) -> Coordinate:
 63        """
 64        Shifts the coordinate in the specified direction by a given offset.
 65        Supports both orthogonal and diagonal directions.
 66
 67        Or add two coordinates together.
 68
 69        For diagonal directions, the offset is divided proportionally.
 70        For example, an offset of `sqrt(2)` in `UP_LEFT` direction shifts
 71        the coordinate `Pixel(1)` in both `UP` and `LEFT` directions.
 72
 73        Args:
 74            `offset`: A `DirectionalOffset` representing the direction
 75                and offset, or `Coordinate` to add to the current `Coordinate`.
 76
 77        Returns:
 78            `Coordinate`: A new coordinate shifted by the specified offset in
 79            the given direction.
 80        """
 81        raise NotImplementedError(f"Non-addable {offset=}")
 82
 83    @property
 84    def tuple(self) -> tuple[Pixel, Pixel]:
 85        """
 86        Gets the coordinates as a tuple.
 87
 88        Returns:
 89            `tuple[Pixel, Pixel]`: A tuple containing the left and top
 90                values in that order.
 91        """
 92        return self.left, self.top
 93
 94
 95@Coordinate.__add__.register
 96def _add_directional_offset(self, offset: DirectionalOffset) -> Coordinate:
 97    match offset.direction:
 98        case Direction.UP:
 99            return Coordinate(self.left, self.top - offset.offset)
100        case Direction.DOWN:
101            return Coordinate(self.left, self.top + offset.offset)
102        case Direction.LEFT:
103            return Coordinate(self.left - offset.offset, self.top)
104        case Direction.RIGHT:
105            return Coordinate(self.left + offset.offset, self.top)
106
107    diag = offset.offset / sqrt(2)
108    match offset.direction:
109        case Direction.UP_LEFT:
110            return Coordinate(self.left - diag, self.top - diag)
111        case Direction.UP_RIGHT:
112            return Coordinate(self.left + diag, self.top - diag)
113        case Direction.DOWN_LEFT:
114            return Coordinate(self.left - diag, self.top + diag)
115        case Direction.DOWN_RIGHT:
116            return Coordinate(self.left + diag, self.top + diag)
117    raise ValueError(f"Invalid direction: {offset.direction}")
118
119
120@Coordinate.__add__.register
121def _add_coordinate(self, offset: Coordinate) -> Coordinate:
122    return Coordinate(self.left + offset.left, self.top + offset.top)
123
124
125@cached(
126    lambda: config().resource.drawing_cache_size,
127    lambda resource: resource if isinstance(resource, Path) else None,
128)
129class Drawing(Model):
130    """
131    Represents a drawable element and provides methods for accessing its size
132    and dimensions.
133
134    This class loads a surface from a file or directly accepts a
135    `pygame.Surface` as input.
136    It provides properties to access surface dimensions and size and methods to
137    crop and scale the surface.
138    """
139
140    resource: Path | Surface = field(repr=False)
141    _: KW_ONLY = field()
142    _surface: Surface = internal_field(
143        lambda self: (
144            self.resource
145            if isinstance(self.resource, Surface)
146            else load(self.resource).convert_alpha()
147        )
148    )
149
150    def __repr__(self) -> str:
151        """
152        A string representation of the `Drawing` object.
153
154        Returns:
155            `str`: A string representation of the `Drawing` object.
156        """
157        return f"Drawing({self.size})"
158
159    @property
160    def width(self) -> Pixel:
161        """
162        Gets the width of the surface.
163
164        Returns:
165            `Pixel`: The width of the surface in pixel measurement.
166        """
167        return self._surface.get_width()
168
169    @property
170    def height(self) -> Pixel:
171        """
172        Gets the height of the surface.
173
174        Returns:
175            `Pixel`: The height of the surface in pixel measurement.
176        """
177        return self._surface.get_height()
178
179    @property
180    def size(self) -> Size:
181        """
182        Gets the size of an object as a combination of its width and height
183
184        Returns:
185            `Size`: A Size object containing the width and height of the object.
186        """
187        return Size(self.width, self.height)
188
189    @property
190    def pygame(self) -> Surface:
191        """
192        Gets the current `pygame.Surface` for the object.
193
194        Returns:
195            `pygame.Surface`: The underlying `pygame.Surface`.
196        """
197        return self._debug_surface or self._surface
198
199    def crop(self, top_left: Coordinate, size: Size) -> Drawing:
200        """
201        Crops a rectangular portion of the drawing specified by the
202        top-left corner and the size.
203
204        The method extracts a subsection of the drawing based on the provided
205        coordinates and dimensions and returns a new `Drawing` instance.
206        The original drawing remains unchanged.
207
208        Args:
209            `top_left`: The top-left coordinate of the rectangle to be cropped.
210
211            `size`: The width and height of the rectangle to be cropped.
212
213        Returns:
214            `Drawing`: A new `Drawing` instance representing the cropped area.
215        """
216        left, top = top_left.tuple
217        width, height = size.tuple
218        return Drawing(self._surface.subsurface((left, top, width, height)))
219
220    @cached_property
221    def _debug_surface(self) -> Surface | None:
222        if not (debug := config().debug):
223            return None
224        surface = Surface(self.size.tuple, SRCALPHA)
225        surface.fill(debug.drawing_background_color.tuple)
226        surface.blit(self._surface, (0, 0))
227        return surface
228
229
230class DrawOnScreen(Model):
231    """
232    Represents a drawable element positioned on the screen with its coordinates.
233
234    This immutable class combines a drawing with its position and provides
235    properties to access various coordinates and dimensions of the drawing
236    on the screen.
237
238    Attributes:
239        `top_left`: The top-left position of the drawing on the screen.
240
241        `drawing`: The drawable element to be displayed on the screen.
242    """
243
244    top_left: Coordinate
245    drawing: Drawing
246
247    @property
248    def rectangle(self) -> Rectangle:
249        """
250        Gets the rectangular bounds of the drawing on screen.
251
252        Returns:
253            `Rectangle`: A rectangle defining the drawing's position and size
254            on screen.
255        """
256        return Rectangle(self.top_left, self.drawing.size)
257
258    @cached_property
259    def visible_rectangle(self) -> Rectangle:
260        """
261        Calculate the actual visible bounds of the drawing,
262        ignoring transparent pixels.
263
264        Returns:
265            `Rectangle`: A rectangle defining only the visible (non-transparent)
266            portion of the drawing on screen.
267        """
268        visible = [
269            Coordinate(x, y)
270            for x, y in product(
271                range(ceil(self.drawing.width)),
272                range(ceil(self.drawing.height)),
273            )
274            # `drawing._surface` to ignore the debug background.
275            if self.drawing._surface.get_at((x, y)).a
276        ]
277        if not visible:
278            return Rectangle(Coordinate(0, 0), Size(0, 0))
279
280        min_x = min(c.left for c in visible)
281        min_y = min(c.top for c in visible)
282        max_x = max(c.left for c in visible)
283        max_y = max(c.top for c in visible)
284        return Rectangle(
285            self.top_left + Coordinate(min_x, min_y),
286            Size(max_x - min_x, max_y - min_y),
287        )
288
289    @property
290    def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]:
291        """
292        Gets the pygame surface and coordinate tuple for rendering.
293
294        Returns:
295            `tuple[pygame.Surface, tuple[float, float]]`:
296                A tuple containing the `pygame.Surface` and a tuple of the left
297                and top coordinates (x, y).
298        """
299        return self.drawing.pygame, self.top_left.tuple
300
301    def __add__(self, coord: Coordinate) -> DrawOnScreen:
302        """
303        Shift the drawing by the specified coordinate.
304
305        Args:
306            `coord`: The coordinate to shift the drawing by.
307
308        Returns:
309            `DrawOnScreen`: A new `DrawOnScreen` shifted by the coordinate.
310        """
311        return DrawOnScreen(self.top_left + coord, self.drawing)
312
313
314class Polygon(ABC):
315    """
316    Abstract base class for polygon, defining the common interface/behavior.
317    """
318
319    @cached_property
320    @abstractmethod
321    def points(self) -> tuple[Coordinate, ...]:
322        """
323        Get the points of the polygon.
324
325        Returns:
326            `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects
327                representing the points.
328        """
329
330    @cached_property
331    def bounding_rectangle(self) -> Rectangle:
332        """
333        Get a rectangle bounding the polygon.
334
335        Returns:
336            `Rectangle`: A rectangle bounding the polygon.
337        """
338        min_x = min(c.left for c in self.points)
339        min_y = min(c.top for c in self.points)
340        max_x = max(c.left for c in self.points)
341        max_y = max(c.top for c in self.points)
342        return Rectangle(
343            Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y)
344        )
345
346    @cached_property
347    def _mask(self) -> Mask:
348        return from_surface(self.fill(Rgba(0, 0, 0, 255)).drawing.pygame)
349
350    def fill(self, color: Rgba) -> DrawOnScreen:
351        """
352        Creates a colored `DrawOnScreen` with the provided color.
353
354        Args:
355            `Color`: The color to fill the rectangle.
356
357        Returns:
358            `DrawOnScreen`: A transparent surface matching rectangle dimensions.
359        """
360        rect = self.bounding_rectangle
361        surface = Surface(rect.size.tuple, SRCALPHA)
362        polygon(
363            surface,
364            color.tuple,
365            [(p - rect.top_left).tuple for p in self.points],
366        )
367        return DrawOnScreen(rect.top_left, Drawing(surface))
368
369    def collide(self, poly: Polygon) -> bool:
370        """
371        Checks if this rectangle overlaps with another polygon.
372
373        Args:
374            `poly`: The polygon to check for collision with.
375
376        Returns:
377            `bool`: True if two polygons overlap, False otherwise.
378        """
379        if not self.bounding_rectangle.collide(poly.bounding_rectangle):
380            return False
381        offset = (
382            self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left
383        )
384        return bool(self._mask.overlap(poly._mask, offset.tuple))
385
386    def __contains__(self, coordinate: Coordinate) -> bool:
387        """
388        Checks if a coordinate point lies within this polygon.
389
390        The point is considered inside if it falls in the polygon's bounds,
391        including points on the edges.
392
393        Arguments:
394            `coordinate`: The coordinate point to check
395
396        Returns:
397            `bool`: Whether the coordinate lies within the rectangle.
398        """
399        x, y = (coordinate - self.bounding_rectangle.top_left).tuple
400        width, height = self._mask.get_size()
401        if 0 <= x < width and 0 <= y < height:
402            return bool(self._mask.get_at((x, y)))
403        return False
404
405
406class GenericPolygon(Model, Polygon):
407    """
408    A collection of points that define a closed polygon.
409
410    Attributes:
411        `points`: A tuple of `Coordinate` objects representing the points
412            bounding the polygon.
413    """
414
415    points: tuple[Coordinate, ...]
416
417
418class Rectangle(Model, Polygon):
419    """
420    Represents an immutable rectangle defined by its top left corner and size.
421
422    Attributes:
423        `top_left`: The top-left corner of the rectangle.
424
425        `size`: The dimensions of the rectangle, including its width and height.
426    """
427
428    top_left: Coordinate
429    size: Size
430
431    @property
432    def left(self) -> Pixel:
433        """
434        Gets the leftmost x-coordinate of the drawing on the screen.
435
436        Returns:
437            `Pixel`: The leftmost x-coordinate.
438        """
439        return self.top_left.left
440
441    @property
442    def right(self) -> Pixel:
443        """
444        Gets the rightmost x-coordinate of the drawing on the screen.
445
446        Returns:
447            `Pixel`: The rightmost x-coordinate (left + width).
448        """
449        return self.left + self.size.width
450
451    @property
452    def top(self) -> Pixel:
453        """
454        Gets the topmost y-coordinate of the drawing on the screen.
455
456        Returns:
457            `Pixel`: The topmost y-coordinate.
458        """
459        return self.top_left.top
460
461    @property
462    def bottom(self) -> Pixel:
463        """
464        Gets the bottommost y-coordinate of the drawing on the screen.
465
466        Returns:
467            `Pixel`: The bottommost y-coordinate (top + height).
468        """
469        return self.top + self.size.height
470
471    @property
472    def top_right(self) -> Coordinate:
473        """
474        Gets the top-right coordinate of the drawing on the screen.
475
476        Returns:
477            `Coordinate`: The top-right coordinate.
478        """
479        return Coordinate(self.right, self.top)
480
481    @property
482    def bottom_left(self) -> Coordinate:
483        """
484        Gets the bottom-left coordinate of the drawing on the screen.
485
486        Returns:
487            `Coordinate`: The bottom-left coordinate.
488        """
489        return Coordinate(self.left, self.bottom)
490
491    @property
492    def bottom_right(self) -> Coordinate:
493        """
494        Gets the bottom-right coordinate of the drawing on the screen.
495
496        Returns:
497            `Coordinate`: The bottom-right coordinate.
498        """
499        return Coordinate(self.right, self.bottom)
500
501    @property
502    def top_center(self) -> Coordinate:
503        """
504        Gets the center point of the top edge of the drawing on the screen.
505
506        Returns:
507            `Coordinate`: The top-center coordinate.
508        """
509        return Coordinate(self.left + self.size.width / 2, self.top)
510
511    @property
512    def bottom_center(self) -> Coordinate:
513        """
514        Gets the center point of the bottom edge of the drawing on the screen.
515
516        Returns:
517            `Coordinate`: The bottom-center coordinate.
518        """
519        return Coordinate(self.left + self.size.width / 2, self.bottom)
520
521    @property
522    def center_left(self) -> Coordinate:
523        """
524        Gets the center point of the left edge of the drawing on the screen.
525
526        Returns:
527            `Coordinate`: The left-center coordinate.
528        """
529        return Coordinate(self.left, self.top + self.size.height / 2)
530
531    @property
532    def center_right(self) -> Coordinate:
533        """
534        Gets the center point of the right edge of the drawing on the screen.
535
536        Returns:
537            `Coordinate`: The right-center coordinate.
538        """
539        return Coordinate(self.right, self.top + self.size.height / 2)
540
541    @property
542    def center(self) -> Coordinate:
543        """
544        Gets the center point of the drawing on the screen.
545
546        Returns:
547            `Coordinate`: The center coordinate of the drawing.
548        """
549        width, height = self.size.tuple
550        return Coordinate(self.left + width / 2, self.top + height / 2)
551
552    @property
553    def points(self) -> tuple[Coordinate, ...]:
554        """
555        Get the coordinates of the corners of the rectangle.
556
557        Returns:
558            `tuple[Coordinate, ...]`: The coordinates of the corners
559                of the rectangle.
560        """
561        return (
562            self.top_left,
563            self.top_right,
564            self.bottom_right,
565            self.bottom_left,
566        )
567
568    @override
569    def collide(self, poly: Polygon) -> bool:
570        if isinstance(poly, Rectangle):
571            return (
572                self.top_left.left < poly.top_right.left
573                and self.top_right.left > poly.top_left.left
574                and self.top_left.top < poly.bottom_right.top
575                and self.bottom_right.top > poly.top_left.top
576            )
577        return super().collide(poly)
578
579    @override
580    def __contains__(self, coordinate: Coordinate) -> bool:
581        return (
582            self.left < coordinate.left < self.right
583            and self.top < coordinate.top < self.bottom
584        )
class Coordinate(abc.ABCMeta, typing.Generic[T]):
26class Coordinate(Model):
27    """
28    Represents a 2D coordinate with immutability and provides methods
29    for scaling and shifting coordinates.
30
31    Attributes:
32        `left`: The horizontal position of the coordinate, measured by
33            the number of pixels from the left edge of the game window.
34
35        `top`: The vertical position of the coordinate, measured by
36            the number of pixels from the top edge of the game window.
37    """
38
39    left: Pixel
40    top: Pixel
41
42    def __mul__(self, scale: float) -> Coordinate:
43        """
44        Scales the current `Coordinate` values (left and top) by a given factor
45        and returns a new `Coordinate` with the scaled values rounded up to the
46        nearest integer.
47
48        Round up so that drawings won't leave tiny, black gaps after scaled.
49
50        Args:
51            `scale`: The scaling factor to multiply the left and
52                top values of the `Coordinate`.
53
54        Returns:
55            `Coordinate`: A new `Coordinate` with the scaled and rounded values.
56        """
57        return Coordinate(ceil(self.left * scale), ceil(self.top * scale))
58
59    def __sub__(self, other: Coordinate) -> Coordinate:
60        return Coordinate(self.left - other.left, self.top - other.top)
61
62    @singledispatchmethod
63    def __add__(self, offset: DirectionalOffset | Coordinate) -> Coordinate:
64        """
65        Shifts the coordinate in the specified direction by a given offset.
66        Supports both orthogonal and diagonal directions.
67
68        Or add two coordinates together.
69
70        For diagonal directions, the offset is divided proportionally.
71        For example, an offset of `sqrt(2)` in `UP_LEFT` direction shifts
72        the coordinate `Pixel(1)` in both `UP` and `LEFT` directions.
73
74        Args:
75            `offset`: A `DirectionalOffset` representing the direction
76                and offset, or `Coordinate` to add to the current `Coordinate`.
77
78        Returns:
79            `Coordinate`: A new coordinate shifted by the specified offset in
80            the given direction.
81        """
82        raise NotImplementedError(f"Non-addable {offset=}")
83
84    @property
85    def tuple(self) -> tuple[Pixel, Pixel]:
86        """
87        Gets the coordinates as a tuple.
88
89        Returns:
90            `tuple[Pixel, Pixel]`: A tuple containing the left and top
91                values in that order.
92        """
93        return self.left, self.top

Represents a 2D coordinate with immutability and provides methods for scaling and shifting coordinates.

Attributes:
  • left: The horizontal position of the coordinate, measured by the number of pixels from the left edge of the game window.
  • top: The vertical position of the coordinate, measured by the number of pixels from the top edge of the game window.
Coordinate(left: Pixel, top: Pixel)
tuple: 'tuple[Pixel, Pixel]'
84    @property
85    def tuple(self) -> tuple[Pixel, Pixel]:
86        """
87        Gets the coordinates as a tuple.
88
89        Returns:
90            `tuple[Pixel, Pixel]`: A tuple containing the left and top
91                values in that order.
92        """
93        return self.left, self.top

Gets the coordinates as a tuple.

Returns:

tuple[Pixel, Pixel]: A tuple containing the left and top values in that order.

@cached(lambda: config().resource.drawing_cache_size, lambda resource: resource if isinstance(resource, Path) else None)
class Drawing(abc.ABCMeta, typing.Generic[T]):
126@cached(
127    lambda: config().resource.drawing_cache_size,
128    lambda resource: resource if isinstance(resource, Path) else None,
129)
130class Drawing(Model):
131    """
132    Represents a drawable element and provides methods for accessing its size
133    and dimensions.
134
135    This class loads a surface from a file or directly accepts a
136    `pygame.Surface` as input.
137    It provides properties to access surface dimensions and size and methods to
138    crop and scale the surface.
139    """
140
141    resource: Path | Surface = field(repr=False)
142    _: KW_ONLY = field()
143    _surface: Surface = internal_field(
144        lambda self: (
145            self.resource
146            if isinstance(self.resource, Surface)
147            else load(self.resource).convert_alpha()
148        )
149    )
150
151    def __repr__(self) -> str:
152        """
153        A string representation of the `Drawing` object.
154
155        Returns:
156            `str`: A string representation of the `Drawing` object.
157        """
158        return f"Drawing({self.size})"
159
160    @property
161    def width(self) -> Pixel:
162        """
163        Gets the width of the surface.
164
165        Returns:
166            `Pixel`: The width of the surface in pixel measurement.
167        """
168        return self._surface.get_width()
169
170    @property
171    def height(self) -> Pixel:
172        """
173        Gets the height of the surface.
174
175        Returns:
176            `Pixel`: The height of the surface in pixel measurement.
177        """
178        return self._surface.get_height()
179
180    @property
181    def size(self) -> Size:
182        """
183        Gets the size of an object as a combination of its width and height
184
185        Returns:
186            `Size`: A Size object containing the width and height of the object.
187        """
188        return Size(self.width, self.height)
189
190    @property
191    def pygame(self) -> Surface:
192        """
193        Gets the current `pygame.Surface` for the object.
194
195        Returns:
196            `pygame.Surface`: The underlying `pygame.Surface`.
197        """
198        return self._debug_surface or self._surface
199
200    def crop(self, top_left: Coordinate, size: Size) -> Drawing:
201        """
202        Crops a rectangular portion of the drawing specified by the
203        top-left corner and the size.
204
205        The method extracts a subsection of the drawing based on the provided
206        coordinates and dimensions and returns a new `Drawing` instance.
207        The original drawing remains unchanged.
208
209        Args:
210            `top_left`: The top-left coordinate of the rectangle to be cropped.
211
212            `size`: The width and height of the rectangle to be cropped.
213
214        Returns:
215            `Drawing`: A new `Drawing` instance representing the cropped area.
216        """
217        left, top = top_left.tuple
218        width, height = size.tuple
219        return Drawing(self._surface.subsurface((left, top, width, height)))
220
221    @cached_property
222    def _debug_surface(self) -> Surface | None:
223        if not (debug := config().debug):
224            return None
225        surface = Surface(self.size.tuple, SRCALPHA)
226        surface.fill(debug.drawing_background_color.tuple)
227        surface.blit(self._surface, (0, 0))
228        return surface

Represents a drawable element and provides methods for accessing its size and dimensions.

This class loads a surface from a file or directly accepts a pygame.Surface as input. It provides properties to access surface dimensions and size and methods to crop and scale the surface.

Drawing( resource: pathlib.Path | pygame.surface.Surface, *, _surface: pygame.surface.Surface = _Init(init=<function Drawing.<lambda>>))
resource: pathlib.Path | pygame.surface.Surface
width: nextrpg.core.Pixel
160    @property
161    def width(self) -> Pixel:
162        """
163        Gets the width of the surface.
164
165        Returns:
166            `Pixel`: The width of the surface in pixel measurement.
167        """
168        return self._surface.get_width()

Gets the width of the surface.

Returns:

Pixel: The width of the surface in pixel measurement.

height: nextrpg.core.Pixel
170    @property
171    def height(self) -> Pixel:
172        """
173        Gets the height of the surface.
174
175        Returns:
176            `Pixel`: The height of the surface in pixel measurement.
177        """
178        return self._surface.get_height()

Gets the height of the surface.

Returns:

Pixel: The height of the surface in pixel measurement.

size: nextrpg.core.Size
180    @property
181    def size(self) -> Size:
182        """
183        Gets the size of an object as a combination of its width and height
184
185        Returns:
186            `Size`: A Size object containing the width and height of the object.
187        """
188        return Size(self.width, self.height)

Gets the size of an object as a combination of its width and height

Returns:

Size: A Size object containing the width and height of the object.

pygame: pygame.surface.Surface
190    @property
191    def pygame(self) -> Surface:
192        """
193        Gets the current `pygame.Surface` for the object.
194
195        Returns:
196            `pygame.Surface`: The underlying `pygame.Surface`.
197        """
198        return self._debug_surface or self._surface

Gets the current pygame.Surface for the object.

Returns:

pygame.Surface: The underlying pygame.Surface.

def crop( self, top_left: Coordinate, size: nextrpg.core.Size) -> Drawing:
200    def crop(self, top_left: Coordinate, size: Size) -> Drawing:
201        """
202        Crops a rectangular portion of the drawing specified by the
203        top-left corner and the size.
204
205        The method extracts a subsection of the drawing based on the provided
206        coordinates and dimensions and returns a new `Drawing` instance.
207        The original drawing remains unchanged.
208
209        Args:
210            `top_left`: The top-left coordinate of the rectangle to be cropped.
211
212            `size`: The width and height of the rectangle to be cropped.
213
214        Returns:
215            `Drawing`: A new `Drawing` instance representing the cropped area.
216        """
217        left, top = top_left.tuple
218        width, height = size.tuple
219        return Drawing(self._surface.subsurface((left, top, width, height)))

Crops a rectangular portion of the drawing specified by the top-left corner and the size.

The method extracts a subsection of the drawing based on the provided coordinates and dimensions and returns a new Drawing instance. The original drawing remains unchanged.

Arguments:
  • top_left: The top-left coordinate of the rectangle to be cropped.
  • size: The width and height of the rectangle to be cropped.
Returns:

Drawing: A new Drawing instance representing the cropped area.

class DrawOnScreen(abc.ABCMeta, typing.Generic[T]):
231class DrawOnScreen(Model):
232    """
233    Represents a drawable element positioned on the screen with its coordinates.
234
235    This immutable class combines a drawing with its position and provides
236    properties to access various coordinates and dimensions of the drawing
237    on the screen.
238
239    Attributes:
240        `top_left`: The top-left position of the drawing on the screen.
241
242        `drawing`: The drawable element to be displayed on the screen.
243    """
244
245    top_left: Coordinate
246    drawing: Drawing
247
248    @property
249    def rectangle(self) -> Rectangle:
250        """
251        Gets the rectangular bounds of the drawing on screen.
252
253        Returns:
254            `Rectangle`: A rectangle defining the drawing's position and size
255            on screen.
256        """
257        return Rectangle(self.top_left, self.drawing.size)
258
259    @cached_property
260    def visible_rectangle(self) -> Rectangle:
261        """
262        Calculate the actual visible bounds of the drawing,
263        ignoring transparent pixels.
264
265        Returns:
266            `Rectangle`: A rectangle defining only the visible (non-transparent)
267            portion of the drawing on screen.
268        """
269        visible = [
270            Coordinate(x, y)
271            for x, y in product(
272                range(ceil(self.drawing.width)),
273                range(ceil(self.drawing.height)),
274            )
275            # `drawing._surface` to ignore the debug background.
276            if self.drawing._surface.get_at((x, y)).a
277        ]
278        if not visible:
279            return Rectangle(Coordinate(0, 0), Size(0, 0))
280
281        min_x = min(c.left for c in visible)
282        min_y = min(c.top for c in visible)
283        max_x = max(c.left for c in visible)
284        max_y = max(c.top for c in visible)
285        return Rectangle(
286            self.top_left + Coordinate(min_x, min_y),
287            Size(max_x - min_x, max_y - min_y),
288        )
289
290    @property
291    def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]:
292        """
293        Gets the pygame surface and coordinate tuple for rendering.
294
295        Returns:
296            `tuple[pygame.Surface, tuple[float, float]]`:
297                A tuple containing the `pygame.Surface` and a tuple of the left
298                and top coordinates (x, y).
299        """
300        return self.drawing.pygame, self.top_left.tuple
301
302    def __add__(self, coord: Coordinate) -> DrawOnScreen:
303        """
304        Shift the drawing by the specified coordinate.
305
306        Args:
307            `coord`: The coordinate to shift the drawing by.
308
309        Returns:
310            `DrawOnScreen`: A new `DrawOnScreen` shifted by the coordinate.
311        """
312        return DrawOnScreen(self.top_left + coord, self.drawing)

Represents a drawable element positioned on the screen with its coordinates.

This immutable class combines a drawing with its position and provides properties to access various coordinates and dimensions of the drawing on the screen.

Attributes:
  • top_left: The top-left position of the drawing on the screen.
  • drawing: The drawable element to be displayed on the screen.
DrawOnScreen( top_left: Coordinate, drawing: Drawing)
top_left: Coordinate
drawing: Drawing
rectangle: Rectangle
248    @property
249    def rectangle(self) -> Rectangle:
250        """
251        Gets the rectangular bounds of the drawing on screen.
252
253        Returns:
254            `Rectangle`: A rectangle defining the drawing's position and size
255            on screen.
256        """
257        return Rectangle(self.top_left, self.drawing.size)

Gets the rectangular bounds of the drawing on screen.

Returns:

Rectangle: A rectangle defining the drawing's position and size on screen.

visible_rectangle: Rectangle
259    @cached_property
260    def visible_rectangle(self) -> Rectangle:
261        """
262        Calculate the actual visible bounds of the drawing,
263        ignoring transparent pixels.
264
265        Returns:
266            `Rectangle`: A rectangle defining only the visible (non-transparent)
267            portion of the drawing on screen.
268        """
269        visible = [
270            Coordinate(x, y)
271            for x, y in product(
272                range(ceil(self.drawing.width)),
273                range(ceil(self.drawing.height)),
274            )
275            # `drawing._surface` to ignore the debug background.
276            if self.drawing._surface.get_at((x, y)).a
277        ]
278        if not visible:
279            return Rectangle(Coordinate(0, 0), Size(0, 0))
280
281        min_x = min(c.left for c in visible)
282        min_y = min(c.top for c in visible)
283        max_x = max(c.left for c in visible)
284        max_y = max(c.top for c in visible)
285        return Rectangle(
286            self.top_left + Coordinate(min_x, min_y),
287            Size(max_x - min_x, max_y - min_y),
288        )

Calculate the actual visible bounds of the drawing, ignoring transparent pixels.

Returns:

Rectangle: A rectangle defining only the visible (non-transparent) portion of the drawing on screen.

pygame: tuple[pygame.surface.Surface, tuple[Pixel, Pixel]]
290    @property
291    def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]:
292        """
293        Gets the pygame surface and coordinate tuple for rendering.
294
295        Returns:
296            `tuple[pygame.Surface, tuple[float, float]]`:
297                A tuple containing the `pygame.Surface` and a tuple of the left
298                and top coordinates (x, y).
299        """
300        return self.drawing.pygame, self.top_left.tuple

Gets the pygame surface and coordinate tuple for rendering.

Returns:

tuple[pygame.Surface, tuple[float, float]]: A tuple containing the pygame.Surface and a tuple of the left and top coordinates (x, y).

class Polygon(abc.ABC):
315class Polygon(ABC):
316    """
317    Abstract base class for polygon, defining the common interface/behavior.
318    """
319
320    @cached_property
321    @abstractmethod
322    def points(self) -> tuple[Coordinate, ...]:
323        """
324        Get the points of the polygon.
325
326        Returns:
327            `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects
328                representing the points.
329        """
330
331    @cached_property
332    def bounding_rectangle(self) -> Rectangle:
333        """
334        Get a rectangle bounding the polygon.
335
336        Returns:
337            `Rectangle`: A rectangle bounding the polygon.
338        """
339        min_x = min(c.left for c in self.points)
340        min_y = min(c.top for c in self.points)
341        max_x = max(c.left for c in self.points)
342        max_y = max(c.top for c in self.points)
343        return Rectangle(
344            Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y)
345        )
346
347    @cached_property
348    def _mask(self) -> Mask:
349        return from_surface(self.fill(Rgba(0, 0, 0, 255)).drawing.pygame)
350
351    def fill(self, color: Rgba) -> DrawOnScreen:
352        """
353        Creates a colored `DrawOnScreen` with the provided color.
354
355        Args:
356            `Color`: The color to fill the rectangle.
357
358        Returns:
359            `DrawOnScreen`: A transparent surface matching rectangle dimensions.
360        """
361        rect = self.bounding_rectangle
362        surface = Surface(rect.size.tuple, SRCALPHA)
363        polygon(
364            surface,
365            color.tuple,
366            [(p - rect.top_left).tuple for p in self.points],
367        )
368        return DrawOnScreen(rect.top_left, Drawing(surface))
369
370    def collide(self, poly: Polygon) -> bool:
371        """
372        Checks if this rectangle overlaps with another polygon.
373
374        Args:
375            `poly`: The polygon to check for collision with.
376
377        Returns:
378            `bool`: True if two polygons overlap, False otherwise.
379        """
380        if not self.bounding_rectangle.collide(poly.bounding_rectangle):
381            return False
382        offset = (
383            self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left
384        )
385        return bool(self._mask.overlap(poly._mask, offset.tuple))
386
387    def __contains__(self, coordinate: Coordinate) -> bool:
388        """
389        Checks if a coordinate point lies within this polygon.
390
391        The point is considered inside if it falls in the polygon's bounds,
392        including points on the edges.
393
394        Arguments:
395            `coordinate`: The coordinate point to check
396
397        Returns:
398            `bool`: Whether the coordinate lies within the rectangle.
399        """
400        x, y = (coordinate - self.bounding_rectangle.top_left).tuple
401        width, height = self._mask.get_size()
402        if 0 <= x < width and 0 <= y < height:
403            return bool(self._mask.get_at((x, y)))
404        return False

Abstract base class for polygon, defining the common interface/behavior.

points: tuple[Coordinate, ...]
320    @cached_property
321    @abstractmethod
322    def points(self) -> tuple[Coordinate, ...]:
323        """
324        Get the points of the polygon.
325
326        Returns:
327            `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects
328                representing the points.
329        """

Get the points of the polygon.

Returns:

tuple[Coordinate, ...]: A tuple of Coordinate objects representing the points.

bounding_rectangle: Rectangle
331    @cached_property
332    def bounding_rectangle(self) -> Rectangle:
333        """
334        Get a rectangle bounding the polygon.
335
336        Returns:
337            `Rectangle`: A rectangle bounding the polygon.
338        """
339        min_x = min(c.left for c in self.points)
340        min_y = min(c.top for c in self.points)
341        max_x = max(c.left for c in self.points)
342        max_y = max(c.top for c in self.points)
343        return Rectangle(
344            Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y)
345        )

Get a rectangle bounding the polygon.

Returns:

Rectangle: A rectangle bounding the polygon.

def fill(self, color: nextrpg.core.Rgba) -> DrawOnScreen:
351    def fill(self, color: Rgba) -> DrawOnScreen:
352        """
353        Creates a colored `DrawOnScreen` with the provided color.
354
355        Args:
356            `Color`: The color to fill the rectangle.
357
358        Returns:
359            `DrawOnScreen`: A transparent surface matching rectangle dimensions.
360        """
361        rect = self.bounding_rectangle
362        surface = Surface(rect.size.tuple, SRCALPHA)
363        polygon(
364            surface,
365            color.tuple,
366            [(p - rect.top_left).tuple for p in self.points],
367        )
368        return DrawOnScreen(rect.top_left, Drawing(surface))

Creates a colored DrawOnScreen with the provided color.

Arguments:
  • Color: The color to fill the rectangle.
Returns:

DrawOnScreen: A transparent surface matching rectangle dimensions.

def collide(self, poly: Polygon) -> bool:
370    def collide(self, poly: Polygon) -> bool:
371        """
372        Checks if this rectangle overlaps with another polygon.
373
374        Args:
375            `poly`: The polygon to check for collision with.
376
377        Returns:
378            `bool`: True if two polygons overlap, False otherwise.
379        """
380        if not self.bounding_rectangle.collide(poly.bounding_rectangle):
381            return False
382        offset = (
383            self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left
384        )
385        return bool(self._mask.overlap(poly._mask, offset.tuple))

Checks if this rectangle overlaps with another polygon.

Arguments:
  • poly: The polygon to check for collision with.
Returns:

bool: True if two polygons overlap, False otherwise.

class GenericPolygon(abc.ABCMeta, typing.Generic[T]):
407class GenericPolygon(Model, Polygon):
408    """
409    A collection of points that define a closed polygon.
410
411    Attributes:
412        `points`: A tuple of `Coordinate` objects representing the points
413            bounding the polygon.
414    """
415
416    points: tuple[Coordinate, ...]

A collection of points that define a closed polygon.

Attributes:
  • points: A tuple of Coordinate objects representing the points bounding the polygon.
GenericPolygon( points: tuple[Coordinate, ...] = <functools.cached_property object>)
points: tuple[Coordinate, ...]
320    @cached_property
321    @abstractmethod
322    def points(self) -> tuple[Coordinate, ...]:
323        """
324        Get the points of the polygon.
325
326        Returns:
327            `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects
328                representing the points.
329        """

Get the points of the polygon.

Returns:

tuple[Coordinate, ...]: A tuple of Coordinate objects representing the points.

class Rectangle(abc.ABCMeta, typing.Generic[T]):
419class Rectangle(Model, Polygon):
420    """
421    Represents an immutable rectangle defined by its top left corner and size.
422
423    Attributes:
424        `top_left`: The top-left corner of the rectangle.
425
426        `size`: The dimensions of the rectangle, including its width and height.
427    """
428
429    top_left: Coordinate
430    size: Size
431
432    @property
433    def left(self) -> Pixel:
434        """
435        Gets the leftmost x-coordinate of the drawing on the screen.
436
437        Returns:
438            `Pixel`: The leftmost x-coordinate.
439        """
440        return self.top_left.left
441
442    @property
443    def right(self) -> Pixel:
444        """
445        Gets the rightmost x-coordinate of the drawing on the screen.
446
447        Returns:
448            `Pixel`: The rightmost x-coordinate (left + width).
449        """
450        return self.left + self.size.width
451
452    @property
453    def top(self) -> Pixel:
454        """
455        Gets the topmost y-coordinate of the drawing on the screen.
456
457        Returns:
458            `Pixel`: The topmost y-coordinate.
459        """
460        return self.top_left.top
461
462    @property
463    def bottom(self) -> Pixel:
464        """
465        Gets the bottommost y-coordinate of the drawing on the screen.
466
467        Returns:
468            `Pixel`: The bottommost y-coordinate (top + height).
469        """
470        return self.top + self.size.height
471
472    @property
473    def top_right(self) -> Coordinate:
474        """
475        Gets the top-right coordinate of the drawing on the screen.
476
477        Returns:
478            `Coordinate`: The top-right coordinate.
479        """
480        return Coordinate(self.right, self.top)
481
482    @property
483    def bottom_left(self) -> Coordinate:
484        """
485        Gets the bottom-left coordinate of the drawing on the screen.
486
487        Returns:
488            `Coordinate`: The bottom-left coordinate.
489        """
490        return Coordinate(self.left, self.bottom)
491
492    @property
493    def bottom_right(self) -> Coordinate:
494        """
495        Gets the bottom-right coordinate of the drawing on the screen.
496
497        Returns:
498            `Coordinate`: The bottom-right coordinate.
499        """
500        return Coordinate(self.right, self.bottom)
501
502    @property
503    def top_center(self) -> Coordinate:
504        """
505        Gets the center point of the top edge of the drawing on the screen.
506
507        Returns:
508            `Coordinate`: The top-center coordinate.
509        """
510        return Coordinate(self.left + self.size.width / 2, self.top)
511
512    @property
513    def bottom_center(self) -> Coordinate:
514        """
515        Gets the center point of the bottom edge of the drawing on the screen.
516
517        Returns:
518            `Coordinate`: The bottom-center coordinate.
519        """
520        return Coordinate(self.left + self.size.width / 2, self.bottom)
521
522    @property
523    def center_left(self) -> Coordinate:
524        """
525        Gets the center point of the left edge of the drawing on the screen.
526
527        Returns:
528            `Coordinate`: The left-center coordinate.
529        """
530        return Coordinate(self.left, self.top + self.size.height / 2)
531
532    @property
533    def center_right(self) -> Coordinate:
534        """
535        Gets the center point of the right edge of the drawing on the screen.
536
537        Returns:
538            `Coordinate`: The right-center coordinate.
539        """
540        return Coordinate(self.right, self.top + self.size.height / 2)
541
542    @property
543    def center(self) -> Coordinate:
544        """
545        Gets the center point of the drawing on the screen.
546
547        Returns:
548            `Coordinate`: The center coordinate of the drawing.
549        """
550        width, height = self.size.tuple
551        return Coordinate(self.left + width / 2, self.top + height / 2)
552
553    @property
554    def points(self) -> tuple[Coordinate, ...]:
555        """
556        Get the coordinates of the corners of the rectangle.
557
558        Returns:
559            `tuple[Coordinate, ...]`: The coordinates of the corners
560                of the rectangle.
561        """
562        return (
563            self.top_left,
564            self.top_right,
565            self.bottom_right,
566            self.bottom_left,
567        )
568
569    @override
570    def collide(self, poly: Polygon) -> bool:
571        if isinstance(poly, Rectangle):
572            return (
573                self.top_left.left < poly.top_right.left
574                and self.top_right.left > poly.top_left.left
575                and self.top_left.top < poly.bottom_right.top
576                and self.bottom_right.top > poly.top_left.top
577            )
578        return super().collide(poly)
579
580    @override
581    def __contains__(self, coordinate: Coordinate) -> bool:
582        return (
583            self.left < coordinate.left < self.right
584            and self.top < coordinate.top < self.bottom
585        )

Represents an immutable rectangle defined by its top left corner and size.

Attributes:
  • top_left: The top-left corner of the rectangle.
  • size: The dimensions of the rectangle, including its width and height.
Rectangle(top_left: Coordinate, size: nextrpg.core.Size)
top_left: Coordinate
left: nextrpg.core.Pixel
432    @property
433    def left(self) -> Pixel:
434        """
435        Gets the leftmost x-coordinate of the drawing on the screen.
436
437        Returns:
438            `Pixel`: The leftmost x-coordinate.
439        """
440        return self.top_left.left

Gets the leftmost x-coordinate of the drawing on the screen.

Returns:

Pixel: The leftmost x-coordinate.

right: nextrpg.core.Pixel
442    @property
443    def right(self) -> Pixel:
444        """
445        Gets the rightmost x-coordinate of the drawing on the screen.
446
447        Returns:
448            `Pixel`: The rightmost x-coordinate (left + width).
449        """
450        return self.left + self.size.width

Gets the rightmost x-coordinate of the drawing on the screen.

Returns:

Pixel: The rightmost x-coordinate (left + width).

top: nextrpg.core.Pixel
452    @property
453    def top(self) -> Pixel:
454        """
455        Gets the topmost y-coordinate of the drawing on the screen.
456
457        Returns:
458            `Pixel`: The topmost y-coordinate.
459        """
460        return self.top_left.top

Gets the topmost y-coordinate of the drawing on the screen.

Returns:

Pixel: The topmost y-coordinate.

bottom: nextrpg.core.Pixel
462    @property
463    def bottom(self) -> Pixel:
464        """
465        Gets the bottommost y-coordinate of the drawing on the screen.
466
467        Returns:
468            `Pixel`: The bottommost y-coordinate (top + height).
469        """
470        return self.top + self.size.height

Gets the bottommost y-coordinate of the drawing on the screen.

Returns:

Pixel: The bottommost y-coordinate (top + height).

top_right: Coordinate
472    @property
473    def top_right(self) -> Coordinate:
474        """
475        Gets the top-right coordinate of the drawing on the screen.
476
477        Returns:
478            `Coordinate`: The top-right coordinate.
479        """
480        return Coordinate(self.right, self.top)

Gets the top-right coordinate of the drawing on the screen.

Returns:

Coordinate: The top-right coordinate.

bottom_left: Coordinate
482    @property
483    def bottom_left(self) -> Coordinate:
484        """
485        Gets the bottom-left coordinate of the drawing on the screen.
486
487        Returns:
488            `Coordinate`: The bottom-left coordinate.
489        """
490        return Coordinate(self.left, self.bottom)

Gets the bottom-left coordinate of the drawing on the screen.

Returns:

Coordinate: The bottom-left coordinate.

bottom_right: Coordinate
492    @property
493    def bottom_right(self) -> Coordinate:
494        """
495        Gets the bottom-right coordinate of the drawing on the screen.
496
497        Returns:
498            `Coordinate`: The bottom-right coordinate.
499        """
500        return Coordinate(self.right, self.bottom)

Gets the bottom-right coordinate of the drawing on the screen.

Returns:

Coordinate: The bottom-right coordinate.

top_center: Coordinate
502    @property
503    def top_center(self) -> Coordinate:
504        """
505        Gets the center point of the top edge of the drawing on the screen.
506
507        Returns:
508            `Coordinate`: The top-center coordinate.
509        """
510        return Coordinate(self.left + self.size.width / 2, self.top)

Gets the center point of the top edge of the drawing on the screen.

Returns:

Coordinate: The top-center coordinate.

bottom_center: Coordinate
512    @property
513    def bottom_center(self) -> Coordinate:
514        """
515        Gets the center point of the bottom edge of the drawing on the screen.
516
517        Returns:
518            `Coordinate`: The bottom-center coordinate.
519        """
520        return Coordinate(self.left + self.size.width / 2, self.bottom)

Gets the center point of the bottom edge of the drawing on the screen.

Returns:

Coordinate: The bottom-center coordinate.

center_left: Coordinate
522    @property
523    def center_left(self) -> Coordinate:
524        """
525        Gets the center point of the left edge of the drawing on the screen.
526
527        Returns:
528            `Coordinate`: The left-center coordinate.
529        """
530        return Coordinate(self.left, self.top + self.size.height / 2)

Gets the center point of the left edge of the drawing on the screen.

Returns:

Coordinate: The left-center coordinate.

center_right: Coordinate
532    @property
533    def center_right(self) -> Coordinate:
534        """
535        Gets the center point of the right edge of the drawing on the screen.
536
537        Returns:
538            `Coordinate`: The right-center coordinate.
539        """
540        return Coordinate(self.right, self.top + self.size.height / 2)

Gets the center point of the right edge of the drawing on the screen.

Returns:

Coordinate: The right-center coordinate.

center: Coordinate
542    @property
543    def center(self) -> Coordinate:
544        """
545        Gets the center point of the drawing on the screen.
546
547        Returns:
548            `Coordinate`: The center coordinate of the drawing.
549        """
550        width, height = self.size.tuple
551        return Coordinate(self.left + width / 2, self.top + height / 2)

Gets the center point of the drawing on the screen.

Returns:

Coordinate: The center coordinate of the drawing.

points: tuple[Coordinate, ...]
553    @property
554    def points(self) -> tuple[Coordinate, ...]:
555        """
556        Get the coordinates of the corners of the rectangle.
557
558        Returns:
559            `tuple[Coordinate, ...]`: The coordinates of the corners
560                of the rectangle.
561        """
562        return (
563            self.top_left,
564            self.top_right,
565            self.bottom_right,
566            self.bottom_left,
567        )

Get the coordinates of the corners of the rectangle.

Returns:

tuple[Coordinate, ...]: The coordinates of the corners of the rectangle.

@override
def collide(self, poly: Polygon) -> bool:
569    @override
570    def collide(self, poly: Polygon) -> bool:
571        if isinstance(poly, Rectangle):
572            return (
573                self.top_left.left < poly.top_right.left
574                and self.top_right.left > poly.top_left.left
575                and self.top_left.top < poly.bottom_right.top
576                and self.bottom_right.top > poly.top_left.top
577            )
578        return super().collide(poly)

Checks if this rectangle overlaps with another polygon.

Arguments:
  • poly: The polygon to check for collision with.
Returns:

bool: True if two polygons overlap, False otherwise.

Inherited Members
Polygon
bounding_rectangle
fill