pyverse2d 0.4.2__tar.gz

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 (67) hide show
  1. pyverse2d-0.4.2/LICENSE +21 -0
  2. pyverse2d-0.4.2/PKG-INFO +16 -0
  3. pyverse2d-0.4.2/README.md +2 -0
  4. pyverse2d-0.4.2/pyproject.toml +23 -0
  5. pyverse2d-0.4.2/pyverse2d/__init__.py +72 -0
  6. pyverse2d-0.4.2/pyverse2d/_flag/__init__.py +9 -0
  7. pyverse2d-0.4.2/pyverse2d/_flag/_stack_mode.py +8 -0
  8. pyverse2d-0.4.2/pyverse2d/_flag/_update_phase.py +8 -0
  9. pyverse2d-0.4.2/pyverse2d/_internal/__init__.py +19 -0
  10. pyverse2d-0.4.2/pyverse2d/_internal/validators.py +247 -0
  11. pyverse2d-0.4.2/pyverse2d/_rendering/__init__.py +13 -0
  12. pyverse2d-0.4.2/pyverse2d/_rendering/_camera.py +138 -0
  13. pyverse2d-0.4.2/pyverse2d/_rendering/_pipeline.py +61 -0
  14. pyverse2d-0.4.2/pyverse2d/_rendering/_screen.py +45 -0
  15. pyverse2d-0.4.2/pyverse2d/_rendering/_viewport.py +111 -0
  16. pyverse2d-0.4.2/pyverse2d/_rendering/_window.py +212 -0
  17. pyverse2d-0.4.2/pyverse2d/_version.py +5 -0
  18. pyverse2d-0.4.2/pyverse2d/abc/__init__.py +23 -0
  19. pyverse2d-0.4.2/pyverse2d/abc/_asset.py +11 -0
  20. pyverse2d-0.4.2/pyverse2d/abc/_component.py +8 -0
  21. pyverse2d-0.4.2/pyverse2d/abc/_layer.py +23 -0
  22. pyverse2d-0.4.2/pyverse2d/abc/_math_object.py +39 -0
  23. pyverse2d-0.4.2/pyverse2d/abc/_shape.py +45 -0
  24. pyverse2d-0.4.2/pyverse2d/abc/_system.py +21 -0
  25. pyverse2d-0.4.2/pyverse2d/asset/__init__.py +11 -0
  26. pyverse2d-0.4.2/pyverse2d/asset/_color.py +95 -0
  27. pyverse2d-0.4.2/pyverse2d/asset/_image.py +114 -0
  28. pyverse2d-0.4.2/pyverse2d/asset/_text.py +74 -0
  29. pyverse2d-0.4.2/pyverse2d/map/__init__.py +3 -0
  30. pyverse2d-0.4.2/pyverse2d/math/__init__.py +11 -0
  31. pyverse2d-0.4.2/pyverse2d/math/_line.py +307 -0
  32. pyverse2d-0.4.2/pyverse2d/math/_point.py +256 -0
  33. pyverse2d-0.4.2/pyverse2d/math/_vector.py +330 -0
  34. pyverse2d-0.4.2/pyverse2d/scene/__init__.py +76 -0
  35. pyverse2d-0.4.2/pyverse2d/scene/_scene.py +117 -0
  36. pyverse2d-0.4.2/pyverse2d/scene/_world_layer.py +48 -0
  37. pyverse2d-0.4.2/pyverse2d/shape/__init__.py +17 -0
  38. pyverse2d-0.4.2/pyverse2d/shape/_capsule.py +117 -0
  39. pyverse2d-0.4.2/pyverse2d/shape/_circle.py +96 -0
  40. pyverse2d-0.4.2/pyverse2d/shape/_ellipse.py +117 -0
  41. pyverse2d-0.4.2/pyverse2d/shape/_polygon.py +161 -0
  42. pyverse2d-0.4.2/pyverse2d/shape/_rect.py +110 -0
  43. pyverse2d-0.4.2/pyverse2d/shape/_segment.py +137 -0
  44. pyverse2d-0.4.2/pyverse2d/tool/__init__.py +7 -0
  45. pyverse2d-0.4.2/pyverse2d/tool/_walls_definer.py +7 -0
  46. pyverse2d-0.4.2/pyverse2d/ui/__init__.py +0 -0
  47. pyverse2d-0.4.2/pyverse2d/world/__init__.py +37 -0
  48. pyverse2d-0.4.2/pyverse2d/world/_component/__init__.py +17 -0
  49. pyverse2d-0.4.2/pyverse2d/world/_component/_collider.py +103 -0
  50. pyverse2d-0.4.2/pyverse2d/world/_component/_rigid_body.py +212 -0
  51. pyverse2d-0.4.2/pyverse2d/world/_component/_shape_renderer.py +94 -0
  52. pyverse2d-0.4.2/pyverse2d/world/_component/_sprite_renderer.py +95 -0
  53. pyverse2d-0.4.2/pyverse2d/world/_component/_text_renderer.py +95 -0
  54. pyverse2d-0.4.2/pyverse2d/world/_component/_transform.py +130 -0
  55. pyverse2d-0.4.2/pyverse2d/world/_entity.py +159 -0
  56. pyverse2d-0.4.2/pyverse2d/world/_system/__init__.py +13 -0
  57. pyverse2d-0.4.2/pyverse2d/world/_system/_collision.py +892 -0
  58. pyverse2d-0.4.2/pyverse2d/world/_system/_gravity.py +57 -0
  59. pyverse2d-0.4.2/pyverse2d/world/_system/_physics.py +44 -0
  60. pyverse2d-0.4.2/pyverse2d/world/_system/_render.py +249 -0
  61. pyverse2d-0.4.2/pyverse2d/world/_world.py +179 -0
  62. pyverse2d-0.4.2/pyverse2d.egg-info/PKG-INFO +16 -0
  63. pyverse2d-0.4.2/pyverse2d.egg-info/SOURCES.txt +65 -0
  64. pyverse2d-0.4.2/pyverse2d.egg-info/dependency_links.txt +1 -0
  65. pyverse2d-0.4.2/pyverse2d.egg-info/requires.txt +1 -0
  66. pyverse2d-0.4.2/pyverse2d.egg-info/top_level.txt +1 -0
  67. pyverse2d-0.4.2/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 WhiteWolf45380
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyverse2d
3
+ Version: 0.4.2
4
+ Summary: 2D Game Engine using pyglet (OpenGL) for rendering
5
+ Author: WhiteWolf45380
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/WhiteWolf45380/pyverse2d
8
+ Keywords: game,engine,2d,pyglet,opengl
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: pyglet>=2.0
13
+ Dynamic: license-file
14
+
15
+ pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip
16
+
@@ -0,0 +1,2 @@
1
+ pip install https://github.com/WhiteWolf45380/PyVerse2D/archive/refs/heads/main.zip
2
+
@@ -0,0 +1,23 @@
1
+ [build-system]
2
+ requires = ["setuptools>=65.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyverse2d"
7
+ version = "0.4.2"
8
+ description = "2D Game Engine using pyglet (OpenGL) for rendering"
9
+ authors = [{ name = "WhiteWolf45380" }]
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.10"
13
+ keywords = ["game", "engine", "2d", "pyglet", "opengl"]
14
+ dependencies = [
15
+ "pyglet>=2.0",
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://github.com/WhiteWolf45380/pyverse2d"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
23
+ include = ["pyverse2d*"]
@@ -0,0 +1,72 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from __future__ import annotations
3
+
4
+ from . import abc, math, shape, asset, world, map, ui, scene
5
+
6
+ from ._rendering import Camera, Viewport, Screen, Window
7
+
8
+ import pyglet
9
+
10
+ # ======================================== STATE ========================================
11
+ _window: Window | None = None
12
+ _fps: int = 60
13
+
14
+ # ======================================== SETTERS ========================================
15
+ def set_window(window: Window):
16
+ """
17
+ Définit la fenêtre du moteur
18
+
19
+ Args:
20
+ window (Window): fenêtre à utiliser
21
+ """
22
+ global _window
23
+ if not isinstance(window, Window):
24
+ raise TypeError("Expected a Window instance")
25
+ _window = window
26
+
27
+ @_window.native.event
28
+ def on_draw():
29
+ _window.clear()
30
+ scene.draw()
31
+
32
+ def set_fps(fps: int):
33
+ """
34
+ Définit le nombre de mises à jour par seconde
35
+
36
+ Args:
37
+ fps (int): fps cible
38
+ """
39
+ global _fps
40
+ _fps = int(fps)
41
+
42
+ # ======================================== LOOP ========================================
43
+ def run():
44
+ """Démarre la boucle de mise à jour"""
45
+ if _window is None:
46
+ raise RuntimeError("No window set — call engine.set_window() before engine.run()")
47
+ pyglet.clock.schedule_interval(_update, 1 / _fps)
48
+ pyglet.app.run()
49
+
50
+ def _update(dt: float):
51
+ scene.update(dt)
52
+
53
+ # ======================================== EXPORTS ========================================
54
+ __all__ = [
55
+ "Camera",
56
+ "Viewport",
57
+ "Screen",
58
+ "Window",
59
+
60
+ "abc",
61
+ "math",
62
+ "shape",
63
+ "asset",
64
+ "world",
65
+ "map",
66
+ "ui",
67
+ "scene",
68
+
69
+ "set_window",
70
+ "set_fps",
71
+ "run",
72
+ ]
@@ -0,0 +1,9 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from ._stack_mode import StackMode
3
+ from ._update_phase import UpdatePhase
4
+
5
+ # ======================================== EXPORTS ========================================
6
+ __all__ = [
7
+ "StackMode",
8
+ "UpdatePhase",
9
+ ]
@@ -0,0 +1,8 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from enum import Enum
3
+
4
+ # ======================================== FLAG ========================================
5
+ class StackMode(Enum):
6
+ PAUSE = "pause" # scene du dessous stop tout
7
+ SUSPEND = "suspend" # scene du dessous stop update mais continue draw
8
+ OVERLAY = "overlay" # scene du dessous continue tout
@@ -0,0 +1,8 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from enum import Enum
3
+
4
+ # ======================================== FLAG ========================================
5
+ class UpdatePhase(Enum):
6
+ EARLY = 0 # Pré-actualisation
7
+ UPDATE = 1 # Actualisation
8
+ LATE = 2 # Post-actualisation
@@ -0,0 +1,19 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from .validators import (
3
+ typename,
4
+ expect,
5
+ not_null,
6
+ positive,
7
+ clamped,
8
+ rgba
9
+ )
10
+
11
+ # ======================================== EXPORTS ========================================
12
+ __all__ = [
13
+ "typename",
14
+ "expect",
15
+ "not_null",
16
+ "positive",
17
+ "clamped",
18
+ "rgba",
19
+ ]
@@ -0,0 +1,247 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from types import UnionType
3
+ from typing import Tuple, get_args, get_origin, Union
4
+ from numbers import Real
5
+
6
+ # ======================================== TYPE CHECK ========================================
7
+ def typename(t: type):
8
+ """
9
+ Renvoie le str du type
10
+
11
+ Args:
12
+ t(type): type à vérifier
13
+ """
14
+ return getattr(t, "__name__", str(t))
15
+
16
+
17
+ def expect(value: object, types: type | Tuple[type, ...]):
18
+ """
19
+ Vérifie la valeur contre un type supporté :
20
+ T type simple
21
+ (T1, T2, T3) multi-types
22
+ T1 | T2 union
23
+ list[T] liste typée
24
+ set[T] set typé
25
+ tuple[T] tuple broadcast
26
+ tuple[T1, T2, T3] tuple positionnel
27
+ dict[K, V] dictionnaire typé
28
+
29
+ Args:
30
+ value(object): valeur à vérifier
31
+ types(type|Tuple[type, ...]): types à vérifier
32
+
33
+ Returns:
34
+ value(object): si valide
35
+
36
+ Raises:
37
+ TypeError: si la valeur n'est pas conforme
38
+ """
39
+ # (T1, T2, T3)
40
+ if isinstance(types, tuple):
41
+ types = tuple(type(None) if t is None else t for t in types)
42
+ if not isinstance(value, types):
43
+ readable = " | ".join(typename(t) for t in types)
44
+ raise TypeError(f"expected ({readable}), got ({typename(value)})")
45
+ return value
46
+
47
+ # T
48
+ origin = get_origin(types)
49
+ if origin is None:
50
+ types = type(None) if types is None else types
51
+ if not isinstance(value, types):
52
+ raise TypeError(f"expected ({typename(types)}), got ({typename(value)})")
53
+ return value
54
+
55
+ # T1 |T2
56
+ args = get_args(types)
57
+ if origin in (UnionType, Union):
58
+ for t in args:
59
+ try:
60
+ return expect(value, t)
61
+ except TypeError:
62
+ continue
63
+ readable = " | ".join(typename(t) for t in args)
64
+ raise TypeError(f"expected ({readable}), got ({typename(value)})")
65
+
66
+ # list[T] / set[T] / frozenset[T]
67
+ if origin in (list, set, frozenset):
68
+ if not isinstance(value, origin):
69
+ raise TypeError(f"expected ({typename(origin)}), got ({typename(value)})")
70
+ inner = args[0]
71
+ if get_origin(inner) is None:
72
+ for i, v in enumerate(value):
73
+ if not isinstance(v, inner):
74
+ raise TypeError(f"at index {i}: expected ({typename(inner)}), got ({typename(v)})")
75
+ else:
76
+ for i, v in enumerate(value):
77
+ try:
78
+ expect(v, inner)
79
+ except TypeError as e:
80
+ raise TypeError(f"at index {i}: {e}") from e
81
+ return value
82
+
83
+ # tuple[T] / tuple[T1, T2, T3]
84
+ if origin is tuple:
85
+ if not isinstance(value, tuple):
86
+ raise TypeError(f"expected (tuple), got ({typename(value)})")
87
+ if len(args) == 1 or len(args) == 2 and args[1] is Ellipsis:
88
+ inner = args[0]
89
+ if get_origin(inner) is None:
90
+ for i, v in enumerate(value):
91
+ if not isinstance(v, inner):
92
+ raise TypeError(f"at index {i}: expected ({typename(inner)}), got ({typename(v)})")
93
+ else:
94
+ for i, v in enumerate(value):
95
+ try:
96
+ expect(v, inner)
97
+ except TypeError as e:
98
+ raise TypeError(f"at index {i}: {e}") from e
99
+ else:
100
+ if len(value) != len(args):
101
+ raise TypeError(f"expected (tuple) of len {len(args)}, got {len(value)}")
102
+ for i, (v, t) in enumerate(zip(value, args)):
103
+ if get_origin(t) is None:
104
+ if not isinstance(v, t):
105
+ raise TypeError(f"at index {i}: expected ({typename(t)}), got ({typename(v)})")
106
+ else:
107
+ try:
108
+ expect(v, t)
109
+ except TypeError as e:
110
+ raise TypeError(f"at index {i}: {e}") from e
111
+ return value
112
+
113
+ # dict[K, V]
114
+ if origin is dict:
115
+ if not isinstance(value, dict):
116
+ raise TypeError(f"expected (dict), got ({typename(value)})")
117
+ kt, vt = args
118
+ kt_simple = get_origin(kt) is None
119
+ vt_simple = get_origin(vt) is None
120
+ for k, v in value.items():
121
+ if kt_simple:
122
+ if not isinstance(k, kt):
123
+ raise TypeError(f"at key {k!r}: invalid key: expected ({typename(kt)}), got ({typename(k)})")
124
+ else:
125
+ try:
126
+ expect(k, kt)
127
+ except TypeError as e:
128
+ raise TypeError(f"at key {k!r}: invalid key: {e}") from e
129
+ if vt_simple:
130
+ if not isinstance(v, vt):
131
+ raise TypeError(f"at key {k!r}: expected ({typename(vt)}), got ({typename(v)})")
132
+ else:
133
+ try:
134
+ expect(v, vt)
135
+ except TypeError as e:
136
+ raise TypeError(f"at key {k!r}: {e}") from e
137
+ return value
138
+
139
+ raise TypeError(f"unsupported type annotation: {types!r}")
140
+
141
+ # ======================================== VALUE CHECK ========================================
142
+ def not_null(value: object, arg: str = "Argument"):
143
+ """
144
+ Vérifie que la valeur ne soit pas nulle
145
+
146
+ Args:
147
+ value(object): valeur à vérifier
148
+ arg(str): nom de l'argument à vérifier
149
+ """
150
+ # None
151
+ if value is None:
152
+ raise ValueError(f"{arg} cannot be None")
153
+
154
+ # Nombre
155
+ if isinstance(value, (int, float, complex)):
156
+ if value == 0:
157
+ raise ValueError(f"{arg} cannot be None")
158
+ return value
159
+
160
+ # Types composés
161
+ if isinstance(value, (str, list, tuple, set, dict, frozenset)):
162
+ if len(value) == 0:
163
+ raise ValueError(f"{arg} cannot be empty")
164
+ return value
165
+
166
+ # Objet possédant une méthode __len__
167
+ if hasattr(value, "__len__"):
168
+ if len(value) == 0:
169
+ raise ValueError(f"{arg} cannot be empty")
170
+ return value
171
+
172
+ # Objet custom
173
+ return value
174
+
175
+ def positive(value: object, arg: str = "Argument"):
176
+ """
177
+ Vérifie que la valeur soit positive
178
+
179
+ Args:
180
+ value(object): valeur à vérifier
181
+ arg(str): nom de l'argument à vérifier
182
+ """
183
+ # Nombres
184
+ if isinstance(value, Real):
185
+ if float(value) < 0:
186
+ raise ValueError(f"{arg} cannot be negative")
187
+ return value
188
+
189
+ # Par défaut
190
+ return value
191
+
192
+ def clamped(value: object, min: float = 0.0, max: float = 1.0, arg: str ="Argument"):
193
+ """
194
+ Vérifie que la valeur soit comprise entre min et max
195
+
196
+ Args:
197
+ value(object): valeur à vérifier
198
+ min(float, optional): valeur minimale autorisée
199
+ max(float, optional): valeur maximale autorisée
200
+ arg(str, optional): nom de l'argument à vérifier
201
+ """
202
+ # Nombres
203
+ if isinstance(value, Real):
204
+ if float(value) < min or float(value) > max:
205
+ raise ValueError(f"{arg} must be between {min} and {max}")
206
+ return value
207
+
208
+ # Par défaut
209
+ return value
210
+
211
+ # ======================================== CONVERSIONS ========================================
212
+ def rgba(value: object, argument: str = "Argument") -> tuple[int, int, int, float]:
213
+ """
214
+ Renvoie, si cela est possible, la valeur en couleur rgba (255, 255, 255, 1.0)
215
+
216
+ Args:
217
+ value(object): valeur à convertir
218
+ """
219
+ # Type
220
+ if type(value) is not tuple:
221
+ raise TypeError(f"{argument} doit être un tuple")
222
+
223
+ # Taille
224
+ n = len(value)
225
+ if n == 3:
226
+ r, g, b = value
227
+ a = 1.0
228
+ elif n == 4:
229
+ r, g, b, a = value
230
+ elif n < 3:
231
+ v = value + (0, 0, 0, 1.0)
232
+ r, g, b, a = v[0], v[1], v[2], v[3]
233
+ else:
234
+ raise ValueError(f"{argument} doit être un tuple de longueur <= 4")
235
+
236
+ # RGB
237
+ if type(r) is float: r = int(r * 255 + 0.5)
238
+ if type(g) is float: g = int(g * 255 + 0.5)
239
+ if type(b) is float: b = int(b * 255 + 0.5)
240
+
241
+ # Alpha
242
+ if type(a) is int:
243
+ a = a / 255
244
+ else:
245
+ a = float(a)
246
+
247
+ return r, g, b, a
@@ -0,0 +1,13 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from ._camera import Camera
3
+ from ._viewport import Viewport
4
+ from ._screen import Screen
5
+ from ._window import Window
6
+
7
+ # ======================================== EXPORTS ========================================
8
+ __all__ = [
9
+ "Camera",
10
+ "Viewport",
11
+ "Screen",
12
+ "Window",
13
+ ]
@@ -0,0 +1,138 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from __future__ import annotations
3
+
4
+ from .._internal import expect, not_null, positive
5
+ from ..math import Point, Vector
6
+ from ..world import Entity, Transform
7
+
8
+ from pyglet.math import Mat4, Vec3
9
+ from numbers import Real
10
+
11
+ # ======================================== CAMERA ========================================
12
+ class Camera:
13
+ """
14
+ Définit le point de vue dans le monde
15
+
16
+ Args:
17
+ pos(Point): position de la caméra
18
+ zoom (Real): facteur de zoom
19
+ """
20
+ __slots__ = ("_pos", "_following", "_offset", "_zoom")
21
+
22
+ def __init__(self, pos: Point = (0.0, 0.0), zoom: Real = 1.0):
23
+ self._pos: Point = Point(pos)
24
+ self._following: Entity | None = None
25
+ self._offset: Vector = Vector(0.0, 0.0)
26
+ self._zoom: float = float(positive(not_null(expect(zoom, Real))))
27
+
28
+ # ======================================== GETTERS ========================================
29
+ @property
30
+ def x(self) -> float:
31
+ """Renvoie la position horizontale"""
32
+ return self._pos.x
33
+
34
+ @property
35
+ def y(self) -> float:
36
+ """Renvoie la position verticale"""
37
+ return self._pos.y
38
+
39
+ @property
40
+ def pos(self) -> Point:
41
+ """Renvoie la position"""
42
+ return self._pos
43
+
44
+ @property
45
+ def offset(self) -> Vector:
46
+ """Renvoie le vecteur de décalage à la position"""
47
+ return self._offset
48
+
49
+ @property
50
+ def final_x(self) -> float:
51
+ """Position horizontale finale"""
52
+ self._check_follow()
53
+ base = self._following.get(Transform).x if self._following else self._pos.x
54
+ return base + self._offset.x
55
+
56
+ @property
57
+ def final_y(self) -> float:
58
+ """Position verticale finale"""
59
+ self._check_follow()
60
+ base = self._following.get(Transform).y if self._following else self._pos.y
61
+ return base + self._offset.y
62
+
63
+ @property
64
+ def final_pos(self) -> Point:
65
+ """Renvoie la position finale"""
66
+ self._check_follow()
67
+ base = self._following.get(Transform).pos if self._following else self._pos
68
+ return base + self._offset
69
+
70
+ @property
71
+ def zoom(self) -> float:
72
+ return self._zoom
73
+
74
+ # ======================================== SETTERS ========================================
75
+ @x.setter
76
+ def x(self, value: Real):
77
+ self._pos.x = float(expect(value, Real))
78
+
79
+ @y.setter
80
+ def y(self, value: Real):
81
+ self._pos.y = float(expect(value, Real))
82
+
83
+ @pos.setter
84
+ def pos(self, value: Point):
85
+ self._pos = Point(value)
86
+
87
+ @zoom.setter
88
+ def zoom(self, value: Real):
89
+ if float(value) <= 0:
90
+ raise ValueError("Zoom must be greater than 0")
91
+ self._zoom = float(value)
92
+
93
+ # ======================================== FOLLOW ========================================
94
+ def follow(self, entity: Entity):
95
+ """
96
+ Suit le Transform d'une entité
97
+
98
+ Args:
99
+ entity (Entity): entité à suivre
100
+ """
101
+ if not entity.has(Transform):
102
+ raise ValueError(f"Entity {entity.id[:8]}... has no Transform component")
103
+ self._following = entity
104
+
105
+ def unfollow(self):
106
+ """Détache la camera de l'entité suivie"""
107
+ self._following = None
108
+
109
+ def _check_follow(self):
110
+ """Unfollow automatique si l'entité est inactive"""
111
+ if self._following is not None and not self._following.is_active():
112
+ self._following = None
113
+
114
+ # ======================================== DÉPLACEMENT ========================================
115
+ def move(self, vector: Vector):
116
+ """
117
+ Déplace la position manuelle de la camera
118
+
119
+ Args:
120
+ vectorr(Vector): vecteur de translation
121
+ """
122
+ self._pos += Vector(vector)
123
+
124
+ # ======================================== RENDU ========================================
125
+ def view_matrix(self, virtual_width: int, virtual_height: int) -> Mat4:
126
+ """
127
+ Produit la matrice de vue à appliquer à la fenêtre
128
+
129
+ Args:
130
+ virtual_width (int): largeur de l'espace virtuel
131
+ virtual_height (int): hauteur de l'espace virtuel
132
+ """
133
+ cx = virtual_width / 2
134
+ cy = virtual_height / 2
135
+ fx, fy = self.final_pos
136
+ translate = Mat4.from_translation(Vec3(cx - fx, cy - fy, 0))
137
+ scale = Mat4.from_scale(Vec3(self._zoom, self._zoom, 1))
138
+ return translate @ scale
@@ -0,0 +1,61 @@
1
+ # ======================================== IMPORTS ========================================
2
+ from __future__ import annotations
3
+
4
+ import pyglet
5
+ import pyglet.gl as gl
6
+ from pyglet.graphics import Batch, Group
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from ..scene import Scene
12
+
13
+ # ======================================== RENDERER ========================================
14
+ class Pipeline:
15
+ """
16
+ Pipeline de rendu exploitant OpenGL
17
+
18
+ Args:
19
+ virtual_width (int): largeur de l'espace virtuel
20
+ virtual_height (int): hauteur de l'espace virtuel
21
+ """
22
+
23
+ def __init__(self, virtual_width: int, virtual_height: int):
24
+ self._virtual_width: int = int(virtual_width)
25
+ self._virtual_height: int = int(virtual_height)
26
+ self._batch: Batch = Batch()
27
+ self._groups: dict[int, Group] = {}
28
+
29
+ # ======================================== GETTERS ========================================
30
+ @property
31
+ def batch(self) -> Batch:
32
+ """Renvoie le batch global"""
33
+ return self._batch
34
+
35
+ def get_group(self, z: int = 0) -> Group:
36
+ """
37
+ Renvoie le Group associé au z_order, le crée si inexistant
38
+
39
+ Args:
40
+ z (int): z_order du group
41
+ """
42
+ if z not in self._groups:
43
+ self._groups[z] = Group(order=z)
44
+ return self._groups[z]
45
+
46
+ # ======================================== PIPELINE ========================================
47
+ def begin(self, scene: Scene):
48
+ """
49
+ Configure le contexte de rendu depuis la scene active.
50
+ Applique la caméra et le viewport.
51
+
52
+ Args:
53
+ scene (Scene): scene à rendre
54
+ """
55
+ x, y, w, h = scene.viewport.resolve(self._virtual_width, self._virtual_height)
56
+ gl.glViewport(int(x), int(y), int(w), int(h))
57
+ pyglet.get_default_window().view = scene.camera.view_matrix(self._virtual_width, self._virtual_height)
58
+
59
+ def flush(self):
60
+ """Envoie tout le batch au GPU"""
61
+ self._batch.draw()