mima-engine 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. mima/__init__.py +4 -0
  2. mima/backend/__init__.py +1 -0
  3. mima/backend/pygame_assets.py +401 -0
  4. mima/backend/pygame_audio.py +78 -0
  5. mima/backend/pygame_backend.py +603 -0
  6. mima/backend/pygame_camera.py +63 -0
  7. mima/backend/pygame_events.py +695 -0
  8. mima/backend/touch_control_scheme_a.py +126 -0
  9. mima/backend/touch_control_scheme_b.py +132 -0
  10. mima/core/__init__.py +0 -0
  11. mima/core/collision.py +325 -0
  12. mima/core/database.py +58 -0
  13. mima/core/engine.py +367 -0
  14. mima/core/mode_engine.py +81 -0
  15. mima/core/scene_engine.py +81 -0
  16. mima/integrated/__init__.py +0 -0
  17. mima/integrated/entity.py +183 -0
  18. mima/integrated/layered_map.py +351 -0
  19. mima/integrated/sprite.py +156 -0
  20. mima/layered/__init__.py +0 -0
  21. mima/layered/assets.py +56 -0
  22. mima/layered/scene.py +415 -0
  23. mima/layered/shape.py +99 -0
  24. mima/layered/shaped_sprite.py +78 -0
  25. mima/layered/virtual_input.py +302 -0
  26. mima/maps/__init__.py +0 -0
  27. mima/maps/template.py +71 -0
  28. mima/maps/tile.py +20 -0
  29. mima/maps/tile_animation.py +7 -0
  30. mima/maps/tile_info.py +10 -0
  31. mima/maps/tile_layer.py +52 -0
  32. mima/maps/tiled/__init__.py +0 -0
  33. mima/maps/tiled/tiled_layer.py +48 -0
  34. mima/maps/tiled/tiled_map.py +95 -0
  35. mima/maps/tiled/tiled_object.py +79 -0
  36. mima/maps/tiled/tiled_objectgroup.py +25 -0
  37. mima/maps/tiled/tiled_template.py +49 -0
  38. mima/maps/tiled/tiled_tile.py +90 -0
  39. mima/maps/tiled/tiled_tileset.py +51 -0
  40. mima/maps/tilemap.py +216 -0
  41. mima/maps/tileset.py +39 -0
  42. mima/maps/tileset_info.py +9 -0
  43. mima/maps/transition_map.py +146 -0
  44. mima/objects/__init__.py +0 -0
  45. mima/objects/animated_sprite.py +217 -0
  46. mima/objects/attribute_effect.py +26 -0
  47. mima/objects/attributes.py +126 -0
  48. mima/objects/creature.py +384 -0
  49. mima/objects/dynamic.py +206 -0
  50. mima/objects/effects/__init__.py +0 -0
  51. mima/objects/effects/colorize_screen.py +60 -0
  52. mima/objects/effects/debug_box.py +133 -0
  53. mima/objects/effects/light.py +103 -0
  54. mima/objects/effects/show_sprite.py +50 -0
  55. mima/objects/effects/walking_on_grass.py +70 -0
  56. mima/objects/effects/walking_on_water.py +57 -0
  57. mima/objects/loader.py +111 -0
  58. mima/objects/projectile.py +111 -0
  59. mima/objects/sprite.py +116 -0
  60. mima/objects/world/__init__.py +0 -0
  61. mima/objects/world/color_gate.py +67 -0
  62. mima/objects/world/color_switch.py +101 -0
  63. mima/objects/world/container.py +175 -0
  64. mima/objects/world/floor_switch.py +109 -0
  65. mima/objects/world/gate.py +178 -0
  66. mima/objects/world/light_source.py +121 -0
  67. mima/objects/world/logic_gate.py +157 -0
  68. mima/objects/world/movable.py +399 -0
  69. mima/objects/world/oneway.py +195 -0
  70. mima/objects/world/pickup.py +157 -0
  71. mima/objects/world/switch.py +179 -0
  72. mima/objects/world/teleport.py +308 -0
  73. mima/py.typed +0 -0
  74. mima/scripts/__init__.py +2 -0
  75. mima/scripts/command.py +38 -0
  76. mima/scripts/commands/__init__.py +0 -0
  77. mima/scripts/commands/add_quest.py +19 -0
  78. mima/scripts/commands/change_map.py +34 -0
  79. mima/scripts/commands/close_dialog.py +9 -0
  80. mima/scripts/commands/equip_weapon.py +23 -0
  81. mima/scripts/commands/give_item.py +26 -0
  82. mima/scripts/commands/give_resource.py +51 -0
  83. mima/scripts/commands/move_map.py +152 -0
  84. mima/scripts/commands/move_to.py +49 -0
  85. mima/scripts/commands/oneway_move.py +58 -0
  86. mima/scripts/commands/parallel.py +66 -0
  87. mima/scripts/commands/play_sound.py +13 -0
  88. mima/scripts/commands/present_item.py +53 -0
  89. mima/scripts/commands/progress_quest.py +12 -0
  90. mima/scripts/commands/quit_game.py +8 -0
  91. mima/scripts/commands/save_game.py +14 -0
  92. mima/scripts/commands/screen_fade.py +83 -0
  93. mima/scripts/commands/serial.py +69 -0
  94. mima/scripts/commands/set_facing_direction.py +21 -0
  95. mima/scripts/commands/set_spawn_map.py +17 -0
  96. mima/scripts/commands/show_choices.py +52 -0
  97. mima/scripts/commands/show_dialog.py +118 -0
  98. mima/scripts/commands/take_coins.py +23 -0
  99. mima/scripts/script_processor.py +61 -0
  100. mima/standalone/__init__.py +0 -0
  101. mima/standalone/camera.py +153 -0
  102. mima/standalone/geometry.py +1318 -0
  103. mima/standalone/multicolumn_list.py +54 -0
  104. mima/standalone/pixel_font.py +84 -0
  105. mima/standalone/scripting.py +145 -0
  106. mima/standalone/spatial.py +186 -0
  107. mima/standalone/sprite.py +158 -0
  108. mima/standalone/tiled_map.py +1247 -0
  109. mima/standalone/transformed_view.py +433 -0
  110. mima/standalone/user_input.py +563 -0
  111. mima/states/__init__.py +0 -0
  112. mima/states/game_state.py +189 -0
  113. mima/states/memory.py +28 -0
  114. mima/states/quest.py +71 -0
  115. mima/types/__init__.py +0 -0
  116. mima/types/alignment.py +7 -0
  117. mima/types/blend.py +8 -0
  118. mima/types/damage.py +42 -0
  119. mima/types/direction.py +44 -0
  120. mima/types/gate_color.py +7 -0
  121. mima/types/graphic_state.py +23 -0
  122. mima/types/keys.py +64 -0
  123. mima/types/mode.py +9 -0
  124. mima/types/nature.py +12 -0
  125. mima/types/object.py +22 -0
  126. mima/types/player.py +9 -0
  127. mima/types/position.py +13 -0
  128. mima/types/start.py +7 -0
  129. mima/types/terrain.py +9 -0
  130. mima/types/tile_collision.py +11 -0
  131. mima/types/weapon_slot.py +6 -0
  132. mima/types/window.py +44 -0
  133. mima/usables/__init__.py +0 -0
  134. mima/usables/item.py +51 -0
  135. mima/usables/weapon.py +68 -0
  136. mima/util/__init__.py +1 -0
  137. mima/util/colors.py +50 -0
  138. mima/util/constants.py +55 -0
  139. mima/util/functions.py +38 -0
  140. mima/util/input_defaults.py +170 -0
  141. mima/util/logging.py +51 -0
  142. mima/util/property.py +8 -0
  143. mima/util/runtime_config.py +327 -0
  144. mima/util/trading_item.py +23 -0
  145. mima/view/__init__.py +0 -0
  146. mima/view/camera.py +192 -0
  147. mima/view/mima_mode.py +618 -0
  148. mima/view/mima_scene.py +231 -0
  149. mima/view/mima_view.py +12 -0
  150. mima/view/mima_window.py +244 -0
  151. mima_engine-0.4.0.dist-info/METADATA +47 -0
  152. mima_engine-0.4.0.dist-info/RECORD +153 -0
  153. mima_engine-0.4.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,54 @@
