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.
- mima/__init__.py +4 -0
- mima/backend/__init__.py +1 -0
- mima/backend/pygame_assets.py +401 -0
- mima/backend/pygame_audio.py +78 -0
- mima/backend/pygame_backend.py +603 -0
- mima/backend/pygame_camera.py +63 -0
- mima/backend/pygame_events.py +695 -0
- mima/backend/touch_control_scheme_a.py +126 -0
- mima/backend/touch_control_scheme_b.py +132 -0
- mima/core/__init__.py +0 -0
- mima/core/collision.py +325 -0
- mima/core/database.py +58 -0
- mima/core/engine.py +367 -0
- mima/core/mode_engine.py +81 -0
- mima/core/scene_engine.py +81 -0
- mima/integrated/__init__.py +0 -0
- mima/integrated/entity.py +183 -0
- mima/integrated/layered_map.py +351 -0
- mima/integrated/sprite.py +156 -0
- mima/layered/__init__.py +0 -0
- mima/layered/assets.py +56 -0
- mima/layered/scene.py +415 -0
- mima/layered/shape.py +99 -0
- mima/layered/shaped_sprite.py +78 -0
- mima/layered/virtual_input.py +302 -0
- mima/maps/__init__.py +0 -0
- mima/maps/template.py +71 -0
- mima/maps/tile.py +20 -0
- mima/maps/tile_animation.py +7 -0
- mima/maps/tile_info.py +10 -0
- mima/maps/tile_layer.py +52 -0
- mima/maps/tiled/__init__.py +0 -0
- mima/maps/tiled/tiled_layer.py +48 -0
- mima/maps/tiled/tiled_map.py +95 -0
- mima/maps/tiled/tiled_object.py +79 -0
- mima/maps/tiled/tiled_objectgroup.py +25 -0
- mima/maps/tiled/tiled_template.py +49 -0
- mima/maps/tiled/tiled_tile.py +90 -0
- mima/maps/tiled/tiled_tileset.py +51 -0
- mima/maps/tilemap.py +216 -0
- mima/maps/tileset.py +39 -0
- mima/maps/tileset_info.py +9 -0
- mima/maps/transition_map.py +146 -0
- mima/objects/__init__.py +0 -0
- mima/objects/animated_sprite.py +217 -0
- mima/objects/attribute_effect.py +26 -0
- mima/objects/attributes.py +126 -0
- mima/objects/creature.py +384 -0
- mima/objects/dynamic.py +206 -0
- mima/objects/effects/__init__.py +0 -0
- mima/objects/effects/colorize_screen.py +60 -0
- mima/objects/effects/debug_box.py +133 -0
- mima/objects/effects/light.py +103 -0
- mima/objects/effects/show_sprite.py +50 -0
- mima/objects/effects/walking_on_grass.py +70 -0
- mima/objects/effects/walking_on_water.py +57 -0
- mima/objects/loader.py +111 -0
- mima/objects/projectile.py +111 -0
- mima/objects/sprite.py +116 -0
- mima/objects/world/__init__.py +0 -0
- mima/objects/world/color_gate.py +67 -0
- mima/objects/world/color_switch.py +101 -0
- mima/objects/world/container.py +175 -0
- mima/objects/world/floor_switch.py +109 -0
- mima/objects/world/gate.py +178 -0
- mima/objects/world/light_source.py +121 -0
- mima/objects/world/logic_gate.py +157 -0
- mima/objects/world/movable.py +399 -0
- mima/objects/world/oneway.py +195 -0
- mima/objects/world/pickup.py +157 -0
- mima/objects/world/switch.py +179 -0
- mima/objects/world/teleport.py +308 -0
- mima/py.typed +0 -0
- mima/scripts/__init__.py +2 -0
- mima/scripts/command.py +38 -0
- mima/scripts/commands/__init__.py +0 -0
- mima/scripts/commands/add_quest.py +19 -0
- mima/scripts/commands/change_map.py +34 -0
- mima/scripts/commands/close_dialog.py +9 -0
- mima/scripts/commands/equip_weapon.py +23 -0
- mima/scripts/commands/give_item.py +26 -0
- mima/scripts/commands/give_resource.py +51 -0
- mima/scripts/commands/move_map.py +152 -0
- mima/scripts/commands/move_to.py +49 -0
- mima/scripts/commands/oneway_move.py +58 -0
- mima/scripts/commands/parallel.py +66 -0
- mima/scripts/commands/play_sound.py +13 -0
- mima/scripts/commands/present_item.py +53 -0
- mima/scripts/commands/progress_quest.py +12 -0
- mima/scripts/commands/quit_game.py +8 -0
- mima/scripts/commands/save_game.py +14 -0
- mima/scripts/commands/screen_fade.py +83 -0
- mima/scripts/commands/serial.py +69 -0
- mima/scripts/commands/set_facing_direction.py +21 -0
- mima/scripts/commands/set_spawn_map.py +17 -0
- mima/scripts/commands/show_choices.py +52 -0
- mima/scripts/commands/show_dialog.py +118 -0
- mima/scripts/commands/take_coins.py +23 -0
- mima/scripts/script_processor.py +61 -0
- mima/standalone/__init__.py +0 -0
- mima/standalone/camera.py +153 -0
- mima/standalone/geometry.py +1318 -0
- mima/standalone/multicolumn_list.py +54 -0
- mima/standalone/pixel_font.py +84 -0
- mima/standalone/scripting.py +145 -0
- mima/standalone/spatial.py +186 -0
- mima/standalone/sprite.py +158 -0
- mima/standalone/tiled_map.py +1247 -0
- mima/standalone/transformed_view.py +433 -0
- mima/standalone/user_input.py +563 -0
- mima/states/__init__.py +0 -0
- mima/states/game_state.py +189 -0
- mima/states/memory.py +28 -0
- mima/states/quest.py +71 -0
- mima/types/__init__.py +0 -0
- mima/types/alignment.py +7 -0
- mima/types/blend.py +8 -0
- mima/types/damage.py +42 -0
- mima/types/direction.py +44 -0
- mima/types/gate_color.py +7 -0
- mima/types/graphic_state.py +23 -0
- mima/types/keys.py +64 -0
- mima/types/mode.py +9 -0
- mima/types/nature.py +12 -0
- mima/types/object.py +22 -0
- mima/types/player.py +9 -0
- mima/types/position.py +13 -0
- mima/types/start.py +7 -0
- mima/types/terrain.py +9 -0
- mima/types/tile_collision.py +11 -0
- mima/types/weapon_slot.py +6 -0
- mima/types/window.py +44 -0
- mima/usables/__init__.py +0 -0
- mima/usables/item.py +51 -0
- mima/usables/weapon.py +68 -0
- mima/util/__init__.py +1 -0
- mima/util/colors.py +50 -0
- mima/util/constants.py +55 -0
- mima/util/functions.py +38 -0
- mima/util/input_defaults.py +170 -0
- mima/util/logging.py +51 -0
- mima/util/property.py +8 -0
- mima/util/runtime_config.py +327 -0
- mima/util/trading_item.py +23 -0
- mima/view/__init__.py +0 -0
- mima/view/camera.py +192 -0
- mima/view/mima_mode.py +618 -0
- mima/view/mima_scene.py +231 -0
- mima/view/mima_view.py +12 -0
- mima/view/mima_window.py +244 -0
- mima_engine-0.4.0.dist-info/METADATA +47 -0
- mima_engine-0.4.0.dist-info/RECORD +153 -0
- 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)
|