PyScene 1.0.0__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.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.pyc
3
+
4
+ .venv/
5
+ .env
6
+
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+
11
+ .vscode/
12
+ .idea/
pyscene-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
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.
pyscene-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.5
2
+ Name: PyScene
3
+ Version: 1.0.0
4
+ Summary: A small example package
5
+ Project-URL: Homepage, https://github.com/sfertig/PyScene
6
+ Project-URL: Issues, https://github.com/sfertig/PyScene/issues
7
+ Author-email: Sam Fertig <sfertig007@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # PyScene
16
+
17
+ PyScene is a custom python + pygame framework / engine developed to streamline 2d game development.
18
+
@@ -0,0 +1,4 @@
1
+ # PyScene
2
+
3
+ PyScene is a custom python + pygame framework / engine developed to streamline 2d game development.
4
+
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling >= 1.26"]
3
+ build-backend = "hatchling.build"
4
+ [project]
5
+ name = "PyScene"
6
+ version = "1.0.0"
7
+ authors = [
8
+ { name="Sam Fertig", email="sfertig007@gmail.com" },
9
+ ]
10
+ description = "A small example package"
11
+ readme = "README.md"
12
+ requires-python = ">=3.9"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ ]
17
+ license = "MIT"
18
+ license-files = ["LICEN[CS]E*"]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/sfertig/PyScene"
22
+ Issues = "https://github.com/sfertig/PyScene/issues"
@@ -0,0 +1,62 @@
1
+ import pygame
2
+
3
+ #collisions generators
4
+
5
+ def _coll_gen_combination(Game): #this functions as the main function for collision rect generation
6
+ if not Game._update_coll: return
7
+ # -- else start collisions generation -- #
8
+ Game.collisions.clear() #clear existing rects
9
+
10
+ #get rects
11
+ rects: list[pygame.Rect] = []
12
+ for col in Game.col: rects.append(col.rect.copy())
13
+
14
+ if Game.optimize_static_colliders: rects = _optimize_static_colliders(Game, rects)
15
+
16
+ #set collisions
17
+ Game.collisions = rects
18
+
19
+ #add dynamic rects
20
+ for col in Game.dynamic_col:
21
+ if col.active: rects.append(col.rect)
22
+
23
+ Game._update_coll = False
24
+
25
+ def _optimize_static_colliders(Game, colliders: list[pygame.Rect]):
26
+ if not colliders:
27
+ return []
28
+
29
+ rects = sorted([r.copy() for r in colliders], key=lambda r: (r.y, r.x))
30
+ merged = []
31
+
32
+ # 2. Horizontal Merge Pass
33
+ while rects:
34
+ current = rects.pop(0)
35
+ i = 0
36
+ while i < len(rects):
37
+ other = rects[i]
38
+ if current.top == other.top and current.height == other.height and current.right == other.x:
39
+ current.width += other.width
40
+ rects.pop(i)
41
+ else:
42
+ i += 1
43
+ merged.append(current)
44
+
45
+ # 3. Vertical Merge Pass
46
+ final_rects = []
47
+ merged = sorted(merged, key=lambda r: (r.x, r.y))
48
+ while merged:
49
+ current = merged.pop(0)
50
+ i = 0
51
+ while i < len(merged):
52
+ other = merged[i]
53
+ if current.left == other.left and current.width == other.width and current.bottom == other.y:
54
+ current.height += other.height
55
+ merged.pop(i)
56
+ else:
57
+ i += 1
58
+ final_rects.append(current)
59
+
60
+ return final_rects
61
+
62
+
@@ -0,0 +1,297 @@
1
+ import pygame
2
+ import sys
3
+
4
+ from .Keys import Keys
5
+ from .Math import Vector2D
6
+ from .Collisions import _coll_gen_combination
7
+
8
+ class _game:
9
+ def __init__(self):
10
+ self.__on = False
11
+ pygame.init()
12
+ pygame.font.init()
13
+
14
+ def init(self, width, height, title, bg="black", fps=60, fullscreen=False):
15
+ self.__on = True
16
+ self.width = width
17
+ self.height = height
18
+ self.title = title
19
+ self.bg = bg
20
+ self.fps = fps
21
+ self.clock = pygame.time.Clock()
22
+ self.dt = 0.0
23
+ self.mouse_down = False
24
+ self.click = False
25
+ self.events = []
26
+ self.cam = Camera(self)
27
+
28
+ self.show_collisions: bool = False
29
+ self.optimize_static_colliders: bool = False
30
+
31
+ #screen
32
+ if not fullscreen: self.screen = pygame.display.set_mode((self.width, self.height))
33
+ else: self.screen = pygame.display.set_mode((width, height), pygame.SCALED | pygame.FULLSCREEN)
34
+ pygame.display.set_caption(self.title)
35
+
36
+ Keys._add_net(self) #to accsess events
37
+
38
+ #game obj info
39
+ self.objects = []
40
+ self.queue_creation = []
41
+ self.queue_del = []
42
+
43
+ self.collisions: list[pygame.Rect] = [] #accual list of rects
44
+ self.col = []
45
+ self.queue_creation_coll = []
46
+ self.queue_del_coll = []
47
+ self._update_coll = False
48
+
49
+ self.dynamic_col = []
50
+ self.queue_creation_D_coll = []
51
+ self.queue_del_D_coll = []
52
+
53
+ self.render_queue = []
54
+
55
+ self.active_scene = None
56
+ self.scenes = {}
57
+ self.id = 0
58
+
59
+
60
+ def _queue_add(self, obj):
61
+ if self.active_scene is None: self.queue_creation.append(obj)
62
+ else: self.active_scene._queue_add(obj)
63
+ def _queue_remove(self, obj):
64
+ if obj in self.objects: self.objects.remove(obj)
65
+ else: self.active_scene._queue_remove(obj)
66
+ def _get_id(self):
67
+ self.id += 1
68
+ return self.id
69
+
70
+ def _queue_add_coll(self, obj): self.queue_creation_coll.append(obj)
71
+ def _queue_remove_coll(self, obj): self.queue_del_coll.append(obj)
72
+ def _queue_add_D_coll(self, obj): self.queue_creation_D_coll.append(obj)
73
+ def _queue_remove_D_coll(self, obj): self.queue_del_D_coll.append(obj)
74
+
75
+ def __update_render_queue(self): #update order of the renders based on z-index
76
+ self.render_queue = sorted(self.objects, key=lambda obj: obj.z_index)
77
+
78
+ def __handle_queue(self):
79
+ for obj in self.queue_creation: self.objects.append(obj)
80
+ for obj in self.queue_del: self.objects.remove(obj)
81
+ if len(self.queue_creation) > 0 or len(self.queue_del) > 0: self.__update_render_queue()
82
+ self.queue_creation.clear()
83
+ self.queue_del.clear()
84
+ #collisions
85
+ for obj in self.queue_creation_coll: self.col.append(obj)
86
+ for obj in self.queue_del_coll: self.col.remove(obj)
87
+ self.queue_creation_coll.clear()
88
+ self.queue_del_coll.clear()
89
+ for obj in self.queue_creation_D_coll: self.dynamic_col.append(obj)
90
+ for obj in self.queue_del_D_coll: self.dynamic_col.remove(obj)
91
+ self.queue_creation_D_coll.clear()
92
+ self.queue_del_D_coll.clear()
93
+ _coll_gen_combination(self)
94
+ self._update_coll = False
95
+
96
+ def __update(self):
97
+ self.click = False
98
+ self.dt = self.clock.tick(self.fps)/1000.0
99
+ self.events = pygame.event.get()
100
+
101
+ for obj in self.dynamic_col: obj.update()
102
+
103
+ for event in self.events:
104
+ if event.type == pygame.QUIT:
105
+ self.quit()
106
+ if event.type == pygame.MOUSEBUTTONDOWN:
107
+ if event.button == 1:
108
+ self.mouse_down = True
109
+ if event.type == pygame.MOUSEBUTTONUP and self.mouse_down:
110
+ if event.button == 1:
111
+ self.mouse_down = False
112
+ self.click = True
113
+
114
+ def update(self, _clear=True):
115
+ if not self.__on: return
116
+ self.__update()
117
+ self.__handle_queue()
118
+
119
+ #update objs
120
+ for obj in self.objects: obj.update()
121
+ #update scene
122
+ if self.active_scene is not None:
123
+ self.active_scene.update()
124
+ self.active_scene._update()
125
+
126
+ if _clear: self.clear_screen() #used if user is drawing objects themselves
127
+
128
+
129
+ def clear_screen(self): self.screen.fill(self.bg)
130
+
131
+ def render(self, _clear=True):
132
+ if not self.__on: return
133
+
134
+ if _clear: self.screen.fill(self.bg)
135
+ #render call
136
+ if self.active_scene is None:
137
+ for obj in self.render_queue: obj.render(self.cam)
138
+ #render scene
139
+ else:
140
+ self.active_scene.render(self.cam)
141
+ self.active_scene._render(self.cam)
142
+ if self.show_collisions: self.__render_collisions()
143
+ #update screen
144
+ pygame.display.flip()
145
+
146
+ def __render_collisions(self):
147
+ for coll in self.collisions:
148
+ pygame.draw.rect(self.screen, "yellow", coll, 2)
149
+
150
+ def quit(self):
151
+ self.__on = False
152
+ pygame.quit()
153
+ sys.exit()
154
+
155
+ def set_title(self, title):
156
+ self.title = title
157
+ pygame.display.set_caption(self.title)
158
+
159
+ def Tick(self):
160
+ self.update()
161
+ self.render()
162
+ def Run(self, _esc=False):
163
+ while True:
164
+ self.Tick()
165
+ if _esc and Keys.is_pressed(pygame.K_ESCAPE): self.quit()
166
+ def full_reset(self):
167
+ self.objects = []
168
+ self.queue_creation = []
169
+ self.queue_del = []
170
+ self.render_queue = []
171
+ self.scenes = {}
172
+ self.id = 0
173
+ self.active_scene = None
174
+ self._clear_all_collisions_()
175
+ def _clear_all_collisions_(self):
176
+ self.collisions = []
177
+ self.col = []
178
+ self.queue_creation_coll = []
179
+ self.queue_del_coll = []
180
+ self._update_coll = False
181
+
182
+ def set_scene(self, scene=None):
183
+ if self.active_scene is not None: self.active_scene.destroy()
184
+ self._clear_all_collisions_()
185
+ self.active_scene = self.scenes.get(scene, None)
186
+ if self.active_scene: self.active_scene.on_change()
187
+
188
+
189
+ class Scene:
190
+ def __init__(self, name):
191
+ self.name = name
192
+ Game.scenes[name] = self
193
+ self._objects = []
194
+ self._render_queue = []
195
+ self.queue_creation = []
196
+ self.queue_del = []
197
+ Game.set_scene(name)
198
+
199
+ def on_change(self): pass
200
+
201
+ def update(self): pass #user defined
202
+ def _update(self): #built in method
203
+ self.__handle_queue()
204
+ for obj in self._objects: obj.update()
205
+
206
+ def render(self, cam): pass #user defined
207
+ def _render(self, cam): #built in method
208
+ #update render queue based on global objects
209
+ _length = len(self._objects)+len(Game.objects)
210
+ if _length != len(self._render_queue): self.__update_render_queue()
211
+ #render
212
+ for obj in self._render_queue: obj.render(cam)
213
+
214
+ def _queue_add(self, obj): self.queue_creation.append(obj)
215
+ def _queue_remove(self, obj): self.queue_del.append(obj)
216
+ def __update_render_queue(self):
217
+ objs = self._objects + Game.objects
218
+ self._render_queue = sorted(objs, key=lambda obj: obj.z_index)
219
+ def __handle_queue(self):
220
+ for obj in self.queue_creation: self._objects.append(obj)
221
+ for obj in self.queue_del: self._objects.remove(obj)
222
+ if len(self.queue_creation) > 0 or len(self.queue_del) > 0: self.__update_render_queue()
223
+ self.queue_creation = []
224
+ self.queue_del = []
225
+
226
+ def destroy(self):
227
+ for obj in self._objects: obj.destroy()
228
+ self._objects = []
229
+ self._render_queue = []
230
+ self.queue_creation = []
231
+ self.queue_del = []
232
+
233
+ Game: _game = _game()
234
+
235
+ class Camera:
236
+ def __init__(self, game):
237
+ self.pos = Vector2D(0, 0)
238
+ self.game = game
239
+
240
+ #rendering helpers
241
+ def draw_rect(self, rect, color, width=0, offset=True):
242
+ r = rect.copy()
243
+ if offset: r.topleft = (r.x - self.pos.x, r.y - self.pos.y)
244
+ pygame.draw.rect(self.game.screen, color, r, width)
245
+ def draw_circle(self, pos, r, color, width=0, offset=True):
246
+ p = pos.copy()
247
+ if offset: p -= self.pos
248
+ pygame.draw.circle(self.game.screen, color, p.to_int(), r, width)
249
+ def draw_line(self, start, end, color, width=1, offset=True):
250
+ s = start.copy()
251
+ e = end.copy()
252
+ if offset:
253
+ s -= self.pos
254
+ e -= self.pos
255
+ pygame.draw.line(self.game.screen, color, s.to_int(), e.to_int(), width)
256
+ def draw_text(self, surface, rect, offset=True):
257
+ r = rect.copy()
258
+ if offset: r.topleft = (r.x - self.pos.x, r.y - self.pos.y)
259
+ self.game.screen.blit(surface, r)
260
+ def draw_image(self, image, pos, offset=True):
261
+ p = pos.copy()
262
+ if offset: p -= self.pos
263
+ self.game.screen.blit(image, p.to_int())
264
+
265
+ def handle_collision(self, dim: Vector2D, Rect=None, ignore_self=True) -> dict:
266
+ returns = {"on_floor": False, "on_wall": False}
267
+
268
+ # 1. Horizontal Collisions
269
+ self.pos.x += self.vel.x * Game.dt
270
+ player_rect = pygame.Rect(self.pos.to_int(), dim.to_int())
271
+ for rect in Game.collisions:
272
+ if ignore_self and Rect is not None:
273
+ if rect == Rect: continue
274
+ if player_rect.colliderect(rect):
275
+ returns["on_wall"] = True
276
+ self.pos.x -= self.vel.x * Game.dt
277
+ self.vel.x = 0
278
+ break
279
+
280
+ # 2. Vertical Collisions
281
+ self.pos.y += self.vel.y * Game.dt
282
+ player_rect = pygame.Rect(self.pos.to_int(), dim.to_int())
283
+ for rect in Game.collisions:
284
+ if ignore_self and Rect is not None:
285
+ if rect == Rect: continue
286
+ if player_rect.colliderect(rect):
287
+ # Step back first
288
+ self.pos.y -= self.vel.y * Game.dt
289
+
290
+ if self.vel.y > 0 and player_rect.bottom >= rect.top and player_rect.top < rect.top:
291
+ returns["on_floor"] = True
292
+
293
+ self.vel.y = 0
294
+ break
295
+
296
+ return returns
297
+
@@ -0,0 +1,100 @@
1
+ import pygame
2
+ from .Math import Vector2D
3
+
4
+ class _keys:
5
+ def __init__(self):
6
+
7
+ #alphabet
8
+ self.a = pygame.K_a
9
+ self.b = pygame.K_b
10
+ self.c = pygame.K_c
11
+ self.d = pygame.K_d
12
+ self.e = pygame.K_e
13
+ self.f = pygame.K_f
14
+ self.g = pygame.K_g
15
+ self.h = pygame.K_h
16
+ self.i = pygame.K_i
17
+ self.j = pygame.K_j
18
+ self.k = pygame.K_k
19
+ self.l = pygame.K_l
20
+ self.m = pygame.K_m
21
+ self.n = pygame.K_n
22
+ self.o = pygame.K_o
23
+ self.p = pygame.K_p
24
+ self.q = pygame.K_q
25
+ self.r = pygame.K_r
26
+ self.s = pygame.K_s
27
+ self.t = pygame.K_t
28
+ self.u = pygame.K_u
29
+ self.v = pygame.K_v
30
+ self.w = pygame.K_w
31
+ self.x = pygame.K_x
32
+ self.y = pygame.K_y
33
+ self.z = pygame.K_z
34
+
35
+ #numbers
36
+ self.n0 = pygame.K_0
37
+ self.n1 = pygame.K_1
38
+ self.n2 = pygame.K_2
39
+ self.n3 = pygame.K_3
40
+ self.n4 = pygame.K_4
41
+ self.n5 = pygame.K_5
42
+ self.n6 = pygame.K_6
43
+ self.n7 = pygame.K_7
44
+ self.n8 = pygame.K_8
45
+ self.n9 = pygame.K_9
46
+
47
+ #special keys
48
+ self.enter = pygame.K_RETURN
49
+ self.escape = pygame.K_ESCAPE
50
+ self.space = pygame.K_SPACE
51
+ self.backspace = pygame.K_BACKSPACE
52
+ self.tab = pygame.K_TAB
53
+ self.control = pygame.K_LCTRL
54
+ self.alt = pygame.K_LALT
55
+
56
+ #arrow keys
57
+ self.up = pygame.K_UP
58
+ self.down = pygame.K_DOWN
59
+ self.left = pygame.K_LEFT
60
+ self.right = pygame.K_RIGHT
61
+
62
+ self.shift = pygame.K_LSHIFT
63
+
64
+ #mouse
65
+ self.mouse_x, self.mouse_y = 0, 0
66
+
67
+ def is_pressed(self, key):
68
+ events = self.Net.events
69
+ if events is None: return False
70
+ for event in events:
71
+ if event.type == pygame.KEYDOWN:
72
+ if event.key == key: return True
73
+ return False
74
+
75
+ def is_pressed_list(self, keys):
76
+ for key in keys:
77
+ if self.is_pressed(key): return True
78
+ return False
79
+
80
+ def is_held(self, key):
81
+ keys = pygame.key.get_pressed()
82
+ return keys[key]
83
+
84
+ def get_axis(self, negative_action, positive_action) -> int:
85
+ value = 0
86
+ if self.is_held(positive_action): value += 1
87
+ if self.is_held(negative_action): value -= 1
88
+ return value
89
+ def get_vector(self, neg_x, pos_x, neg_y, pos_y, normalise=True) -> Vector2D:
90
+ value = Vector2D(self.get_axis(neg_x, pos_x), self.get_axis(neg_y, pos_y))
91
+ if normalise and value.length() > 1.0:
92
+ value.normalize()
93
+ return value
94
+
95
+ def _add_net(self, net): self.Net = net
96
+
97
+ def update(self): self.mouse_x, self.mouse_y = pygame.mouse.get_pos()
98
+
99
+
100
+ Keys = _keys()
@@ -0,0 +1,52 @@
1
+ import pygame
2
+
3
+ class Vector2D:
4
+ def __init__(self, x, y):
5
+ self.x = x
6
+ self.y = y
7
+ def to_int(self): return (int(self.x), int(self.y))
8
+ def to_tuple(self): return (tuple(self.x), tuple(self.y))
9
+ def copy(self): return Vector2D(self.x, self.y)
10
+ def int(self):
11
+ self.x = int(self.x)
12
+ self.y = int(self.y)
13
+
14
+ def length(self):
15
+ return (self.x**2 + self.y**2)**0.5
16
+
17
+ def normalize(self):
18
+ length = self.length()
19
+ self.x /= length
20
+ self.y /= length
21
+ return self
22
+
23
+ def __add__(self, other):
24
+ if type(other) == Vector2D:
25
+ return Vector2D(self.x + other.x, self.y + other.y)
26
+ else:
27
+ return Vector2D(self.x + other, self.y + other)
28
+
29
+ def __sub__(self, other):
30
+ if type(other) == Vector2D:
31
+ return Vector2D(self.x - other.x, self.y - other.y)
32
+ else:
33
+ return Vector2D(self.x - other, self.y - other)
34
+
35
+ def __mul__(self, other):
36
+ if type(other) == Vector2D:
37
+ return Vector2D(self.x * other.x, self.y * other.y)
38
+ else:
39
+ return Vector2D(self.x * other, self.y * other)
40
+
41
+ def __truediv__(self, other):
42
+ if type(other) == Vector2D:
43
+ return Vector2D(self.x / other.x, self.y / other.y)
44
+ else:
45
+ return Vector2D(self.x / other, self.y / other)
46
+
47
+ def clamp(value, min, max):
48
+ if value < min: return min
49
+ if value > max: return max
50
+ return value
51
+
52
+
@@ -0,0 +1,31 @@
1
+ import pygame
2
+
3
+ class _net_:
4
+ def __init__(self):
5
+ self.screen: pygame.Surface = None
6
+ self.dt: float = 0.0
7
+ self.clock: pygame.time.Clock = None
8
+ self.width, self.height = 0, 0
9
+ self.fps = 0
10
+
11
+ self.events = []
12
+ self.click = False
13
+
14
+ def _update(self):
15
+ self.click = False
16
+ self.dt = self.clock.tick(self.FPS)/1000.0
17
+ self.events = pygame.event.get()
18
+
19
+ for event in self.events:
20
+ if event.type == pygame.QUIT:
21
+ self.shut_down()
22
+ if event.type == pygame.MOUSEBUTTONDOWN:
23
+ if event.button == 1:
24
+ self.mouse_down = True
25
+ if event.type == pygame.MOUSEBUTTONUP and self.mouse_down:
26
+ if event.button == 1:
27
+ self.mouse_down = False
28
+ self.click = True
29
+
30
+
31
+ Net = _net_()
@@ -0,0 +1,34 @@
1
+ #imports
2
+ "PyScene is a custom python + pygame framework / engine developed to streamline 2d game development."
3
+ "Version: 1.0.0"
4
+
5
+ from .Game import Game, Scene, handle_collision
6
+ from .Math import *
7
+ from .utils.Subscreen import *
8
+
9
+ from .Keys import *
10
+ from .objs import *
11
+ from .utils import *
12
+
13
+
14
+ #all
15
+ __all__ = [
16
+ "Vector2D",
17
+ "SubScreen",
18
+ "Game",
19
+ "Scene",
20
+ "Keys",
21
+
22
+ "GameObj",
23
+ "Rect",
24
+ "Circle",
25
+ "Line",
26
+ "Text",
27
+ "Image",
28
+
29
+ "StaticCollisionRect",
30
+ "DynamicCollisionRect",
31
+
32
+ "Assets",
33
+ "handle_collision"
34
+ ]
@@ -0,0 +1,41 @@
1
+ import pygame
2
+ from ..Game import Game, handle_collision
3
+ from ..Math import Vector2D
4
+ from .GameObj import GameObj
5
+
6
+ class StaticCollisionRect:
7
+ def __init__(self, x, y, w, h, active=True):
8
+ self.rect = pygame.Rect(x, y, w, h)
9
+ self.active = active
10
+
11
+ Game._update_coll = True
12
+ Game._queue_add_coll(self)
13
+
14
+ def set_active(self, active=True):
15
+ self.active = active
16
+ Game._update_coll = True
17
+ def destroy(self):
18
+ Game._queue_remove_coll(self)
19
+
20
+ class DynamicCollisionRect:
21
+ def __init__(self, x, y, w, h, active=True):
22
+ self.pos = Vector2D(x, y)
23
+ self.dim = Vector2D(w, h)
24
+ self.active = active
25
+ self.vel = Vector2D(0.0, 0.0)
26
+ self.rect = pygame.Rect(x, y, w, h)
27
+
28
+ Game._update_coll = True
29
+ Game._queue_add_D_coll(self)
30
+
31
+ def update(self):
32
+ if self.vel.to_int != (0.0, 0.0): Game._update_coll = True
33
+ #print(self.active, len(Game.collisions), self.vel.x)
34
+ handle_collision(self, self.dim, self.rect)
35
+ self.rect.topleft = self.pos.to_int()
36
+
37
+ def set_active(self, active=True):
38
+ self.active = active
39
+ Game._update_coll = True
40
+ def destroy(self):
41
+ Game._queue_remove_D_coll(self)
@@ -0,0 +1,20 @@
1
+ import pygame
2
+ from ..Math import Vector2D
3
+ from ..Game import Game
4
+
5
+ class GameObj: #this will be the base class of every game obj
6
+ def __init__(self, pos, z_index=0, active=True, visible=True):
7
+ self.pos: Vector2D = pos
8
+ self.vel: Vector2D = Vector2D(0, 0)
9
+ self.z_index = z_index
10
+ self.active = active
11
+ self.visible = visible
12
+ self._id = Game._get_id()
13
+
14
+ #add to creation
15
+ Game._queue_add(self)
16
+
17
+ def update(self): pass
18
+ def render(self, cam): pass
19
+ def destroy(self): Game._queue_remove(self)
20
+
@@ -0,0 +1,45 @@
1
+ import pygame
2
+ from ..Math import Vector2D
3
+ from ..Game import Game
4
+
5
+ from .GameObj import GameObj
6
+
7
+ class Rect(GameObj):
8
+ def __init__(self, x, y, w, h, color, z_index=0, width=0, active=True, visible=True, offset=True):
9
+ super().__init__(Vector2D(x, y), z_index, active, visible)
10
+ self.dim = Vector2D(w, h)
11
+ self.color = color
12
+ self.rect = pygame.Rect((x, y), (w, h))
13
+ self.offset = offset
14
+ self.width = width
15
+
16
+ def update(self):
17
+ if self.pos.to_int() != self.rect.topleft: self.rect.topleft = self.pos.to_int()
18
+
19
+ def render(self, cam):
20
+ if not self.visible: return
21
+ cam.draw_rect(self.rect, self.color, self.width, self.offset)
22
+
23
+ class Circle(GameObj):
24
+ def __init__(self, x, y, r, color, z_index=0, width=0, active=True, visible=True, offset=True):
25
+ super().__init__(Vector2D(x, y), z_index, active, visible)
26
+ self.radius = r
27
+ self.color = color
28
+ self.width = width
29
+ self.offset = offset
30
+
31
+ def render(self, cam):
32
+ if not self.visible: return
33
+ cam.draw_circle(self.pos, self.radius, self.color, self.width, self.offset)
34
+
35
+ class Line(GameObj):
36
+ def __init__(self, x1, y1, x2, y2, color, z_index=0, width=1, active=True, visible=True, offset=True):
37
+ super().__init__(Vector2D(x1, y1), z_index, active, visible)
38
+ self.end = Vector2D(x2, y2)
39
+ self.color = color
40
+ self.width = width
41
+ self.offset = offset
42
+
43
+ def render(self, cam):
44
+ if not self.visible: return
45
+ cam.draw_line(self.pos, self.end, self.color, self.width, self.offset)
@@ -0,0 +1,50 @@
1
+ from ..Math import Vector2D
2
+ from ..Game import Game
3
+ from .GameObj import GameObj
4
+ import pygame
5
+
6
+ class Text(GameObj):
7
+ def __init__(self, font, text, color, pos=Vector2D(0, 0), align="center", z_index=0, active=True, visible=True, offset=True):
8
+ super().__init__(pos, z_index, active, visible)
9
+ self.font = font
10
+ self.text = text
11
+ self.color = color
12
+ self.align = align
13
+ self.surface = None
14
+ self.rect = None
15
+ self.offset = offset
16
+
17
+ self._update_surface()
18
+
19
+ def _update_surface(self):
20
+ """Internal method to re-render surface only when text changes."""
21
+ self.surface = self.font.render(str(self.text), False, self.color) # False for sharp pixel fonts!
22
+ # Dynamically set rect based on alignment (topleft, center, topright, etc.)
23
+ self.rect = self.surface.get_rect(**{self.align: self.pos.to_int()})
24
+
25
+ def set_text(self, new_text, color=None):
26
+ """Only call this when the string actually changes!"""
27
+ if color is not None:
28
+ self.color = color
29
+ if new_text != self.text:
30
+ self.text = new_text
31
+ self._update_surface()
32
+
33
+ def set_pos(self, new_pos):
34
+ self.pos = new_pos
35
+ self.rect = self.surface.get_rect(**{self.align: self.pos})
36
+
37
+ def render(self, cam):
38
+ if not self.visible: return
39
+ cam.draw_text(self.surface, self.rect, self.offset)
40
+
41
+ class Image(GameObj):
42
+ def __init__(self, image, pos=Vector2D(0, 0), z_index=0, active=True, visible=True, offset=True):
43
+ super().__init__(pos, z_index, active, visible)
44
+ self.image = image
45
+ self.offset = offset
46
+
47
+ def render(self, cam):
48
+ if not self.visible: return
49
+ cam.draw_image(self.image, self.pos, self.offset)
50
+
@@ -0,0 +1,7 @@
1
+ from .GameObj import GameObj
2
+ from .Shapes import *
3
+ from .Text import *
4
+ from .animation import *
5
+ from .Collision_shapes import *
6
+
7
+ __all__ = ["GameObj", "Rect", "Circle", "Line", "Text", "Image", "Animation", "AnimationManager", "StaticCollisionRect", "DynamicCollisionRect"]
@@ -0,0 +1,116 @@
1
+ import pygame
2
+
3
+ from ..Math import Vector2D
4
+ from .GameObj import GameObj
5
+ from ..Game import Game
6
+
7
+ class Animation(GameObj):
8
+ def __init__(self, image, size, fps, loop=True, pos=Vector2D(0, 0), z_index=0, active=True, visible=True, offset=True):
9
+ super().__init__(pos, z_index, active, visible)
10
+ self.offset = offset
11
+ self.image = image
12
+ self.size = size
13
+ self.fps = fps
14
+ self.images = []
15
+ self.index = 0.0
16
+ self.loop=loop
17
+ self.done=False
18
+
19
+ # Automatically calculate frames based on image width
20
+ total_frames = int(image.get_width() // size)
21
+
22
+ img_rect = image.get_rect()
23
+
24
+ for i in range(total_frames):
25
+ # Slice each frame from the sheet
26
+ frame_rect = pygame.Rect(i * size, 0, size, size)
27
+ #checks
28
+ #if frame_rect.height > img_rect.height: frame_rect.height = img_rect.height
29
+ self.images.append(self.image.subsurface(frame_rect))
30
+
31
+ def last_frame(self):
32
+ return int(self.index) >= (len(self.images) - 1)
33
+ def reset(self):
34
+ self.index = 0
35
+ self.done = False
36
+
37
+ def update(self):
38
+ if self.done:
39
+ return
40
+
41
+ self.index += self.fps * Game.dt
42
+
43
+ if self.index >= len(self.images):
44
+ if self.loop:
45
+ # Wrap around using modulo so fractional progress carries over smoothly
46
+ self.index %= len(self.images)
47
+ else:
48
+ # Clamp to the exact last frame index and mark as finished
49
+ self.index = float(len(self.images) - 1)
50
+ self.done = True
51
+
52
+ def get_image(self):
53
+ # Safe indexing guard
54
+ idx = min(int(self.index), len(self.images) - 1)
55
+ return self.images[idx]
56
+
57
+ def flip_v(self):
58
+ for i in range(len(self.images)):
59
+ self.images[i] = pygame.transform.flip(self.images[i], False, True)
60
+
61
+ def flip_h(self):
62
+ for i in range(len(self.images)):
63
+ self.images[i] = pygame.transform.flip(self.images[i], True, False)
64
+
65
+ def copy(self):
66
+ return Animation(self.image, self.size, self.fps, self.loop)
67
+ def render(self, cam):
68
+ if not self.visible: return
69
+ cam.draw_image(self.get_image(), self.pos, self.offset)
70
+
71
+ class AnimationManager(GameObj):
72
+ def __init__(self, animations: dict[str, Animation], current_anim=None, z_index=0, active=True, visible=True, offset=True):
73
+ super().__init__(Vector2D(0, 0), z_index, active, visible)
74
+ self.animations = animations
75
+ self.current_anim = current_anim
76
+
77
+ for anim in self.animations:
78
+ anim.destroy() #remove from global or scene calls
79
+
80
+ def new_anim(self, name, anim):
81
+ self.animations[name] = anim
82
+ anim.destroy()
83
+
84
+ def new_anim_set(self, anims: dict[str, Animation]):
85
+ #add new anims to existing ones
86
+ self.animations.update(anims)
87
+ for anim in anims:
88
+ anims[anim].destroy()
89
+
90
+ def flip_v(self):
91
+ for name in self.animations:
92
+ self.animations[name].flip_v()
93
+
94
+ def flip_h(self):
95
+ for name in self.animations:
96
+ self.animations[name].flip_h()
97
+
98
+ def change_anim(self, name):
99
+ self.current_anim = name
100
+
101
+ def update(self):
102
+ if self.current_anim is None and self.current_anim in self.animations:
103
+ return
104
+ self.animations[self.current_anim].update(Game.dt)
105
+ def get_image(self) -> pygame.Surface:
106
+ if self.current_anim is None and self.current_anim in self.animations:
107
+ return
108
+ return self.animations[self.current_anim].get_image()
109
+ def get_animation(self) -> Animation:
110
+ if self.current_anim is None and self.current_anim in self.animations:
111
+ return
112
+ return self.animations[self.current_anim]
113
+
114
+ def render(self, cam):
115
+ if not self.visible: return
116
+ self.get_animation().render(cam)
@@ -0,0 +1,151 @@
1
+ import pygame
2
+
3
+ from ..objs import Animation
4
+ from ..Math import Vector2D
5
+
6
+ class _assets:
7
+ def __init__(self):
8
+ self.images: dict[str, pygame.Surface] = {}
9
+ self.animations: dict[str, Animation] = {}
10
+ self.fonts: dict[str, pygame.font.Font] = {}
11
+ self.sounds: dict[str, pygame.mixer.Sound] = {}
12
+ self.tilesets: dict[str, dict[tuple[int, int], pygame.surface.Surface]] = {}
13
+ self._tileset_cache_data: dict[str, tuple[int, int]] = {}
14
+
15
+ #clearing
16
+ def _clear(self, _confirm=False):
17
+ if _confirm:
18
+ self.images = {}
19
+ self.animations = {}
20
+ self.fonts = {}
21
+ self.sounds = {}
22
+ self.tilesets = {}
23
+ self._tileset_cache_data = {}
24
+
25
+ def clear_images(self, _confirm=False):
26
+ if _confirm:
27
+ self.images = {}
28
+
29
+ def del_image(self, name):
30
+ if name in self.images:
31
+ del self.images[name]
32
+
33
+ def clear_animations(self, _confirm=False):
34
+ if _confirm:
35
+ self.animations = {}
36
+
37
+ def del_animation(self, name):
38
+ if name in self.animations:
39
+ del self.animations[name]
40
+
41
+ def clear_fonts(self, _confirm=False):
42
+ if _confirm:
43
+ self.fonts = {}
44
+
45
+ def del_font(self, name):
46
+ if name in self.fonts:
47
+ del self.fonts[name]
48
+
49
+ def clear_sounds(self, _confirm=False):
50
+ if _confirm:
51
+ self.sounds = {}
52
+
53
+ def del_sound(self, name):
54
+ if name in self.sounds:
55
+ del self.sounds[name]
56
+
57
+ def clear_tilesets(self, _confirm=False):
58
+ if _confirm:
59
+ self.tilesets = {}
60
+ self._tileset_cache_data = {}
61
+
62
+ def del_tileset(self, name):
63
+ if name in self.tilesets:
64
+ del self.tilesets[name]
65
+ del self._tileset_cache_data[name]
66
+
67
+ #creation
68
+ def new_image(self, name, path, rect=None, scale=1.0, colorKey=None, sWidth=None, sHeight=None):
69
+ image = pygame.image.load(path)
70
+ #rect
71
+ if rect is not None:
72
+ image = image.subsurface(rect)
73
+ #scale
74
+ if scale != 1.0:
75
+ image = pygame.transform.scale_by(image, scale)
76
+ #colorkey
77
+ if colorKey is not None:
78
+ image.set_colorkey(colorKey)
79
+ #size
80
+ if sWidth is not None and sHeight is not None:
81
+ image = pygame.transform.scale(image, (sWidth, sHeight))
82
+ self.images[name] = image.convert()
83
+
84
+ def new_font(self, name, path, size):
85
+ self.fonts[name] = pygame.font.Font(path, size)
86
+
87
+ def new_animation(self, name, image, size=16, fps=3, loop=True, pos=Vector2D(0, 0), z_index=0, active=True, visible=True, offset=True):
88
+ self.animations[name] = Animation(image, size, fps, loop, pos, z_index, active, visible, offset)
89
+ def new_anim_nIMG(self, name, size=16, fps=3, path=None, rect=None, scale=1.0, colorKey=(0, 0, 0), sWidth=None, sHeight=None, loop=True, pos=Vector2D(0, 0), z_index=0, active=True, visible=True, offset=True):
90
+ #create image
91
+ self.new_image(name, path, rect, scale, colorKey, sWidth, sHeight)
92
+ #create animation
93
+ self.new_animation(name, self.images[name], size*scale, fps, loop, pos, z_index, active, visible, offset)
94
+
95
+ def new_sound(self, name, path):
96
+ self.sounds[name] = pygame.mixer.Sound(path)
97
+
98
+ def new_tileset(self, path, name, width=16, height=16, scale=1.0, colorkey=None):
99
+ image = pygame.image.load(path).convert_alpha()
100
+ if colorkey is not None:
101
+ image.set_colorkey(colorkey)
102
+ if scale != 1.0:
103
+ image = pygame.transform.scale(image, (int(image.get_width()*scale), int(image.get_height()*scale)))
104
+ data, cache = load_tileset(image, width*scale, height*scale)
105
+ self.tilesets[name] = data
106
+ self._tileset_cache_data[name] = cache
107
+
108
+ #getters
109
+ def get_image(self, name):
110
+ if name in self.images:
111
+ return self.images[name]
112
+ else:
113
+ return None
114
+
115
+ def get_animation(self, name):
116
+ if name in self.animations:
117
+ return self.animations[name]
118
+ else:
119
+ return None
120
+
121
+ def get_font(self, name):
122
+ if name in self.fonts:
123
+ return self.fonts[name]
124
+ else:
125
+ return None
126
+
127
+ def get_sound(self, name):
128
+ if name in self.sounds:
129
+ return self.sounds[name]
130
+ else:
131
+ return None
132
+
133
+ def get_tileset(self, name):
134
+ if name in self.tilesets:
135
+ return self.tilesets[name]
136
+ else:
137
+ return None
138
+
139
+
140
+
141
+ Assets = _assets()
142
+
143
+ def load_tileset(image, width, height):
144
+ tileset = {}
145
+ w, h = image.get_size()
146
+ w = int(w/width)
147
+ h = int(h/height)
148
+ for y in range(int(image.get_height()//height)):
149
+ for x in range(int(image.get_width()//width)):
150
+ tileset[(x, y)] = image.subsurface(pygame.Rect(x*width, y*height, width, height))
151
+ return tileset, (w, h)
@@ -0,0 +1,36 @@
1
+ import pygame
2
+
3
+ from ..Math import Vector2D
4
+
5
+
6
+ class SubScreen:
7
+ def __init__(self, x, y, w, h, bgColor: None, displaySurface: pygame.Surface = None, parent=None):
8
+ self.pos = Vector2D(x, y)
9
+ self.dim = Vector2D(w, h)
10
+ self.color = bgColor
11
+ self._screen = displaySurface
12
+ self.parent = parent # Reference to parent SubScreen
13
+ if self.color == None: self.screen = pygame.Surface((w, h), pygame.SRCALPHA).convert_alpha()
14
+ else: self.screen = pygame.Surface((w, h)).convert()
15
+
16
+ def clear(self):
17
+ if self.color == None: self.screen.fill((0, 0, 0, 0))
18
+ else: self.screen.fill(self.color)
19
+
20
+ def get_absolute_pos(self):
21
+ """Recursively calculates the true global screen position."""
22
+ if self.parent:
23
+ return self.pos + self.parent.get_absolute_pos()
24
+ return self.pos
25
+
26
+ def local_mouse_pos(self, cam: Vector2D = Vector2D(0, 0)):
27
+ # Uses absolute position so nested screens correctly account for parent offsets
28
+ return Vector2D(*pygame.mouse.get_pos()) - (self.get_absolute_pos() - cam)
29
+
30
+ def get_global_rect(self, rect):
31
+ # Uses absolute position so hitboxes align on screen regardless of nesting level
32
+ abs_pos = self.get_absolute_pos()
33
+ return pygame.Rect((abs_pos.x + rect.x, abs_pos.y + rect.y), rect.size)
34
+
35
+ def render(self, cam: Vector2D = Vector2D(0, 0)):
36
+ self._screen.blit(self.screen, (self.pos - cam).to_int())
@@ -0,0 +1,4 @@
1
+ from .Subscreen import *
2
+ from .Assets import *
3
+
4
+ __all__ = ["SubScreen", "Assets"]
@@ -0,0 +1,38 @@
1
+ from src import *
2
+ import pygame
3
+ from random import randint as I
4
+ while True:
5
+ Game.full_reset()
6
+ Game.init(640, 360, "Floppy Bird", "darkblue")
7
+ bird = Circle(100, 25, 10, "yellow", -1) #show behind pipes
8
+ vel = 0
9
+ top_pipes = [Rect(400, 0, 20, I(20, 100), "green"),Rect(600, 0, 20, I(20, 100), "green"),Rect(800, 0, 20, I(20, 100), "green"),Rect(1000, 0, 20, I(20, 100), "green"),]
10
+ bottom_pipes = [Rect(400, Game.height-I(175, 225), 20, 225, "green"),Rect(600, Game.height-I(175, 225), 20, 225, "green"),Rect(800, Game.height-I(175, 225), 20, 225, "green"),Rect(1000, Game.height-I(175, 225), 20, 225, "green"),]
11
+ text = Text(pygame.font.Font(None, 20), "Score: 0", "white", Vector2D(3, Game.height-20), "topleft", 100)
12
+ score = 0
13
+ added = False
14
+ while True:
15
+ bird.pos.y += vel*Game.dt
16
+ if bird.pos.y < -10 or bird.pos.y > Game.height-10: break
17
+ vel += 4
18
+ if Keys.is_pressed(Keys.space): vel = -150
19
+ for pipe in top_pipes: pipe.pos.x -= 100*Game.dt
20
+ for pipe in bottom_pipes: pipe.pos.x -= 100*Game.dt
21
+ #lose
22
+ if top_pipes[0].rect.collidepoint(bird.pos.to_int()): break
23
+ elif bottom_pipes[0].rect.collidepoint(bird.pos.to_int()): break
24
+ elif top_pipes[0].pos.x <= bird.pos.x and not added: #score
25
+ score+=1
26
+ text.set_text(f"Score: {score}")
27
+ added = True
28
+ #resetting pipes
29
+ if top_pipes[0].pos.x < -21:
30
+ top_pipes[0].destroy()
31
+ top_pipes.pop(0)
32
+ top_pipes.append(Rect(top_pipes[-1].pos.x + 200, 0, 20, I(20, 100), "green"))
33
+ bottom_pipes[0].destroy()
34
+ bottom_pipes.pop(0)
35
+ bottom_pipes.append(Rect(bottom_pipes[-1].pos.x + 200, Game.height-I(175, 225), 20, 225, "green"))
36
+ added = False
37
+ Game.Tick()
38
+ if Keys.is_pressed(Keys.escape): Game.quit()
Binary file
@@ -0,0 +1,21 @@
1
+ from src import *
2
+ import pygame
3
+
4
+
5
+ Game.init(640, 360, "Test - 01", "darkblue", fullscreen=False)
6
+
7
+ class player(GameObj):
8
+ def __init__(self):
9
+ super().__init__(Vector2D(0, 0), 0)
10
+ #create animation
11
+ Assets.new_anim_nIMG("player", 8, 2, "tests/pause.png", (0, 0, 16, 8), colorKey=None, scale=3.0)
12
+ self.anim = Assets.get_animation("player")
13
+
14
+ def update(self):
15
+ self.pos += (Keys.get_vector(Keys.a, Keys.d, Keys.w, Keys.s, False)*150)*Game.dt
16
+ self.anim.pos = self.pos #positions stay linked
17
+
18
+ player()
19
+
20
+ Game.Run()
21
+
@@ -0,0 +1,39 @@
1
+ from src import *
2
+ import pygame
3
+
4
+ Game.init(640, 360, "Test - 02: Collisions", "black")
5
+ Game.show_collisions = True
6
+ Game.optimize_static_colliders = True
7
+
8
+ class Player(GameObj):
9
+ def __init__(self):
10
+ super().__init__(Vector2D(200, 0))
11
+ self.coll = DynamicCollisionRect(*self.pos.to_int(), 25, 25)
12
+
13
+ def update(self):
14
+ self.coll.vel = Keys.get_vector(Keys.a, Keys.d, Keys.w, Keys.s, False)*150
15
+
16
+ class TestDynamic(GameObj):
17
+ def __init__(self):
18
+ super().__init__(Vector2D(0, 0), 5)
19
+ self.coll = DynamicCollisionRect(100, 100, 20, 40, True)
20
+ self.coll.vel.x = 100
21
+ self.vel.x = 100
22
+ self.timer = 0.0
23
+
24
+ def update(self):
25
+ self.timer += Game.dt
26
+ if self.timer >= 3.0:
27
+ self.vel.x *= -1
28
+ self.timer = 0.0
29
+ self.coll.vel = self.vel.copy()
30
+
31
+ StaticCollisionRect(0, 0, 50, 100, True)
32
+ StaticCollisionRect(0, 100, 50, 50, True)
33
+ StaticCollisionRect(250, 100, 50, 50, True)
34
+ StaticCollisionRect(300, 100, 50, 50, True)
35
+ TestDynamic()
36
+
37
+ Player()
38
+ Game.Run(True)
39
+