1
+ def compute_visible_area(
2
+ prev: tuple[int, int],
3
+ cursor: int,
4
+ dim: tuple[int, int],
5
+ list_sizes: tuple[int, int],
6
+ ) -> tuple[int, int]:
7
+ """Return first (inclusive) and last (exclusive) visible index of a list.
8
+
9
+ Args:
10
+ prev: The previous first and last visible item.
11
+ cursor: The selected item index.
12
+ dim: The dimension in which the list should be displayed.
13
+ list_sizes: Previous size of the list and current size of the list.
14
+
15
+ Returns:
16
+ The new first and last visible indicies.
17
+
18
+ Raises:
19
+ ValueError on an unhandled case
20
+ """
21
+
22
+ target_cursor = cursor
23
+ first, last = prev
24
+ n_rows, n_cols = dim
25
+ capacity = n_rows * n_cols
26
+
27
+ if list_sizes[0] > list_sizes[1]:
28
+ # List became smaller
29
+ cursor = min(cursor, list_sizes[1] - 1)
30
+ last = list_sizes[1]
31
+ first = last - capacity + last % n_cols
32
+
33
+ if first <= cursor < first + capacity:
34
+ # Cursor already within visible area
35
+ last = first + capacity
36
+ elif cursor >= last:
37
+ # Cursor below current visible area
38
+ last = cursor + (n_cols - cursor % n_cols)
39
+ first = last - capacity
40
+ elif cursor < first:
41
+ # Cursor above current visible area
42
+ first = cursor - cursor % n_cols
43
+ last = first + capacity
44
+ else:
45
+ msg = (
46
+ f"Unhandled case: prev=({prev}), cursor={target_cursor}, dim={dim}, "
47
+ f"old_list_size={list_sizes[0]}, new_list_size={list_sizes[1]}"
48
+ )
49
+ raise ValueError(msg)
50
+
51
+ first = max(0, first)
52
+ last = min(list_sizes[1], last)
53
+
54
+ return first, last
@@ -0,0 +1,84 @@
1
+ import pygame
2
+ from pygame import BLEND_RGBA_MIN, Vector2
3
+ from typing_extensions import TYPE_CHECKING, Protocol, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from pygame import Surface
7
+
8
+ TRANSPARENT_COLOR = (254, 252, 253)
9
+
10
+
11
+ class Renderer(Protocol):
12
+ def draw_surface(
13
+ self,
14
+ pos: Vector2,
15
+ surf: pygame.Surface,
16
+ *,
17
+ src_pos: Vector2 | None = None,
18
+ src_size: Vector2 | None = None,
19
+ scale: float = 1.0,
20
+ angle: float = 0,
21
+ special_flags: int = 0,
22
+ ) -> None: ...
23
+
24
+
25
+ class PixelFont:
26
+ def __init__(self, image: pygame.Surface, font_size: tuple[int, int]) -> None:
27
+ self._image = image
28
+ self._font_size = font_size
29
+
30
+ self._colorized: dict[tuple[int, int, int, int], pygame.Surface] = {
31
+ (255, 255, 255, 255): self._image
32
+ }
33
+
34
+ def draw_text(
35
+ self,
36
+ pos: Vector2,
37
+ text: str,
38
+ display: Union[Renderer, "Surface"],
39
+ color: tuple[int, int, int, int] | None = None,
40
+ ) -> None:
41
+ if color is None:
42
+ color = (255, 255, 255, 255)
43
+ font = self._colorized.get(color)
44
+ if font is None:
45
+ font = self._image.copy()
46
+ font.fill(color, special_flags=BLEND_RGBA_MIN)
47
+ self._colorized[color] = font
48
+
49
+ surf = pygame.Surface((len(text) * self._font_size[0], self._font_size[1]))
50
+ surf.fill(TRANSPARENT_COLOR)
51
+ for i, c in enumerate(text):
52
+ sx = ((ord(c) - 32) % 16) * self._font_size[0]
53
+ sy = ((ord(c) - 32) // 16) * self._font_size[1]
54
+ surf.blit(font, (i * self._font_size[0], 0), ((sx, sy), self._font_size))
55
+ surf.set_colorkey(TRANSPARENT_COLOR)
56
+ if isinstance(display, pygame.Surface):
57
+ display.blit(surf, pos)
58
+ else:
59
+ display.draw_surface(pos, surf)
60
+
61
+ def draw_text_to_surface(
62
+ self,
63
+ text: str,
64
+ color: tuple[int, int, int, int] | None = None,
65
+ surface: pygame.Surface | None = None,
66
+ ) -> pygame.Surface:
67
+ if surface is None:
68
+ surface = pygame.Surface(
69
+ (len(text) * self._font_size[0], self._font_size[1])
70
+ )
71
+ surface.fill(TRANSPARENT_COLOR)
72
+ surface.set_colorkey(TRANSPARENT_COLOR)
73
+
74
+ self.draw_text(Vector2(), text, surface, color)
75
+
76
+ return surface
77
+
78
+ @property
79
+ def width(self) -> int:
80
+ return self._font_size[0]
81
+
82
+ @property
83
+ def height(self) -> int:
84
+ return self._font_size[1]
@@ -0,0 +1,145 @@
1
+ from enum import Enum
2
+
3
+
4
+ class Command:
5
+ """A script that can be used to interact with the game engine.
6
+
7
+ Args:
8
+ scopes: The scopes this command should be added to.
9
+
10
+ Attributes:
11
+ started: Indicates if this command has started.
12
+ completed: Indicates if this command has completed.
13
+ interruptible: Indicates if this command can be finished before it has
14
+ completed (it still can be forced to complete, regardless of this
15
+ flag).
16
+ scopes: A list containing different command scopes. This is to be
17
+ interpreted by the game engine, but can, e.g., be used to
18
+ differentiate between different players in a split-screen
19
+ multiplayer session.
20
+
21
+ """
22
+
23
+ def __init__(self, scopes: list[int] | None = None) -> None:
24
+ self.started: bool = False
25
+ self.completed: bool = False
26
+ self.interruptible: bool = False
27
+ self.scopes: list[int] = scopes if scopes else [0]
28
+
29
+ def start(self) -> None:
30
+ """Start this command in the next frame."""
31
+ return
32
+
33
+ def _start(self) -> None:
34
+ """Prepare for start and start this command (internal function)."""
35
+ self.started = True
36
+ self.start()
37
+
38
+ def update(self, elapsed_time: float) -> bool:
39
+ """Update this command."""
40
+ return False
41
+
42
+ def finalize(self) -> None:
43
+ """Finalize the operation of this command and clean-up."""
44
+ pass
45
+
46
+ def set_scopes(self, scopes: list[int]) -> None:
47
+ """Set or override the scopes variable of this command."""
48
+ self.scopes = scopes
49
+
50
+
51
+ class CompleteWhen(Enum):
52
+ ALL_COMPLETED = 0
53
+ ANY_COMPLETED = 1
54
+ FIRST_COMPLETED = 2
55
+
56
+
57
+ class CommandParallel(Command):
58
+ """A script that executes multiple commands in parallel.
59
+
60
+ This command completes based on different termination conditions. It either
61
+ completes after all subcommands are completed, after any subcommand is
62
+ completed, or if the first command in the list is completed.
63
+
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ cmds: list[Command],
69
+ completed_when: CompleteWhen = CompleteWhen.ALL_COMPLETED,
70
+ scopes: list[int] | None = None,
71
+ ) -> None:
72
+ super().__init__(scopes)
73
+
74
+ self._cmds: list[Command] = cmds
75
+ self._completed_when: CompleteWhen = completed_when
76
+
77
+ def start(self) -> None:
78
+ for cmd in self._cmds:
79
+ cmd.start()
80
+
81
+ def update(self, elapsed_time: float) -> bool:
82
+ for cmd in self._cmds:
83
+ cmd.update(elapsed_time)
84
+
85
+ if self._completed_when == CompleteWhen.ALL_COMPLETED:
86
+ self.completed = True
87
+ for cmd in self._cmds:
88
+ self.completed = self.completed and cmd.completed
89
+ elif self._completed_when == CompleteWhen.ANY_COMPLETED:
90
+ self.completed = False
91
+ for cmd in self._cmds:
92
+ self.completed = self.completed or cmd.completed
93
+ elif self._completed_when == CompleteWhen.FIRST_COMPLETED:
94
+ self.completed = self._cmds[0].completed
95
+ else:
96
+ msg = f"Unknown termination condition: {self._completed_when}"
97
+ raise ValueError(msg)
98
+
99
+ return self.completed
100
+
101
+ def set_scopes(self, scopes: list[int]) -> None:
102
+ self.scopes = scopes
103
+ for cmd in self._cmds:
104
+ cmd.set_scopes(scopes)
105
+
106
+
107
+ class ScriptProcessor:
108
+ def __init__(self) -> None:
109
+ self._commands: dict[int, list[Command]] = {}
110
+ self._script_active: dict[int, bool] = {}
111
+
112
+ def add_command(self, cmd: Command, scopes: list[int] | None = None) -> None:
113
+ if scopes:
114
+ cmd.set_scopes(scopes)
115
+
116
+ for scope in cmd.scopes:
117
+ self._commands.setdefault(scope, []).append(cmd)
118
+
119
+ def process_command(self, elapsed_time: float) -> bool:
120
+ for scope in self._commands:
121
+ if not self._commands[scope]:
122
+ self._script_active[scope] = False
123
+ continue
124
+
125
+ self._script_active[scope] = True
126
+ if not self._commands[scope][0].completed:
127
+ if not self._commands[scope][0].started:
128
+ self._commands[scope][0]._start()
129
+ else:
130
+ self._commands[scope][0].update(elapsed_time)
131
+ else:
132
+ self._commands[scope][0].finalize()
133
+ self._commands[scope].pop()
134
+ self._script_active[scope] = False
135
+
136
+ return any(list(self._script_active.values()))
137
+
138
+ def complete_command(self, scope: int, force: bool = False) -> bool:
139
+ if scope not in self._commands or not self._commands[scope]:
140
+ return True
141
+
142
+ if self._commands[scope][0].interruptible or force:
143
+ self._commands[scope][0].completed = True
144
+ return True
145
+ return False
@@ -0,0 +1,186 @@
1
+ import logging
2
+ import math
3
+
4
+ from pygame import Vector2
5
+ from typing_extensions import Generic, TypeVar, overload
6
+
7
+ LOG = logging.getLogger(__name__)
8
+ T = TypeVar("T")
9
+
10
+
11
+ class SpatialGrid(Generic[T]):
12
+ def __init__(self, world_size: Vector2, cell_size: int) -> None:
13
+ self._world_size: Vector2 = world_size
14
+ self._cell_size: int = cell_size
15
+
16
+ self._n_cols = int(math.ceil(world_size.x / cell_size))
17
+ self._n_rows = int(math.ceil(world_size.y / cell_size))
18
+
19
+ self._cells: list[list[T]] = [[] for _ in range(self._n_cols * self._n_rows)]
20
+ self._object_map: dict[T, list[int]] = {}
21
+ self.n_objects = 0
22
+
23
+ def insert(self, obj: T, pos: Vector2, size: Vector2 | None = None) -> None:
24
+ """Insert object at given position in the grid."""
25
+ size = Vector2(1, 1) if size is None else size
26
+ indices = cells_for_aabb(pos, size, self._cell_size, self._n_rows, self._n_cols)
27
+
28
+ for idx in indices:
29
+ self._cells[idx].append(obj)
30
+
31
+ self._object_map[obj] = indices
32
+ self.n_objects += 1
33
+
34
+ def remove(self, obj: T) -> None:
35
+ """Remove object from all grid cells it occupies."""
36
+ indices = self._object_map.get(obj)
37
+ if indices is None:
38
+ return
39
+
40
+ for idx in indices:
41
+ if obj in self._cells[idx]:
42
+ self._cells[idx].remove(obj)
43
+
44
+ self._object_map.pop(obj)
45
+ self.n_objects -= 1
46
+
47
+ def relocate(self, obj: T, new_pos: Vector2, size: Vector2 | None = None) -> None:
48
+ """Move object to a new position in the grid."""
49
+
50
+ old_indices = self._object_map.get(obj)
51
+ if old_indices is None:
52
+ LOG.warning(
53
+ "Objects %s not in grid. Performing insertion instead.", str(obj)
54
+ )
55
+ return self.insert(obj, new_pos, size)
56
+
57
+ size = Vector2(1, 1) if size is None else size
58
+
59
+ new_indices = cells_for_aabb(
60
+ new_pos, size, self._cell_size, self._n_rows, self._n_cols
61
+ )
62
+ if old_indices == new_indices:
63
+ return
64
+
65
+ LOG.debug(
66
+ "Object %s (%s) moving from cells %s to %s",
67
+ str(obj),
68
+ new_pos,
69
+ old_indices,
70
+ new_indices,
71
+ )
72
+ for idx in old_indices:
73
+ if obj in self._cells[idx]:
74
+ self._cells[idx].remove(obj)
75
+
76
+ for idx in new_indices:
77
+ self._cells[idx].append(obj)
78
+
79
+ self._object_map[obj] = new_indices
80
+
81
+ def get_grid_index(self, pos: Vector2) -> int:
82
+ x, y = self.get_grid_coords(pos)
83
+ return x + y * self._n_cols
84
+
85
+ def get_grid_coords(self, pos: Vector2) -> tuple[int, int]:
86
+ return (
87
+ int(clamp(pos.x / self._cell_size, 0, self._n_cols - 1)),
88
+ int(clamp(pos.y / self._cell_size, 0, self._n_rows - 1)),
89
+ )
90
+
91
+ def get_all_objects(self) -> list[T]:
92
+ """Return all objects stored in the grid."""
93
+ return list(self._object_map.keys())
94
+
95
+ def get_objects_in_region(self, pos: Vector2, size: Vector2) -> list[T]:
96
+ """Return all objects in the area defined by pos and size."""
97
+
98
+ sx = int(max(0, pos.x // self._cell_size))
99
+ sy = int(max(0, pos.y // self._cell_size))
100
+
101
+ # Clamp correctly here
102
+ ex = int(min(self._n_cols - 1, (pos.x + size.x - 1e-9) // self._cell_size))
103
+ ey = int(min(self._n_rows - 1, (pos.y + size.y - 1e-9) // self._cell_size))
104
+
105
+ result: set[T] = set()
106
+
107
+ for gy in range(sy, ey + 1):
108
+ row_offset = gy * self._n_cols
109
+ for gx in range(sx, ex + 1):
110
+ result.update(self._cells[gx + row_offset])
111
+
112
+ return list(result)
113
+
114
+ def get_cells_in_region(
115
+ self, pos: Vector2, size: Vector2
116
+ ) -> list[tuple[tuple[int, int], list[T]]]:
117
+ """Return all cells in the area defined by pos and size."""
118
+ tl = self.get_grid_coords(pos)
119
+ br = self.get_grid_coords(pos + size)
120
+ clamped_tl = (
121
+ clamp(tl[0], 0, self._n_cols - 1),
122
+ clamp(tl[1], 0, self._n_rows - 1),
123
+ )
124
+ clamped_br = (
125
+ clamp(br[0], 0, self._n_cols - 1),
126
+ clamp(br[0], 0, self._n_rows - 1),
127
+ )
128
+
129
+ results = [
130
+ ((x, y), self._cells[x + y * self._n_cols])
131
+ for y in range(clamped_tl[1], clamped_br[1] + 1)
132
+ for x in range(clamped_tl[0], clamped_br[0] + 1)
133
+ ]
134
+
135
+ return results
136
+
137
+ def clear(self) -> None:
138
+ self._cells: list[list[T]] = [[] for _ in range(self._n_cols * self._n_rows)]
139
+ self._object_map = {}
140
+ self.n_objects = 0
141
+
142
+
143
+ class SpatialHash(Generic[T]):
144
+ def __init__(self, cell_size: int):
145
+ self._cell_size: int = cell_size
146
+ self._cells: dict[tuple[int, int], list[T]] = {}
147
+
148
+ def _hash(self, pos: Vector2) -> tuple[int, int]:
149
+ return (int(pos.x) // self._cell_size, int(pos.y) // self._cell_size)
150
+
151
+
152
+ @overload
153
+ def clamp(val: int, low: int, high: int) -> int: ...
154
+
155
+
156
+ @overload
157
+ def clamp(val: float, low: float, high: float) -> float: ...
158
+
159
+
160
+ def clamp(val: int | float, low: int | float, high: int | float) -> int | float:
161
+ return max(low, min(high, val))
162
+
163
+
164
+ def vmax(val: Vector2, other: Vector2) -> Vector2:
165
+ return Vector2(max(val.x, other.x), max(val.y, other.y))
166
+
167
+
168
+ def vmin(val: Vector2, other: Vector2) -> Vector2:
169
+ return Vector2(min(val.x, other.x), min(val.y, other.y))
170
+
171
+
172
+ def cells_for_aabb(
173
+ pos: Vector2, size: Vector2, cell_size: int, n_rows: int, n_cols: int
174
+ ) -> list[int]:
175
+ tl = pos // cell_size
176
+ br = (pos + size) // cell_size
177
+ v_cols = Vector2(n_cols - 1, n_rows - 1)
178
+ tl = vmax(Vector2(), vmin(tl, v_cols))
179
+ br = vmax(Vector2(), vmin(br, v_cols))
180
+
181
+ indices: list[int] = []
182
+ for y in range(int(tl.y), int(br.y) + 1):
183
+ for x in range(int(tl.x), int(br.x) + 1):
184
+ indices.append(x + y * n_cols)
185
+
186
+ return indices
@@ -0,0 +1,158 @@
1
+ from enum import Enum
2
+
3
+ from pygame import Surface, Vector2
4
+ from typing_extensions import Any, Generic, Protocol, TypeVar
5
+
6
+ GS = TypeVar("GS", bound=Enum)
7
+ D = TypeVar("D", bound=Enum)
8
+
9
+
10
+ class Renderer(Protocol):
11
+ def draw_surface(
12
+ self,
13
+ pos: Vector2,
14
+ surf: Surface,
15
+ *,
16
+ src_pos: Vector2 | None = None,
17
+ src_size: Vector2 | None = None,
18
+ scale: float = 1.0,
19
+ angle: float = 0,
20
+ cache: bool = False,
21
+ special_flags: int = 0,
22
+ ) -> None: ...
23
+
24
+
25
+ class SpriteData:
26
+ def __init__(self) -> None:
27
+ self.duration: list[float] = []
28
+ self.offset: list[Vector2] = []
29
+ self.image: list[Surface] = []
30
+ self.size: list[Vector2] = []
31
+ self.frame_id: list[int] = []
32
+ self.hitboxes: list[list[dict]] = []
33
+
34
+ def n_frames(self) -> int:
35
+ return len(self.duration)
36
+
37
+
38
+ class SpriteSet(Generic[D]):
39
+ def __init__(self) -> None:
40
+ self.data: dict[D, SpriteData] = {}
41
+
42
+ def __getitem__(self, direction: D) -> SpriteData:
43
+ return self.data[direction]
44
+
45
+
46
+ class AnimatedSprite(Generic[GS, D]):
47
+ def __init__(self) -> None:
48
+ self._last_graphic_state: GS | None = None
49
+ self._last_direction: D | None = None
50
+
51
+ self.sprites: dict[GS, SpriteSet] = {}
52
+ self._timer: float = 0.0
53
+ self._frame: int = -1
54
+ self._src_pos: Vector2 = Vector2()
55
+ self._src_size: Vector2 = Vector2()
56
+ self._image: Surface | None = None
57
+ self._scaled_image: Surface | None = None
58
+ self._scale: float = 1.0
59
+ self._scaled_size: tuple[int, int] = (0, 0)
60
+
61
+ def update(self, elapsed_time: float, graphic_state: GS, direction: D) -> bool:
62
+ if self._last_graphic_state is None:
63
+ self._last_graphic_state = graphic_state
64
+ if self._last_direction is None:
65
+ self._last_direction = direction
66
+
67
+ gs = self.sprites.get(graphic_state)
68
+ if gs is None:
69
+ msg = (
70
+ f"Sprite has no {graphic_state=}. Available states are "
71
+ f"{self.sprites.keys()}"
72
+ )
73
+ raise ValueError(msg)
74
+ data = gs[direction]
75
+ update_vals = True
76
+ if (
77
+ graphic_state == self._last_graphic_state
78
+ and direction == self._last_direction
79
+ ):
80
+ self._timer -= elapsed_time
81
+ if self._timer <= 0.0:
82
+ self._frame = (self._frame + 1) % data.n_frames()
83
+ self._timer += data.duration[self._frame]
84
+ else:
85
+ update_vals = False
86
+
87
+ else:
88
+ # Something changed
89
+ self._frame = 0
90
+ self._timer = data.duration[self._frame]
91
+
92
+ if update_vals:
93
+ self._src_pos = data.offset[self._frame]
94
+ self._src_size = data.size[self._frame]
95
+ self._image = data.image[self._frame]
96
+ # self._scale_image()
97
+
98
+ self._last_graphic_state = graphic_state
99
+ self._last_direction = direction
100
+
101
+ return update_vals
102
+
103
+ def draw(self, pos: Vector2, ttv: Renderer, cache: bool = False) -> None:
104
+ if self._image is None:
105
+ return
106
+
107
+ ttv.draw_surface(
108
+ pos,
109
+ self._image,
110
+ src_pos=self._src_pos,
111
+ src_size=self._src_size,
112
+ scale=self._scale,
113
+ cache=cache,
114
+ )
115
+
116
+ def reset(self) -> None:
117
+ self._frame = -1
118
+ self._timer = 0.0
119
+
120
+ def __getitem__(self, graphic_state: GS) -> SpriteSet:
121
+ return self.sprites[graphic_state]
122
+
123
+ def add_frame(
124
+ self, graphic_state: GS, direction: D, frame_data: dict[str, Any]
125
+ ) -> None:
126
+ sprite_set = self.sprites.setdefault(graphic_state, SpriteSet())
127
+ sprite_data = sprite_set.data.setdefault(direction, SpriteData())
128
+ sprite_data.duration.append(frame_data["duration"])
129
+ sprite_data.offset.append(Vector2(frame_data["offset"]))
130
+ sprite_data.size.append(Vector2(frame_data["size"]))
131
+ sprite_data.image.append(frame_data["image"])
132
+ sprite_data.frame_id.append(frame_data["frame_id"])
133
+ hitboxes = frame_data.get("collision", [])
134
+ sprite_data.hitboxes.append(hitboxes)
135
+
136
+ def set_scale(self, scale: float) -> None:
137
+ self._scale = scale
138
+ # self._scale_image()
139
+
140
+ def get_size(self) -> Vector2:
141
+ if self._src_size is not None:
142
+ return self._src_size * self._scale
143
+ return Vector2()
144
+
145
+ # def _scale_image(self) -> None:
146
+ # if self._image is None:
147
+ # self._scaled_image = None
148
+ # self._scaled_size = (0, 0)
149
+ # return
150
+ # if self._scale == 1.0:
151
+ # self._scaled_image = self._image
152
+ # self._scaled_size = self._image.get_size()
153
+ # return
154
+ # self._scaled_size = (
155
+ # int((size := self._image.get_size())[0] * self._scale),
156
+ # int(size[1] * self._scale),
157
+ # )
158
+ # self._scaled_image = pygame.transform.scale(self._image, self._scaled_size)