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,1318 @@
1
+ # noqa: E741
2
+ import math
3
+
4
+ from pygame.math import Vector2
5
+ from typing_extensions import Any, TypeAlias, overload
6
+
7
+ EPSILON = 1e-3
8
+
9
+
10
+ class Line:
11
+ def __init__(
12
+ self, start: Vector2 | None = None, end: Vector2 | None = None
13
+ ) -> None:
14
+ self.start: Vector2 = start if start is not None else Vector2()
15
+ self.end: Vector2 = end if end is not None else Vector2()
16
+
17
+ def vector(self) -> Vector2:
18
+ """Get vector pointing from start to end"""
19
+ return self.end - self.start
20
+
21
+ def length(self) -> float:
22
+ return self.vector().magnitude()
23
+
24
+ def length2(self) -> float:
25
+ return self.vector().magnitude_squared()
26
+
27
+ def rpoint(self, distance: float) -> Vector2:
28
+ """Get point along the line for a real distance."""
29
+ return self.start + self.vector().normalize() * distance
30
+
31
+ def upoint(self, distance: float) -> Vector2:
32
+ """Get point along the line for a unit distance."""
33
+ return self.start + self.vector() * distance
34
+
35
+ def side(self, point: Vector2) -> int:
36
+ """Return which side of the line does a point lie"""
37
+ d = self.vector().cross(point - self.start)
38
+ return -1 if d < 0 else 1 if d > 0 else 0
39
+
40
+ def coefficients(self) -> Vector2:
41
+ """Returns line equation coefficients.
42
+
43
+ Calculates line equation "mx + a" coefficients where:
44
+ x: m
45
+ y: a
46
+
47
+ Returns (inf, inf) when abs(end.x - start.x) < EPSILON
48
+ """
49
+ # Check if line is vertical or close to vertical
50
+ if abs(self.end.x - self.start.y) < EPSILON:
51
+ return Vector2(math.inf, math.inf)
52
+
53
+ m = (self.end.y - self.start.y) / (self.end.x - self.start.x)
54
+ return Vector2(m, -m * self.start.x + self.start.y)
55
+
56
+ def __len__(self) -> float:
57
+ return self.length()
58
+
59
+ @property
60
+ def pos(self) -> Vector2:
61
+ return self.start
62
+
63
+ @pos.setter
64
+ def pos(self, pos: Vector2) -> None:
65
+ self.start = pos
66
+
67
+
68
+ class Ray:
69
+ def __init__(
70
+ self, origin: Vector2 | None = None, direction: Vector2 | None = None
71
+ ) -> None:
72
+ self.origin: Vector2 = origin if origin is not None else Vector2()
73
+ self.direction: Vector2 = direction if direction is not None else Vector2()
74
+
75
+
76
+ class Rect:
77
+ def __init__(self, pos: Vector2 | None = None, size: Vector2 | None = None) -> None:
78
+ self.pos: Vector2 = pos if pos is not None else Vector2()
79
+ self.size: Vector2 = size if size is not None else Vector2()
80
+
81
+ def middle(self) -> Vector2:
82
+ """Get point in the center of the rectangle."""
83
+ return self.pos + (self.size * 0.5)
84
+
85
+ def top(self) -> Line:
86
+ """Get line segment from top side of the rectangle."""
87
+ return Line(self.pos, Vector2(self.pos.x + self.size.x, self.pos.y))
88
+
89
+ def bottom(self) -> Line:
90
+ """Get line segment from bottom side of the rectangle."""
91
+ return Line(Vector2(self.pos.x, self.pos.y + self.size.y), self.pos + self.size)
92
+
93
+ def left(self) -> Line:
94
+ """Get line segment from left side of the rectangle."""
95
+ return Line(self.pos, Vector2(self.pos.x, self.pos.y + self.size.y))
96
+
97
+ def right(self) -> Line:
98
+ """Get line segment from right side of the rectangle."""
99
+ return Line(Vector2(self.pos.x + self.size.x, self.pos.y), self.pos + self.size)
100
+
101
+ def side(self, index: int) -> Line:
102
+ """Get a line from an indexed side.
103
+
104
+ Starting top, going clockwise:
105
+ 0 = top
106
+ 1 = right
107
+ 2 = bottom
108
+ 3 = left
109
+
110
+ """
111
+ if index % 4 == 0:
112
+ return self.top()
113
+ elif index % 4 == 1:
114
+ return self.right()
115
+ elif index % 4 == 2:
116
+ return self.bottom()
117
+ else:
118
+ return self.left()
119
+
120
+ def area(self) -> float:
121
+ """Get area of the rectangle."""
122
+ return self.size.x * self.size.y
123
+
124
+ def perimeter(self) -> float:
125
+ """Get perimeter of the rectangle."""
126
+ return 2.0 * (self.size.x + self.size.y)
127
+
128
+ def side_count(self) -> int:
129
+ return 4
130
+
131
+
132
+ class Circle:
133
+ def __init__(self, pos: Vector2 | None = None, radius: float = 0.0) -> None:
134
+ self.pos = pos if pos is not None else Vector2()
135
+ self.radius = radius
136
+
137
+ def area(self) -> float:
138
+ return math.pi * self.radius**2
139
+
140
+ def perimeter(self) -> float:
141
+ return math.pi * self.radius * 2.0
142
+
143
+ def circumference(self) -> float:
144
+ return self.perimeter()
145
+
146
+
147
+ def clamp(val: float, low: float, high: float) -> float:
148
+ return max(low, min(high, val))
149
+
150
+
151
+ def filter_duplicates(points: list[Vector2]) -> list[Vector2]:
152
+ filtered = []
153
+ for p in points:
154
+ is_duplicate = False
155
+ for fp in filtered:
156
+ if abs(p.x - fp.x) < EPSILON and abs(p.y - fp.y) < EPSILON:
157
+ is_duplicate = True
158
+ break
159
+
160
+ if not is_duplicate:
161
+ filtered.append(p)
162
+
163
+ return filtered
164
+
165
+
166
+ Shape: TypeAlias = Line | Rect | Circle
167
+
168
+
169
+ def shape_from_str(
170
+ shape: str, pos: Vector2, pos2_or_size: Vector2, radius: float
171
+ ) -> Shape:
172
+ if shape == "circle":
173
+ return Circle(pos, radius)
174
+ elif shape == "rect":
175
+ return Rect(pos, pos2_or_size)
176
+ elif shape == "line":
177
+ return Line(pos, pos2_or_size)
178
+
179
+ msg = f"Unsupported shape {shape}. Use circle, line, or rect."
180
+ raise ValueError(msg)
181
+
182
+
183
+ def shape_from_dict(shape_dict: dict[str, Any]) -> Shape:
184
+ return shape_from_str(
185
+ shape_dict["shape"], shape_dict["pos"], shape_dict["size"], shape_dict["radius"]
186
+ )
187
+
188
+
189
+ @overload
190
+ def copy_shape(shape: Rect) -> Rect: ...
191
+ @overload
192
+ def copy_shape(shape: Circle) -> Circle: ...
193
+ @overload
194
+ def copy_shape(shape: Line) -> Line: ...
195
+
196
+
197
+ def copy_shape(shape: Shape) -> Shape:
198
+ if isinstance(shape, Rect):
199
+ return Rect(Vector2(shape.pos), Vector2(shape.size))
200
+ elif isinstance(shape, Circle):
201
+ return Circle(Vector2(shape.pos), shape.radius)
202
+ elif isinstance(shape, Line):
203
+ return Line(Vector2(shape.start), Vector2(shape.end))
204
+
205
+
206
+ @overload
207
+ def contains(g1: Vector2, g2: Vector2) -> bool: ...
208
+ @overload
209
+ def contains(g1: Vector2, g2: Line) -> bool: ...
210
+ @overload
211
+ def contains(g1: Vector2, g2: Rect) -> bool: ...
212
+ @overload
213
+ def contains(g1: Vector2, g2: Circle) -> bool: ...
214
+ @overload
215
+ def contains(g1: Line, g2: Vector2) -> bool: ...
216
+ @overload
217
+ def contains(g1: Line, g2: Line) -> bool: ...
218
+ @overload
219
+ def contains(g1: Line, g2: Rect) -> bool: ...
220
+ @overload
221
+ def contains(g1: Line, g2: Circle) -> bool: ...
222
+ @overload
223
+ def contains(g1: Rect, g2: Vector2) -> bool: ...
224
+ @overload
225
+ def contains(g1: Rect, g2: Line) -> bool: ...
226
+ @overload
227
+ def contains(g1: Rect, g2: Rect) -> bool: ...
228
+ @overload
229
+ def contains(g1: Rect, g2: Circle) -> bool: ...
230
+ @overload
231
+ def contains(g1: Circle, g2: Vector2) -> bool: ...
232
+ @overload
233
+ def contains(g1: Circle, g2: Line) -> bool: ...
234
+ @overload
235
+ def contains(g1: Circle, g2: Rect) -> bool: ...
236
+ @overload
237
+ def contains(g1: Circle, g2: Circle) -> bool: ...
238
+
239
+
240
+ def contains(g1: Vector2 | Shape, g2: Vector2 | Shape) -> bool:
241
+ if isinstance(g1, Vector2):
242
+ if isinstance(g2, Vector2):
243
+ return _point_contains_point(g1, g2)
244
+ elif isinstance(g2, Line):
245
+ return False # It can't!
246
+ if isinstance(g2, Rect):
247
+ return False # It can't!
248
+ elif isinstance(g2, Circle):
249
+ return False # It can't!
250
+ elif isinstance(g1, Line):
251
+ if isinstance(g2, Vector2):
252
+ return _line_contains_point(g1, g2)
253
+ elif isinstance(g2, Line):
254
+ return _line_contains_line(g1, g2)
255
+ elif isinstance(g2, Rect):
256
+ return False # It can't!
257
+ elif isinstance(g2, Circle):
258
+ return False # It can't!
259
+ elif isinstance(g1, Rect):
260
+ if isinstance(g2, Vector2):
261
+ return _rect_contains_point(g1, g2)
262
+ elif isinstance(g2, Line):
263
+ return _rect_contains_line(g1, g2)
264
+ elif isinstance(g2, Rect):
265
+ return _rect_contains_rect(g1, g2)
266
+ elif isinstance(g2, Circle):
267
+ return _rect_contains_circle(g1, g2)
268
+ elif isinstance(g1, Circle):
269
+ if isinstance(g2, Vector2):
270
+ return _circle_contains_point(g1, g2)
271
+ elif isinstance(g2, Line):
272
+ return _circle_contains_line(g1, g2)
273
+ elif isinstance(g2, Rect):
274
+ return _circle_contains_rect(g1, g2)
275
+ elif isinstance(g2, Circle):
276
+ return _circle_contains_circle(g1, g2)
277
+
278
+ msg = f"Unsupported types {type(g1)} and {type(g2)}"
279
+ raise ValueError(msg)
280
+
281
+
282
+ # Point contains ...
283
+ def _point_contains_point(p1: Vector2, p2: Vector2) -> bool:
284
+ return (p1 - p2).magnitude_squared() < EPSILON
285
+
286
+
287
+ # Line contains ...
288
+ def _line_contains_point(l: Line, p: Vector2) -> bool: # noqa: E741
289
+ d = (p.x - l.start.x) * (l.end.y - l.start.y) - (p.y - l.start.y) * (
290
+ l.end.x - l.start.x
291
+ )
292
+ if abs(d) < EPSILON:
293
+ # point is on line
294
+ u = l.vector().dot(p - l.start) / l.vector().magnitude_squared()
295
+ return 0.0 <= u <= 1.0
296
+ return False
297
+
298
+
299
+ def _line_contains_line(l1: Line, l2: Line) -> bool:
300
+ return overlaps(l1, l2.start) and overlaps(l1, l2.end)
301
+
302
+
303
+ # Rect contains ...
304
+ def _rect_contains_point(r1: Rect, p2: Vector2) -> bool:
305
+ return not (
306
+ p2.x < r1.pos.x
307
+ or p2.y < r1.pos.y
308
+ or p2.x > (r1.pos.x + r1.size.x)
309
+ or p2.y > (r1.pos.y + r1.size.y)
310
+ )
311
+
312
+
313
+ def _rect_contains_line(r: Rect, l: Line) -> bool: # noqa: E741
314
+ return contains(r, l.start) and contains(r, l.end)
315
+
316
+
317
+ def _rect_contains_rect(r1: Rect, r2: Rect) -> bool:
318
+ return (
319
+ (r2.pos.x >= r1.pos.x)
320
+ and (r2.pos.x + r2.size.x <= r1.pos.x + r1.size.x)
321
+ and (r2.pos.y >= r1.pos.y)
322
+ and (r2.pos.y + r2.size.y <= r1.pos.y + r1.size.y)
323
+ )
324
+
325
+
326
+ def _rect_contains_circle(r: Rect, c: Circle) -> bool:
327
+ return (
328
+ r.pos.x + c.radius <= c.pos.x
329
+ and c.pos.x <= r.pos.x + r.size.x - c.radius
330
+ and r.pos.y + c.radius <= c.pos.y
331
+ and c.pos.y <= r.pos.y + r.size.y - c.radius
332
+ )
333
+
334
+
335
+ # Circle contains ...
336
+ def _circle_contains_point(c1: Circle, p2: Vector2) -> bool:
337
+ return (c1.pos - p2).magnitude_squared() <= c1.radius**2
338
+
339
+
340
+ def _circle_contains_line(c: Circle, l: Line) -> bool: # noqa: E741
341
+ return contains(c, l.start) and contains(c, l.end)
342
+
343
+
344
+ def _circle_contains_rect(c: Circle, r: Rect) -> bool:
345
+ return (
346
+ contains(c, r.pos)
347
+ and contains(c, Vector2(r.pos.x + r.size.x, r.pos.y))
348
+ and contains(c, Vector2(r.pos.x, r.pos.y + r.size.y))
349
+ and contains(c, r.pos + r.size)
350
+ )
351
+
352
+
353
+ def _circle_contains_circle(c1: Circle, c2: Circle) -> bool:
354
+ return (
355
+ math.sqrt((c2.pos.x - c1.pos.x) ** 2 + (c2.pos.y - c1.pos.y) ** 2) + c2.radius
356
+ ) <= c1.radius
357
+
358
+
359
+ @overload
360
+ def closest(g1: Vector2, g2: Vector2) -> Vector2: ...
361
+ @overload
362
+ def closest(g1: Vector2, g2: Line) -> Vector2: ...
363
+ @overload
364
+ def closest(g1: Vector2, g2: Rect) -> Vector2: ...
365
+ @overload
366
+ def closest(g1: Vector2, g2: Circle) -> Vector2: ...
367
+ @overload
368
+ def closest(g1: Line, g2: Vector2) -> Vector2: ...
369
+ @overload
370
+ def closest(g1: Line, g2: Line) -> Vector2: ...
371
+ @overload
372
+ def closest(g1: Line, g2: Rect) -> Vector2: ...
373
+ @overload
374
+ def closest(g1: Line, g2: Circle) -> Vector2: ...
375
+ @overload
376
+ def closest(g1: Rect, g2: Vector2) -> Vector2: ...
377
+ @overload
378
+ def closest(g1: Rect, g2: Line) -> Vector2: ...
379
+ @overload
380
+ def closest(g1: Rect, g2: Rect) -> Vector2: ...
381
+ @overload
382
+ def closest(g1: Rect, g2: Circle) -> Vector2: ...
383
+ @overload
384
+ def closest(g1: Circle, g2: Vector2) -> Vector2: ...
385
+ @overload
386
+ def closest(g1: Circle, g2: Line) -> Vector2: ...
387
+ @overload
388
+ def closest(g1: Circle, g2: Rect) -> Vector2: ...
389
+ @overload
390
+ def closest(g1: Circle, g2: Circle) -> Vector2: ...
391
+
392
+
393
+ def closest(
394
+ g1: Vector2 | Line | Rect | Circle, g2: Vector2 | Line | Rect | Circle
395
+ ) -> Vector2:
396
+ if isinstance(g1, Vector2):
397
+ return g1
398
+ elif isinstance(g1, Line):
399
+ if isinstance(g2, Vector2):
400
+ return _closest_point_on_line_to_point(g1, g2)
401
+ elif isinstance(g2, Line):
402
+ return _closest_point_on_line_to_line(g1, g2)
403
+ elif isinstance(g2, Rect):
404
+ return _closest_point_on_line_to_rect(g1, g2)
405
+ elif isinstance(g2, Circle):
406
+ return _closest_point_on_line_to_circle(g1, g2)
407
+ elif isinstance(g1, Rect):
408
+ if isinstance(g2, Vector2):
409
+ return _closest_point_on_rect_to_point(g1, g2)
410
+ elif isinstance(g2, Line):
411
+ return _closest_point_on_rect_to_line(g1, g2)
412
+ elif isinstance(g2, Rect):
413
+ return _closest_point_on_rect_to_rect(g1, g2)
414
+ elif isinstance(g2, Circle):
415
+ return _closest_point_on_rect_to_circle(g1, g2)
416
+ elif isinstance(g1, Circle):
417
+ if isinstance(g2, Vector2):
418
+ return _closest_point_on_circle_to_point(g1, g2)
419
+ elif isinstance(g2, Line):
420
+ return _closest_point_on_circle_to_line(g1, g2)
421
+ elif isinstance(g2, Rect):
422
+ return _closest_point_on_circle_to_rect(g1, g2)
423
+ elif isinstance(g2, Circle):
424
+ return _closest_point_on_circle_to_circle(g1, g2)
425
+
426
+ msg = f"Unsupported types {type(g1)} and {type(g2)}"
427
+ raise ValueError(msg)
428
+
429
+
430
+ # Closest point on Line to ...
431
+ def _closest_point_on_line_to_point(
432
+ l: Line, # noqa: E741
433
+ p: Vector2,
434
+ ) -> Vector2:
435
+ d = l.vector()
436
+ u = clamp(d.dot(p - l.start) / d.magnitude_squared(), 0.0, 1.0)
437
+ return l.start + u * d
438
+
439
+
440
+ def _closest_point_on_line_to_line(l1: Line, l2: Line) -> Vector2:
441
+ raise NotImplementedError
442
+
443
+
444
+ def _closest_point_on_line_to_rect(l: Line, r: Rect) -> Vector2: # noqa: E741
445
+ raise NotImplementedError
446
+
447
+
448
+ def _closest_point_on_line_to_circle(
449
+ l: Line, # noqa: E741
450
+ c: Circle, # noqa: E741
451
+ ) -> Vector2:
452
+ p = closest(c, l)
453
+ return closest(l, p)
454
+
455
+
456
+ # Closest point on rectangle to ...
457
+ def _closest_point_on_rect_to_point(r: Rect, p: Vector2) -> Vector2:
458
+ edges = [r.top(), r.bottom(), r.left(), r.right()]
459
+
460
+ closest_to_edges = [closest(e, p) for e in edges]
461
+ distance_to_edges = [(c - p).magnitude_squared() for c in closest_to_edges]
462
+
463
+ dmin = distance_to_edges[0]
464
+ cmin = closest_to_edges[0]
465
+ for i in range(1, len(distance_to_edges)):
466
+ if distance_to_edges[i] < dmin:
467
+ dmin = distance_to_edges[i]
468
+ cmin = closest_to_edges[i]
469
+ return cmin
470
+
471
+
472
+ def _closest_point_on_rect_to_line(r: Rect, l: Line) -> Vector2: # noqa: E741
473
+ raise NotImplementedError
474
+
475
+
476
+ def _closest_point_on_rect_to_rect(r1: Rect, r2: Rect) -> Vector2:
477
+ raise NotImplementedError
478
+
479
+
480
+ def _closest_point_on_rect_to_circle(r: Rect, c: Circle) -> Vector2:
481
+ return Vector2(
482
+ max(r.pos.x, min(c.pos.x, r.pos.x + r.size.x)),
483
+ max(r.pos.y, min(c.pos.y, r.pos.y + r.size.y)),
484
+ )
485
+
486
+
487
+ # Closest point on circle to ...
488
+ def _closest_point_on_circle_to_point(c: Circle, p: Vector2) -> Vector2:
489
+ return c.pos + (p - c.pos).normalize() * c.radius
490
+
491
+
492
+ def _closest_point_on_circle_to_line(
493
+ c: Circle,
494
+ l: Line, # noqa: E741
495
+ ) -> Vector2:
496
+ p = closest(l, c.pos)
497
+ return c.pos + (p - c.pos).normalize() * c.radius
498
+
499
+
500
+ def _closest_point_on_circle_to_rect(c: Circle, r: Rect) -> Vector2:
501
+ # Find the closest point on the rectangle to the center of the circle
502
+ closest_point = Vector2(
503
+ max(r.pos.x, min(c.pos.x, r.pos.x + r.size.x)),
504
+ max(r.pos.y, min(c.pos.y, r.pos.y + r.size.y)),
505
+ )
506
+
507
+ # Compute the direction vector from the circle's center to the closest
508
+ # rectangle point
509
+ direction = closest_point - c.pos
510
+
511
+ # Normalize the direction vector
512
+ if direction.length() > 0:
513
+ direction = direction.normalize()
514
+ else:
515
+ direction = Vector2(0, 0)
516
+
517
+ # Scale the direction vector by the circle's radius and add the circle's
518
+ # position
519
+ return c.pos + direction * c.radius
520
+
521
+
522
+ def _closest_point_on_circle_to_circle(c1: Circle, c2: Circle) -> Vector2:
523
+ return closest(c1, c2.pos)
524
+
525
+
526
+ @overload
527
+ def overlaps(g1: Vector2, g2: Vector2) -> bool: ...
528
+ @overload
529
+ def overlaps(g1: Vector2, g2: Line) -> bool: ...
530
+ @overload
531
+ def overlaps(g1: Vector2, g2: Rect) -> bool: ...
532
+ @overload
533
+ def overlaps(g1: Vector2, g2: Circle) -> bool: ...
534
+ @overload
535
+ def overlaps(g1: Line, g2: Vector2) -> bool: ...
536
+ @overload
537
+ def overlaps(g1: Line, g2: Line) -> bool: ...
538
+ @overload
539
+ def overlaps(g1: Line, g2: Rect) -> bool: ...
540
+ @overload
541
+ def overlaps(g1: Line, g2: Circle) -> bool: ...
542
+ @overload
543
+ def overlaps(g1: Rect, g2: Vector2) -> bool: ...
544
+ @overload
545
+ def overlaps(g1: Rect, g2: Line) -> bool: ...
546
+ @overload
547
+ def overlaps(g1: Rect, g2: Rect) -> bool: ...
548
+ @overload
549
+ def overlaps(g1: Rect, g2: Circle) -> bool: ...
550
+ @overload
551
+ def overlaps(g1: Circle, g2: Vector2) -> bool: ...
552
+ @overload
553
+ def overlaps(g1: Circle, g2: Line) -> bool: ...
554
+ @overload
555
+ def overlaps(g1: Circle, g2: Rect) -> bool: ...
556
+ @overload
557
+ def overlaps(g1: Circle, g2: Circle) -> bool: ...
558
+
559
+
560
+ def overlaps(
561
+ g1: Vector2 | Line | Rect | Circle, g2: Vector2 | Line | Rect | Circle
562
+ ) -> bool:
563
+ if isinstance(g1, Vector2):
564
+ if isinstance(g2, Vector2):
565
+ return _point_overlaps_point(g1, g2)
566
+ elif isinstance(g2, Line):
567
+ return _point_overlaps_line(g1, g2)
568
+ if isinstance(g2, Rect):
569
+ return _point_overlaps_rect(g1, g2)
570
+ elif isinstance(g2, Circle):
571
+ return _point_overlaps_circle(g1, g2)
572
+ elif isinstance(g1, Line):
573
+ if isinstance(g2, Vector2):
574
+ return _line_overlaps_point(g1, g2)
575
+ elif isinstance(g2, Line):
576
+ return _line_overlaps_line(g1, g2)
577
+ elif isinstance(g2, Rect):
578
+ return _line_overlaps_rect(g1, g2)
579
+ elif isinstance(g2, Circle):
580
+ return _line_overlaps_circle(g1, g2)
581
+ elif isinstance(g1, Rect):
582
+ if isinstance(g2, Vector2):
583
+ return _rect_overlaps_point(g1, g2)
584
+ elif isinstance(g2, Line):
585
+ return _rect_overlaps_line(g1, g2)
586
+ elif isinstance(g2, Rect):
587
+ return _rect_overlaps_rect(g1, g2)
588
+ elif isinstance(g2, Circle):
589
+ return _rect_overlaps_circle(g1, g2)
590
+ elif isinstance(g1, Circle):
591
+ if isinstance(g2, Vector2):
592
+ return _circle_overlaps_point(g1, g2)
593
+ elif isinstance(g2, Line):
594
+ return _circle_overlaps_line(g1, g2)
595
+ elif isinstance(g2, Rect):
596
+ return _circle_overlaps_rect(g1, g2)
597
+ elif isinstance(g2, Circle):
598
+ return _circle_overlaps_circle(g1, g2)
599
+
600
+ msg = f"Unsupported types {type(g1)} and {type(g2)}"
601
+ raise ValueError(msg)
602
+
603
+
604
+ # Point overlaps ...
605
+ def _point_overlaps_point(p1: Vector2, p2: Vector2) -> bool:
606
+ return contains(p1, p2)
607
+
608
+
609
+ def _point_overlaps_line(p: Vector2, l: Line) -> bool: # noqa: E741
610
+ return contains(l, p)
611
+
612
+
613
+ def _point_overlaps_rect(p: Vector2, r: Rect) -> bool:
614
+ return overlaps(r, p)
615
+
616
+
617
+ def _point_overlaps_circle(p: Vector2, c: Circle) -> bool:
618
+ return overlaps(c, p)
619
+
620
+
621
+ # Line overlaps ...
622
+ def _line_overlaps_point(l: Line, p: Vector2) -> bool: # noqa: E741
623
+ return contains(l, p)
624
+
625
+
626
+ def _line_overlaps_line(l1: Line, l2: Line) -> bool:
627
+ d = (l2.end.y - l2.start.y) * (l1.end.x - l1.start.x) - (l2.end.x - l2.start.x) * (
628
+ l1.end.y - l1.start.y
629
+ )
630
+ ua = (
631
+ (l2.end.x - l2.start.x) * (l1.start.y - l2.start.y)
632
+ - (l2.end.y - l2.start.y) * (l1.start.x - l2.start.x)
633
+ ) / d
634
+ ub = (
635
+ (l1.end.x - l1.start.x) * (l1.start.y - l2.start.y)
636
+ - (l1.end.y - l1.start.y) * (l1.start.x - l2.start.x)
637
+ ) / d
638
+ return 0 <= ua <= 1 and 0 <= ub <= 1
639
+
640
+
641
+ def _line_overlaps_rect(l: Line, r: Rect) -> bool: # noqa: E741
642
+ return overlaps(r, l)
643
+
644
+
645
+ def _line_overlaps_circle(l: Line, c: Circle) -> bool: # noqa: E741
646
+ return overlaps(c, l)
647
+
648
+
649
+ # Rect overlaps ...
650
+ def _rect_overlaps_point(r: Rect, p: Vector2) -> bool:
651
+ return contains(r, p)
652
+
653
+
654
+ def _rect_overlaps_line(r: Rect, l: Line) -> bool: # noqa: E741
655
+ return (
656
+ contains(r, l.start)
657
+ or overlaps(r.top(), l)
658
+ or overlaps(r.bottom(), l)
659
+ or overlaps(r.right(), l)
660
+ )
661
+
662
+
663
+ def _rect_overlaps_rect(r1: Rect, r2: Rect) -> bool:
664
+ return (
665
+ r1.pos.x <= r2.pos.x + r2.size.x
666
+ and r1.pos.x + r1.size.x >= r2.pos.x
667
+ and r1.pos.y <= r2.pos.y + r2.size.y
668
+ and r1.pos.y + r1.size.y >= r2.pos.y
669
+ )
670
+
671
+
672
+ def _rect_overlaps_circle(r: Rect, c: Circle) -> bool:
673
+ return overlaps(c, r)
674
+
675
+
676
+ # Circle overlaps ...
677
+ def _circle_overlaps_point(c: Circle, p: Vector2) -> bool:
678
+ return contains(c, p)
679
+
680
+
681
+ def _circle_overlaps_line(c: Circle, l: Line) -> bool: # noqa: E741
682
+ closest_p = closest(l, c.pos)
683
+ return (c.pos - closest_p).magnitude_squared() <= (c.radius**2)
684
+
685
+
686
+ def _circle_overlaps_rect(c: Circle, r: Rect) -> bool:
687
+ overlap = (
688
+ Vector2(
689
+ clamp(c.pos.x, r.pos.x, r.pos.x + r.size.x),
690
+ clamp(c.pos.y, r.pos.y, r.pos.y + r.size.y),
691
+ )
692
+ - c.pos
693
+ ).magnitude_squared()
694
+ # TODO: if (std::isnan(overlap)) overlap = 0
695
+ return (overlap - (c.radius**2)) < 0
696
+
697
+
698
+ def _circle_overlaps_circle(c1: Circle, c2: Circle) -> bool:
699
+ return (c1.pos - c2.pos).magnitude_squared() <= (c1.radius + c2.radius) * (
700
+ c1.radius + c2.radius
701
+ )
702
+
703
+
704
+ @overload
705
+ def intersects(g1: Vector2, g2: Vector2) -> list[Vector2]: ...
706
+ @overload
707
+ def intersects(g1: Vector2, g2: Line) -> list[Vector2]: ...
708
+ @overload
709
+ def intersects(g1: Vector2, g2: Rect) -> list[Vector2]: ...
710
+ @overload
711
+ def intersects(g1: Vector2, g2: Circle) -> list[Vector2]: ...
712
+ @overload
713
+ def intersects(g1: Line, g2: Vector2) -> list[Vector2]: ...
714
+ @overload
715
+ def intersects(g1: Line, g2: Line) -> list[Vector2]: ...
716
+ @overload
717
+ def intersects(g1: Line, g2: Rect) -> list[Vector2]: ...
718
+ @overload
719
+ def intersects(g1: Line, g2: Circle) -> list[Vector2]: ...
720
+ @overload
721
+ def intersects(g1: Rect, g2: Vector2) -> list[Vector2]: ...
722
+ @overload
723
+ def intersects(g1: Rect, g2: Line) -> list[Vector2]: ...
724
+ @overload
725
+ def intersects(g1: Rect, g2: Rect) -> list[Vector2]: ...
726
+ @overload
727
+ def intersects(g1: Rect, g2: Circle) -> list[Vector2]: ...
728
+ @overload
729
+ def intersects(g1: Circle, g2: Vector2) -> list[Vector2]: ...
730
+ @overload
731
+ def intersects(g1: Circle, g2: Line) -> list[Vector2]: ...
732
+ @overload
733
+ def intersects(g1: Circle, g2: Rect) -> list[Vector2]: ...
734
+ @overload
735
+ def intersects(g1: Circle, g2: Circle) -> list[Vector2]: ...
736
+
737
+
738
+ def intersects(
739
+ g1: Vector2 | Line | Rect | Circle, g2: Vector2 | Line | Rect | Circle
740
+ ) -> list[Vector2]:
741
+ if isinstance(g1, Vector2):
742
+ if isinstance(g2, Vector2):
743
+ return _point_intersects_point(g1, g2)
744
+ elif isinstance(g2, Line):
745
+ return _point_intersects_line(g1, g2)
746
+ if isinstance(g2, Rect):
747
+ return _point_intersects_rect(g1, g2)
748
+ elif isinstance(g2, Circle):
749
+ return _point_intersects_circle(g1, g2)
750
+ elif isinstance(g1, Line):
751
+ if isinstance(g2, Vector2):
752
+ return _line_intersects_point(g1, g2)
753
+ elif isinstance(g2, Line):
754
+ return _line_intersects_line(g1, g2)
755
+ elif isinstance(g2, Rect):
756
+ return _line_intersects_rect(g1, g2)
757
+ elif isinstance(g2, Circle):
758
+ return _line_intersects_circle(g1, g2)
759
+ elif isinstance(g1, Rect):
760
+ if isinstance(g2, Vector2):
761
+ return _rect_intersects_point(g1, g2)
762
+ elif isinstance(g2, Line):
763
+ return _rect_intersects_line(g1, g2)
764
+ elif isinstance(g2, Rect):
765
+ return _rect_intersects_rect(g1, g2)
766
+ elif isinstance(g2, Circle):
767
+ return _rect_intersects_circle(g1, g2)
768
+ elif isinstance(g1, Circle):
769
+ if isinstance(g2, Vector2):
770
+ return _circle_intersects_point(g1, g2)
771
+ elif isinstance(g2, Line):
772
+ return _circle_intersects_line(g1, g2)
773
+ elif isinstance(g2, Rect):
774
+ return _circle_intersects_rect(g1, g2)
775
+ elif isinstance(g2, Circle):
776
+ return _circle_intersects_circle(g1, g2)
777
+
778
+ msg = f"Unsupported types {type(g1)} and {type(g2)}"
779
+ raise ValueError(msg)
780
+
781
+
782
+ # Point intersects ...
783
+ def _point_intersects_point(p1: Vector2, p2: Vector2) -> list[Vector2]:
784
+ return [p1] if contains(p1, p2) else []
785
+
786
+
787
+ def _point_intersects_line(p: Vector2, l: Line) -> list[Vector2]: # noqa: E741
788
+ return intersects(l, p)
789
+
790
+
791
+ def _point_intersects_rect(p: Vector2, r: Rect) -> list[Vector2]:
792
+ return intersects(r, p)
793
+
794
+
795
+ def _point_intersects_circle(p: Vector2, c: Circle) -> list[Vector2]:
796
+ return intersects(c, p)
797
+
798
+
799
+ # Line intersects ...
800
+ def _line_intersects_point(l: Line, p: Vector2) -> list[Vector2]: # noqa: E741
801
+ return [p] if contains(l, p) else []
802
+
803
+
804
+ def _line_intersects_line(l1: Line, l2: Line, infinite: bool = False) -> list[Vector2]:
805
+ rd = l1.vector().cross(l2.vector())
806
+ if rd == 0:
807
+ # Parallel or Colinear TODO: return two points
808
+ return []
809
+
810
+ rd = 1.0 / rd
811
+ rn = (
812
+ (l2.end.x - l2.start.x) * (l1.start.y - l2.start.y)
813
+ - (l2.end.y - l2.start.y) * (l1.start.x - l2.start.x)
814
+ ) * rd
815
+ sn = (
816
+ (l1.end.x - l1.start.x) * (l1.start.y - l2.start.y)
817
+ - (l1.end.y - l1.start.y) * (l1.start.x - l2.start.x)
818
+ ) * rd
819
+
820
+ if not infinite and (rn < 0.0 or rn > 1.0 or sn < 0.0 or sn > 1.0):
821
+ return [] # Intersection not within line segment
822
+ return [l1.start + rn * l1.vector()]
823
+
824
+
825
+ def _line_intersects_rect(l: Line, r: Rect) -> list[Vector2]: # noqa: E741
826
+ return intersects(r, l)
827
+
828
+
829
+ def _line_intersects_circle(l: Line, c: Circle) -> list[Vector2]: # noqa: E741
830
+ return intersects(c, l)
831
+
832
+
833
+ # Rect intersects ...
834
+ def _rect_intersects_point(r: Rect, p: Vector2) -> list[Vector2]:
835
+ for i in range(r.side_count()):
836
+ if contains(r.side(i), p):
837
+ return [p]
838
+ return []
839
+
840
+
841
+ def _rect_intersects_line(r: Rect, l: Line) -> list[Vector2]: # noqa: E741
842
+ intersections = []
843
+ for i in range(r.side_count()):
844
+ intersections.extend(intersects(r.side(i), l))
845
+
846
+ return filter_duplicates(intersections)
847
+
848
+
849
+ def _rect_intersects_rect(r1: Rect, r2: Rect) -> list[Vector2]:
850
+ intersections = []
851
+ for i in range(r2.side_count()):
852
+ intersections.extend(intersects(r1, r2.side(i)))
853
+
854
+ return filter_duplicates(intersections)
855
+
856
+
857
+ def _rect_intersects_circle(r: Rect, c: Circle) -> list[Vector2]:
858
+ return intersects(c, r)
859
+
860
+
861
+ # Circle intersects ...
862
+ def _circle_intersects_point(c: Circle, p: Vector2) -> list[Vector2]:
863
+ if abs((p - c.pos).magnitude_squared() - (c.radius**2)) <= EPSILON:
864
+ return [p]
865
+ return []
866
+
867
+
868
+ def _circle_intersects_line(c: Circle, l: Line) -> list[Vector2]: # noqa: E741
869
+ closest_point_to_segment = closest(l, c.pos)
870
+
871
+ if overlaps(c, closest_point_to_segment):
872
+ # Circle is too far away
873
+ return []
874
+
875
+ # Compute closest to the circle on the line
876
+ d = l.vector()
877
+ u_line = d.dot(c.pos - l.start) / d.magnitude_squared()
878
+ closest_point_to_line = l.start + u_line * d
879
+ dist_to_line = (c.pos - closest_point_to_line).magnitude_squared()
880
+
881
+ if abs(dist_to_line - c.radius**2) < EPSILON:
882
+ # Circle "kisses" the line
883
+ return [closest_point_to_line]
884
+
885
+ # Circle intersects the line
886
+ length = math.sqrt(c.radius**2 - dist_to_line)
887
+ p1 = closest_point_to_line + l.vector().normalize() * length
888
+ p2 = closest_point_to_line - l.vector().normalize() * length
889
+
890
+ intersections = []
891
+ if (p1 - closest(l, p1)).magnitude_squared() < EPSILON**2:
892
+ intersections.append(p1)
893
+ if (p2 - closest(l, p2)).magnitude_squared() < EPSILON**2:
894
+ intersections.append(p2)
895
+
896
+ return filter_duplicates(intersections)
897
+
898
+
899
+ def _circle_intersects_rect(c: Circle, r: Rect) -> list[Vector2]:
900
+ intersections = []
901
+ for i in range(r.side_count()):
902
+ intersections.extend(intersects(c, r.side(i)))
903
+
904
+ return filter_duplicates(intersections)
905
+
906
+
907
+ def _circle_intersects_circle(c1: Circle, c2: Circle) -> list[Vector2]:
908
+ if c1.pos == c2.pos:
909
+ # Circles are either within one another so cannot intersect or
910
+ # are identical so share all point but this cannot be
911
+ # represented in a good way in the return value
912
+ return []
913
+
914
+ between = c2.pos - c1.pos
915
+ dist2 = between.magnitude_squared()
916
+ radius_sum = c1.radius + c2.radius
917
+ if dist2 > radius_sum**2:
918
+ # Circles are too far apart to be touching
919
+ return []
920
+ if contains(c1, c2) or contains(c2, c1):
921
+ # One circle is inside of the other, they can't be intersecting
922
+ return []
923
+ if dist2 == radius_sum:
924
+ # Circles are touching at exactly 1 point
925
+ return [Vector2(c1.pos + between.normalize() * c1.radius)]
926
+
927
+ # Otherwise, they're touching at 2 points
928
+ dist = math.sqrt(dist2)
929
+ cc_dist = (dist2 + c1.radius**2 - c2.radius**2) / (2 * dist)
930
+ chord_center = c1.pos + between.normalize() * cc_dist
931
+ half_chord = between.normalize().rotate(-90) * math.sqrt(
932
+ c1.radius**2 - cc_dist**2
933
+ ) # FIXME: 90 or -90
934
+ return [chord_center + half_chord, chord_center - half_chord]
935
+
936
+
937
+ @overload
938
+ def resolve_collision(
939
+ g1: Vector2, g2: Vector2, move_both: bool = False
940
+ ) -> tuple[Vector2, Vector2]: ...
941
+ @overload
942
+ def resolve_collision(
943
+ g1: Vector2, g2: Line, move_both: bool = False
944
+ ) -> tuple[Vector2, Vector2]: ...
945
+ @overload
946
+ def resolve_collision(
947
+ g1: Vector2, g2: Rect, move_both: bool = False
948
+ ) -> tuple[Vector2, Vector2]: ...
949
+ @overload
950
+ def resolve_collision(
951
+ g1: Vector2, g2: Circle, move_both: bool = False
952
+ ) -> tuple[Vector2, Vector2]: ...
953
+ @overload
954
+ def resolve_collision(
955
+ g1: Line, g2: Vector2, move_both: bool = False
956
+ ) -> tuple[Vector2, Vector2]: ...
957
+ @overload
958
+ def resolve_collision(
959
+ g1: Line, g2: Line, move_both: bool = False
960
+ ) -> tuple[Vector2, Vector2]: ...
961
+ @overload
962
+ def resolve_collision(
963
+ g1: Line, g2: Rect, move_both: bool = False
964
+ ) -> tuple[Vector2, Vector2]: ...
965
+ @overload
966
+ def resolve_collision(
967
+ g1: Line, g2: Circle, move_both: bool = False
968
+ ) -> tuple[Vector2, Vector2]: ...
969
+ @overload
970
+ def resolve_collision(
971
+ g1: Rect, g2: Vector2, move_both: bool = False
972
+ ) -> tuple[Vector2, Vector2]: ...
973
+ @overload
974
+ def resolve_collision(
975
+ g1: Rect, g2: Line, move_both: bool = False
976
+ ) -> tuple[Vector2, Vector2]: ...
977
+ @overload
978
+ def resolve_collision(
979
+ g1: Rect, g2: Rect, move_both: bool = False
980
+ ) -> tuple[Vector2, Vector2]: ...
981
+ @overload
982
+ def resolve_collision(
983
+ g1: Rect, g2: Circle, move_both: bool = False
984
+ ) -> tuple[Vector2, Vector2]: ...
985
+ @overload
986
+ def resolve_collision(
987
+ g1: Circle, g2: Vector2, move_both: bool = False
988
+ ) -> tuple[Vector2, Vector2]: ...
989
+ @overload
990
+ def resolve_collision(
991
+ g1: Circle, g2: Line, move_both: bool = False
992
+ ) -> tuple[Vector2, Vector2]: ...
993
+ @overload
994
+ def resolve_collision(
995
+ g1: Circle, g2: Rect, move_both: bool = False
996
+ ) -> tuple[Vector2, Vector2]: ...
997
+ @overload
998
+ def resolve_collision(
999
+ g1: Circle, g2: Circle, move_both: bool = False
1000
+ ) -> tuple[Vector2, Vector2]: ...
1001
+
1002
+
1003
+ def resolve_collision(
1004
+ g1: Vector2 | Line | Rect | Circle,
1005
+ g2: Vector2 | Line | Rect | Circle,
1006
+ move_both: bool = False,
1007
+ ) -> tuple[Vector2, Vector2]:
1008
+ if isinstance(g1, Vector2):
1009
+ if isinstance(g2, Vector2):
1010
+ return _point_collides_with_point(g1, g2, move_both)
1011
+ elif isinstance(g2, Line):
1012
+ return _point_collides_with_line(g1, g2, move_both)
1013
+ if isinstance(g2, Rect):
1014
+ return _point_collides_with_rect(g1, g2, move_both)
1015
+ elif isinstance(g2, Circle):
1016
+ return _point_collides_with_circle(g1, g2, move_both)
1017
+ elif isinstance(g1, Line):
1018
+ if isinstance(g2, Vector2):
1019
+ return _line_collides_with_point(g1, g2, move_both)
1020
+ elif isinstance(g2, Line):
1021
+ return _line_collides_with_line(g1, g2, move_both)
1022
+ elif isinstance(g2, Rect):
1023
+ return _line_collides_with_rect(g1, g2, move_both)
1024
+ elif isinstance(g2, Circle):
1025
+ return _line_collides_with_circle(g1, g2, move_both)
1026
+ elif isinstance(g1, Rect):
1027
+ if isinstance(g2, Vector2):
1028
+ return _rect_collides_with_point(g1, g2, move_both)
1029
+ elif isinstance(g2, Line):
1030
+ return _rect_collides_with_line(g1, g2, move_both)
1031
+ elif isinstance(g2, Rect):
1032
+ return _rect_collides_with_rect(g1, g2, move_both)
1033
+ elif isinstance(g2, Circle):
1034
+ return _rect_collides_with_circle(g1, g2, move_both)
1035
+ elif isinstance(g1, Circle):
1036
+ if isinstance(g2, Vector2):
1037
+ return _circle_collides_with_point(g1, g2, move_both)
1038
+ elif isinstance(g2, Line):
1039
+ return _circle_collides_with_line(g1, g2, move_both)
1040
+ elif isinstance(g2, Rect):
1041
+ return _circle_collides_with_rect(g1, g2, move_both)
1042
+ elif isinstance(g2, Circle):
1043
+ return _circle_collides_with_circle(g1, g2, move_both)
1044
+
1045
+ msg = f"Unsupported types {type(g1)} and {type(g2)}"
1046
+ raise ValueError(msg)
1047
+
1048
+
1049
+ # Point collides_with ...
1050
+ def _point_collides_with_point(
1051
+ p1: Vector2, p2: Vector2, move_both: bool = False
1052
+ ) -> tuple[Vector2, Vector2]:
1053
+ raise NotImplementedError
1054
+
1055
+
1056
+ def _point_collides_with_line(
1057
+ p1: Vector2, l2: Line, move_both: bool = False
1058
+ ) -> tuple[Vector2, Vector2]:
1059
+ raise NotImplementedError
1060
+
1061
+
1062
+ def _point_collides_with_rect(
1063
+ p1: Vector2, r2: Rect, move_both: bool = False
1064
+ ) -> tuple[Vector2, Vector2]:
1065
+ raise NotImplementedError
1066
+
1067
+
1068
+ def _point_collides_with_circle(
1069
+ p1: Vector2, c2: Circle, move_both: bool = False
1070
+ ) -> tuple[Vector2, Vector2]:
1071
+ raise NotImplementedError
1072
+
1073
+
1074
+ # Line collides_with ...
1075
+ def _line_collides_with_point(
1076
+ l1: Line, p2: Vector2, move_both: bool = False
1077
+ ) -> tuple[Vector2, Vector2]:
1078
+ raise NotImplementedError
1079
+
1080
+
1081
+ def _line_collides_with_line(
1082
+ l1: Line, l2: Line, move_both: bool = False
1083
+ ) -> tuple[Vector2, Vector2]:
1084
+ raise NotImplementedError
1085
+
1086
+
1087
+ def _line_collides_with_rect(
1088
+ l1: Line, r2: Rect, move_both: bool = False
1089
+ ) -> tuple[Vector2, Vector2]:
1090
+ raise NotImplementedError
1091
+
1092
+
1093
+ def _line_collides_with_circle(
1094
+ l1: Line, c2: Circle, move_both: bool = False
1095
+ ) -> tuple[Vector2, Vector2]:
1096
+ raise NotImplementedError
1097
+
1098
+
1099
+ # Rect collides_with ...
1100
+ def _rect_collides_with_point(
1101
+ r1: Rect, p2: Vector2, move_both: bool = False
1102
+ ) -> tuple[Vector2, Vector2]:
1103
+ raise NotImplementedError
1104
+
1105
+
1106
+ def _rect_collides_with_line(
1107
+ r1: Rect, l2: Line, move_both: bool = False
1108
+ ) -> tuple[Vector2, Vector2]:
1109
+ raise NotImplementedError
1110
+
1111
+
1112
+ def _rect_collides_with_rect(
1113
+ r1: Rect, r2: Rect, move_both: bool = False
1114
+ ) -> tuple[Vector2, Vector2]:
1115
+ """Calculate the collision between two rectangles.
1116
+
1117
+ This function calculates the overlap of two rectangles r1 and r2
1118
+ and returns the new positions of both rectangles as tuple. By
1119
+ default, only the first rectangle will be moved and the second will
1120
+ be considered 'solid'. This can be changed with the move_both flag.
1121
+ In that case, both rectangles will be pushed away from each other
1122
+ by the same amount.
1123
+
1124
+ Args:
1125
+ r1: Rectangle with `pos` and `size`
1126
+ r2: Second rectangle, solid by default
1127
+ move_both: If True, both rectangles will be moved.
1128
+
1129
+ Returns:
1130
+ Tuple with the new positions of the rectangles.
1131
+ """
1132
+
1133
+ # Calculate the horizontal (X) and vertical (Y) overlap
1134
+ br1 = r1.pos + r1.size
1135
+ br2 = r2.pos + r2.size
1136
+ overlap = vmin(br1, br2) - vmax(r1.pos, r2.pos)
1137
+
1138
+ if overlap.x <= 0 or overlap.y <= 0:
1139
+ return r1.pos, r2.pos
1140
+
1141
+ # Check if both rectangles have the same center point
1142
+ r1_center = r1.pos + r1.size / 2.0
1143
+ r2_center = r2.pos + r2.size / 2.0
1144
+ if r1_center == r2_center:
1145
+ # Special case: Both rectangles have identical center points
1146
+ # In this case, one (or both) of the rectangles will get a
1147
+ # Small adjustment to break perfect overlap
1148
+ push = Vector2(0.1, 0.1)
1149
+ if move_both:
1150
+ new_pos1 = r1.pos - push / 2.0
1151
+ new_pos2 = r2.pos + push / 2.0
1152
+ else:
1153
+ new_pos1 = r1.pos - push
1154
+ new_pos2 = r2.pos
1155
+ return new_pos1, new_pos2
1156
+
1157
+ push = Vector2()
1158
+ # Resolve horizontal overlap
1159
+ if overlap.x > 0:
1160
+ roverlap = br2.x - r1.pos.x
1161
+ loverlap = br1.x - r2.pos.x
1162
+ if roverlap < loverlap:
1163
+ push.x = roverlap
1164
+ else:
1165
+ push.x = -loverlap
1166
+
1167
+ # Resolve vertical overlap
1168
+ if overlap.y > 0:
1169
+ boverlap = br2.y - r1.pos.y
1170
+ toverlap = br1.y - r2.pos.y
1171
+ if boverlap < toverlap:
1172
+ push.y = boverlap
1173
+ else:
1174
+ push.y = -toverlap
1175
+
1176
+ # Calculate new positions based on the move_both flag
1177
+ if move_both:
1178
+ new_pos1 = r1.pos + push / 2
1179
+ new_pos2 = r2.pos - push / 2
1180
+ else:
1181
+ new_pos1 = r1.pos + push
1182
+ new_pos2 = r2.pos
1183
+ return new_pos1, new_pos2
1184
+
1185
+
1186
+ def _rect_collides_with_circle(
1187
+ r: Rect, c: Circle, move_both: bool = False
1188
+ ) -> tuple[Vector2, Vector2]:
1189
+ closest_point = closest(c, r)
1190
+ ray = closest_point - c.pos
1191
+ overlap = c.radius - ray.magnitude()
1192
+ # TODO: std::isnan(overlap): overlap=0
1193
+ new_pos1 = Vector2(r.pos.x, r.pos.y)
1194
+ new_pos2 = Vector2(c.pos.x, c.pos.y)
1195
+ try:
1196
+ ray.normalize()
1197
+ except ValueError:
1198
+ # breakpoint()
1199
+ overlap = 0
1200
+
1201
+ if overlap > 0:
1202
+ if move_both:
1203
+ new_pos1 -= ray.normalize() * overlap * 0.5
1204
+ new_pos2 += ray.normalize() * overlap * 0.5
1205
+ else:
1206
+ new_pos1 -= ray.normalize() * overlap
1207
+
1208
+ return new_pos1, new_pos2
1209
+
1210
+
1211
+ # Circle collides_with ...
1212
+ def _circle_collides_with_point(
1213
+ c1: Circle, p2: Vector2, move_both: bool = False
1214
+ ) -> tuple[Vector2, Vector2]:
1215
+ raise NotImplementedError
1216
+
1217
+
1218
+ def _circle_collides_with_line(
1219
+ c1: Circle, l2: Line, move_both: bool = False
1220
+ ) -> tuple[Vector2, Vector2]:
1221
+ raise NotImplementedError
1222
+
1223
+
1224
+ def _circle_collides_with_rect(
1225
+ c: Circle, r: Rect, move_both: bool = False
1226
+ ) -> tuple[Vector2, Vector2]:
1227
+ new_pos1 = Vector2(c.pos.x, c.pos.y)
1228
+ new_pos2 = Vector2(r.pos.x, r.pos.y)
1229
+
1230
+ closest_point = closest(r, c)
1231
+ # ray = closest_point - c.pos
1232
+ # overlap = c.radius - ray.magnitude()
1233
+ # TODO: std::isnan(overlap): overlap=0
1234
+ # try:
1235
+ # ray.normalize()
1236
+ # except ValueError:
1237
+ # # breakpoint()
1238
+ # overlap = 0
1239
+
1240
+ # if overlap > 0:
1241
+ # if move_both:
1242
+ # new_pos1 -= ray.normalize() * overlap * 0.5
1243
+ # new_pos2 += ray.normalize() * overlap * 0.5
1244
+ # else:
1245
+ # new_pos1 -= ray.normalize() * overlap
1246
+
1247
+ delta = c.pos - closest_point
1248
+ dist_sq = delta.length_squared()
1249
+ if dist_sq == 0:
1250
+ dx = min(abs(c.pos.x - r.left().pos.x), abs(c.pos.x - r.right().pos.x))
1251
+ dy = min(abs(c.pos.y - r.top().pos.y), abs(c.pos.y - r.bottom().pos.y))
1252
+ if dx < dy:
1253
+ normal = Vector2(1, 0) if c.pos.x > r.middle().x else Vector2(-1, 0)
1254
+ else:
1255
+ normal = Vector2(0, 1) if c.pos.y > r.middle().y else Vector2(0, -1)
1256
+ distance = 0
1257
+ else:
1258
+ distance = math.sqrt(dist_sq)
1259
+ normal = delta / distance
1260
+
1261
+ penetration = c.radius - distance
1262
+ if move_both:
1263
+ new_pos1 += normal * penetration * 0.5
1264
+ new_pos2 -= normal * penetration * 0.5
1265
+ else:
1266
+ new_pos1 += normal * penetration
1267
+ return new_pos1, new_pos2
1268
+
1269
+
1270
+ def _circle_collides_with_circle(
1271
+ c1: Circle, c2: Circle, move_both: bool = False
1272
+ ) -> tuple[Vector2, Vector2]:
1273
+ new_pos1 = Vector2(c1.pos.x, c1.pos.y)
1274
+ new_pos2 = Vector2(c2.pos.x, c2.pos.y)
1275
+
1276
+ # distance = (c1.pos - c2.pos).magnitude()
1277
+ # overlap = distance - c1.radius - c2.radius
1278
+
1279
+ # offset = c1.pos - c2.pos
1280
+
1281
+ # if distance == 0:
1282
+ # offset = Vector2(overlap, overlap)
1283
+ # else:
1284
+ # offset *= overlap / distance
1285
+
1286
+ # if move_both:
1287
+ # new_pos1 -= offset * 0.5
1288
+ # new_pos2 += offset * 0.5
1289
+ # else:
1290
+ # new_pos1 -= offset
1291
+ delta = c1.pos - c2.pos
1292
+ dist_sq = delta.length_squared()
1293
+
1294
+ if dist_sq == 0:
1295
+ # Deterministic fallback
1296
+ normal = Vector2(1, 0) if id(c1) < id(c2) else Vector2(-1, 0)
1297
+ distance = 0
1298
+ else:
1299
+ distance = math.sqrt(dist_sq)
1300
+ normal = delta / distance
1301
+
1302
+ penetration = c1.radius + c2.radius - distance
1303
+ if penetration <= 0:
1304
+ return new_pos1, new_pos2
1305
+
1306
+ if move_both:
1307
+ correction = normal * (penetration * 0.5)
1308
+ return new_pos1 + correction, new_pos2 - correction
1309
+
1310
+ return new_pos1 + normal * penetration, new_pos2
1311
+
1312
+
1313
+ def vmax(val: Vector2, other: Vector2) -> Vector2:
1314
+ return Vector2(max(val.x, other.x), max(val.y, other.y))
1315
+
1316
+
1317
+ def vmin(val: Vector2, other: Vector2) -> Vector2:
1318
+ return Vector2(min(val.x, other.x), min(val.y, other.y))