crystalwindow 4.6__py3-none-any.whl → 4.7__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.
- crystalwindow/FileHelper.py +159 -38
- crystalwindow/Icons/file_icons.png +0 -0
- crystalwindow/__init__.py +16 -27
- crystalwindow/assets.py +250 -59
- crystalwindow/clock.py +21 -63
- crystalwindow/color_handler.py +3 -21
- crystalwindow/crystal3d.py +135 -56
- crystalwindow/fun_helpers.py +4 -0
- crystalwindow/gametests/3dsquare.py +2 -2
- crystalwindow/gametests/gravitytest.py +96 -23
- crystalwindow/gametests/guitesting.py +4 -2
- crystalwindow/gametests/squaremove.py +4 -1
- crystalwindow/gametests/testtttagain.py +17 -0
- crystalwindow/gui.py +87 -17
- crystalwindow/gui_ext.py +51 -39
- crystalwindow/objects.py +171 -0
- crystalwindow/sprites.py +174 -25
- crystalwindow/tilemap.py +1 -1
- crystalwindow/websearch.py +91 -165
- crystalwindow/window.py +430 -12
- {crystalwindow-4.6.dist-info → crystalwindow-4.7.dist-info}/METADATA +6 -2
- crystalwindow-4.7.dist-info/RECORD +43 -0
- crystalwindow/cw_client.py +0 -95
- crystalwindow/player.py +0 -106
- crystalwindow-4.6.dist-info/RECORD +0 -42
- {crystalwindow-4.6.dist-info → crystalwindow-4.7.dist-info}/WHEEL +0 -0
- {crystalwindow-4.6.dist-info → crystalwindow-4.7.dist-info}/licenses/LICENSE +0 -0
- {crystalwindow-4.6.dist-info → crystalwindow-4.7.dist-info}/top_level.txt +0 -0
crystalwindow/gui_ext.py
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from .window import Window
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def parse_color(c):
|
|
9
|
-
if isinstance(c, tuple):
|
|
10
|
-
return c
|
|
11
|
-
if isinstance(c, Color):
|
|
12
|
-
return c.to_tuple()
|
|
13
|
-
if isinstance(c, str):
|
|
14
|
-
if hasattr(Colors, c):
|
|
15
|
-
return getattr(Colors, c).to_tuple()
|
|
16
|
-
return (255,255,255)
|
|
17
|
-
return (255,255,255)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# ============================================================
|
|
6
|
+
# T O G G L E (behaves EXACTLY like old version)
|
|
7
|
+
# ============================================================
|
|
18
8
|
|
|
19
9
|
class Toggle:
|
|
20
|
-
def __init__(self, rect, value=False,
|
|
10
|
+
def __init__(self, rect, value=False,
|
|
11
|
+
color=(200, 200, 200), hover_color=(255, 255, 255),
|
|
12
|
+
cooldown=0.1):
|
|
13
|
+
|
|
21
14
|
self.rect = rect
|
|
22
15
|
self.value = value
|
|
23
|
-
|
|
24
|
-
self.
|
|
25
|
-
self.hover_color = parse_color(hover_color)
|
|
16
|
+
self.color = color
|
|
17
|
+
self.hover_color = hover_color
|
|
26
18
|
|
|
27
19
|
self.hovered = False
|
|
28
20
|
self.cooldown = cooldown
|
|
@@ -31,20 +23,22 @@ class Toggle:
|
|
|
31
23
|
def update(self, win: Window):
|
|
32
24
|
mx, my = win.mouse_pos
|
|
33
25
|
x, y, w, h = self.rect
|
|
34
|
-
self.hovered = x <= mx <= x + w and y <= my <= y + h
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
# hover check
|
|
28
|
+
self.hovered = (x <= mx <= x + w and y <= my <= y + h)
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
# cooldown logic (same as old ver)
|
|
31
|
+
now = time.time()
|
|
32
|
+
if self.hovered and win.mouse_pressed(1):
|
|
33
|
+
if now - self._last_toggle >= self.cooldown:
|
|
34
|
+
self.value = not self.value
|
|
35
|
+
self._last_toggle = now
|
|
42
36
|
|
|
43
37
|
def draw(self, win: Window):
|
|
44
38
|
draw_color = self.hover_color if self.hovered else self.color
|
|
45
39
|
win.draw_rect(draw_color, self.rect)
|
|
46
40
|
|
|
47
|
-
# ON glow
|
|
41
|
+
# ON glow (same as old ver)
|
|
48
42
|
if self.value:
|
|
49
43
|
inner = (
|
|
50
44
|
self.rect[0] + 4,
|
|
@@ -55,44 +49,62 @@ class Toggle:
|
|
|
55
49
|
win.draw_rect((0, 255, 0), inner)
|
|
56
50
|
|
|
57
51
|
|
|
52
|
+
|
|
53
|
+
# ============================================================
|
|
54
|
+
# S L I D E R (old logic fully restored)
|
|
55
|
+
# ============================================================
|
|
56
|
+
|
|
58
57
|
class Slider:
|
|
59
|
-
def __init__(self, rect, min_val=0, max_val=100, value=50,
|
|
58
|
+
def __init__(self, rect, min_val=0, max_val=100, value=50,
|
|
59
|
+
color=(150, 150, 150), handle_color=(255, 0, 0),
|
|
60
|
+
handle_radius=10):
|
|
61
|
+
|
|
60
62
|
self.rect = rect
|
|
61
63
|
self.min_val = min_val
|
|
62
64
|
self.max_val = max_val
|
|
63
65
|
self.value = value
|
|
64
66
|
|
|
65
|
-
self.color =
|
|
66
|
-
self.handle_color =
|
|
67
|
-
|
|
67
|
+
self.color = color
|
|
68
|
+
self.handle_color = handle_color
|
|
68
69
|
self.handle_radius = handle_radius
|
|
70
|
+
|
|
69
71
|
self.dragging = False
|
|
70
72
|
|
|
71
73
|
def update(self, win: Window):
|
|
72
74
|
mx, my = win.mouse_pos
|
|
73
75
|
x, y, w, h = self.rect
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
# compute handle pos
|
|
78
|
+
handle_x = x + ((self.value - self.min_val) /
|
|
79
|
+
(self.max_val - self.min_val)) * w
|
|
76
80
|
handle_y = y + h // 2
|
|
77
81
|
|
|
82
|
+
inside_slider = (x <= mx <= x + w and y <= my <= y + h)
|
|
83
|
+
|
|
78
84
|
if win.mouse_pressed(1):
|
|
79
|
-
|
|
85
|
+
# start drag if within slider area (old behavior)
|
|
86
|
+
if not self.dragging and inside_slider:
|
|
80
87
|
self.dragging = True
|
|
81
88
|
else:
|
|
82
89
|
self.dragging = False
|
|
83
90
|
|
|
91
|
+
# dragging updates value
|
|
84
92
|
if self.dragging:
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
rel = max(0, min(mx - x, w))
|
|
94
|
+
t = rel / w
|
|
95
|
+
self.value = self.min_val + t * (self.max_val - self.min_val)
|
|
87
96
|
|
|
88
97
|
def draw(self, win: Window):
|
|
89
98
|
x, y, w, h = self.rect
|
|
90
99
|
|
|
91
|
-
# bar
|
|
92
|
-
win.draw_rect(self.color, (x, y + h//2 - 2, w, 4))
|
|
100
|
+
# slider bar
|
|
101
|
+
win.draw_rect(self.color, (x, y + h // 2 - 2, w, 4))
|
|
93
102
|
|
|
94
|
-
# handle
|
|
95
|
-
handle_x = x + ((self.value - self.min_val) /
|
|
103
|
+
# handle pos
|
|
104
|
+
handle_x = x + ((self.value - self.min_val) /
|
|
105
|
+
(self.max_val - self.min_val)) * w
|
|
96
106
|
handle_y = y + h // 2
|
|
97
107
|
|
|
98
|
-
win.draw_circle(self.handle_color,
|
|
108
|
+
win.draw_circle(self.handle_color,
|
|
109
|
+
(int(handle_x), int(handle_y)),
|
|
110
|
+
self.handle_radius)
|
crystalwindow/objects.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import math, re
|
|
2
|
+
from .sprites import Sprite
|
|
3
|
+
from .assets import load_folder_images, load_image
|
|
4
|
+
from .animation import Animation
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ============================================================
|
|
8
|
+
# PLAYER / ENEMY WITH HP SECURITY + COOLDOWN + RESPAWN
|
|
9
|
+
# ============================================================
|
|
10
|
+
|
|
11
|
+
class Player(Sprite):
|
|
12
|
+
def __init__(self, name="Player", pos=(0, 0), size=(32, 32), speed=4, hp=100):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.hp = hp
|
|
15
|
+
self.max_hp = hp # allows 0–100 or even 0–500
|
|
16
|
+
self.speed = speed
|
|
17
|
+
|
|
18
|
+
# frames
|
|
19
|
+
self.animations = {}
|
|
20
|
+
self.current_anim = None
|
|
21
|
+
self.flip_x = False
|
|
22
|
+
|
|
23
|
+
# death / respawn flags
|
|
24
|
+
self.dead = False
|
|
25
|
+
|
|
26
|
+
super().__init__(pos, size=size, image=None)
|
|
27
|
+
|
|
28
|
+
# ----------------------------------------------------
|
|
29
|
+
# BASIC DAMAGE HANDLING
|
|
30
|
+
# ----------------------------------------------------
|
|
31
|
+
def take_damage(self, dmg):
|
|
32
|
+
if self.dead:
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
self.hp -= dmg
|
|
36
|
+
if self.hp <= 0:
|
|
37
|
+
self.hp = 0
|
|
38
|
+
self.dead = True
|
|
39
|
+
|
|
40
|
+
# ----------------------------------------------------
|
|
41
|
+
# RESPAWN (normal redraw logic kept)
|
|
42
|
+
# ----------------------------------------------------
|
|
43
|
+
def respawn(self, win):
|
|
44
|
+
"""Manual respawn call if coder wants it."""
|
|
45
|
+
self.hp = self.max_hp
|
|
46
|
+
self.dead = False
|
|
47
|
+
self.redraw(win)
|
|
48
|
+
|
|
49
|
+
# ----------------------------------------------------
|
|
50
|
+
# LOAD ANIMATION
|
|
51
|
+
# ----------------------------------------------------
|
|
52
|
+
def load_anim(self, key, folder, loop=True):
|
|
53
|
+
imgs = self._load_sorted(folder)
|
|
54
|
+
anim = Animation(imgs)
|
|
55
|
+
anim.loop = loop
|
|
56
|
+
self.animations[key] = anim
|
|
57
|
+
|
|
58
|
+
if self.current_anim is None:
|
|
59
|
+
self.current_anim = anim
|
|
60
|
+
|
|
61
|
+
# ----------------------------------------------------
|
|
62
|
+
# UPDATE (movement + anim)
|
|
63
|
+
# ----------------------------------------------------
|
|
64
|
+
def update(self, dt, win):
|
|
65
|
+
if self.dead:
|
|
66
|
+
# still draw corpse frame
|
|
67
|
+
self.draw(win)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
moving = False
|
|
71
|
+
spd = self.speed * dt * 60
|
|
72
|
+
|
|
73
|
+
# movement
|
|
74
|
+
if win.is_key_pressed("left"):
|
|
75
|
+
self.x -= spd
|
|
76
|
+
self.flip_x = True
|
|
77
|
+
moving = True
|
|
78
|
+
|
|
79
|
+
if win.is_key_pressed("right"):
|
|
80
|
+
self.x += spd
|
|
81
|
+
self.flip_x = False
|
|
82
|
+
moving = True
|
|
83
|
+
|
|
84
|
+
if win.is_key_pressed("up"):
|
|
85
|
+
self.y -= spd
|
|
86
|
+
moving = True
|
|
87
|
+
|
|
88
|
+
if win.is_key_pressed("down"):
|
|
89
|
+
self.y += spd
|
|
90
|
+
moving = True
|
|
91
|
+
|
|
92
|
+
self.pos = (self.x, self.y)
|
|
93
|
+
|
|
94
|
+
# anim switching
|
|
95
|
+
if moving and "run" in self.animations:
|
|
96
|
+
self.current_anim = self.animations["run"]
|
|
97
|
+
elif not moving and "idle" in self.animations:
|
|
98
|
+
self.current_anim = self.animations["idle"]
|
|
99
|
+
|
|
100
|
+
# apply anim frame
|
|
101
|
+
if self.current_anim:
|
|
102
|
+
self.current_anim.update(dt)
|
|
103
|
+
frame = self.current_anim.get_frame()
|
|
104
|
+
self.set_image(frame)
|
|
105
|
+
|
|
106
|
+
# final draw
|
|
107
|
+
self.draw(win)
|
|
108
|
+
|
|
109
|
+
# ----------------------------------------------------
|
|
110
|
+
# FOLDER LOADING (unchanged)
|
|
111
|
+
# ----------------------------------------------------
|
|
112
|
+
def _load_sorted(self, folder):
|
|
113
|
+
imgs_dict = load_folder_images(folder)
|
|
114
|
+
return self._sort_images(imgs_dict)
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _sort_images(imgs_dict):
|
|
118
|
+
def extract_num(f):
|
|
119
|
+
m = re.search(r"(\d+)", f)
|
|
120
|
+
return int(m.group(1)) if m else 0
|
|
121
|
+
|
|
122
|
+
items = [(name, img) for name, img in imgs_dict.items() if not isinstance(img, dict)]
|
|
123
|
+
sorted_imgs = [img for name, img in sorted(items, key=lambda x: extract_num(x[0]))]
|
|
124
|
+
return sorted_imgs
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ============================================================
|
|
128
|
+
# BLOCK (same)
|
|
129
|
+
# ============================================================
|
|
130
|
+
class Block(Sprite):
|
|
131
|
+
def __init__(self, pos, w, h, color=None, texture=None):
|
|
132
|
+
if texture:
|
|
133
|
+
img = load_image(texture)
|
|
134
|
+
super().__init__(pos, size=(w, h), image=img)
|
|
135
|
+
else:
|
|
136
|
+
super().__init__(pos, size=(w, h), color=color or (150,150,150))
|
|
137
|
+
|
|
138
|
+
def collide_with(self, other):
|
|
139
|
+
return self.colliderect(other)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ============================================================
|
|
143
|
+
# ENEMY WITH DAMAGE COOLDOWN
|
|
144
|
+
# ============================================================
|
|
145
|
+
class Enemy(Sprite):
|
|
146
|
+
def __init__(self, pos, w, h, dmg=10, speed=2, color=(200,50,50), texture=None, cooldown_max=5):
|
|
147
|
+
self.dmg = dmg
|
|
148
|
+
self.speed = speed
|
|
149
|
+
|
|
150
|
+
# cooldown system
|
|
151
|
+
self.cooldown = 0
|
|
152
|
+
self.cooldown_max = cooldown_max
|
|
153
|
+
|
|
154
|
+
if texture:
|
|
155
|
+
super().__init__(pos, size=(w, h), image=load_image(texture))
|
|
156
|
+
else:
|
|
157
|
+
super().__init__(pos, size=(w, h), color=color)
|
|
158
|
+
|
|
159
|
+
# enemy tick
|
|
160
|
+
def update(self, dt):
|
|
161
|
+
if self.cooldown > 0:
|
|
162
|
+
self.cooldown -= dt * 60
|
|
163
|
+
|
|
164
|
+
def collide_with(self, other):
|
|
165
|
+
return self.colliderect(other)
|
|
166
|
+
|
|
167
|
+
# enemy tries to damage
|
|
168
|
+
def hit_player(self, player):
|
|
169
|
+
if self.cooldown <= 0:
|
|
170
|
+
player.take_damage(self.dmg)
|
|
171
|
+
self.cooldown = self.cooldown_max
|
crystalwindow/sprites.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# sprites.py
|
|
2
1
|
import random
|
|
3
2
|
from tkinter import PhotoImage
|
|
3
|
+
from .assets import generate_missing_texture
|
|
4
4
|
|
|
5
5
|
try:
|
|
6
6
|
from PIL import Image, ImageTk
|
|
@@ -8,94 +8,175 @@ except ImportError:
|
|
|
8
8
|
Image = None
|
|
9
9
|
ImageTk = None
|
|
10
10
|
|
|
11
|
+
|
|
11
12
|
class Sprite:
|
|
12
13
|
def __init__(self, pos, size=None, image=None, color=(255, 0, 0)):
|
|
13
14
|
"""
|
|
14
15
|
pos: (x, y)
|
|
15
|
-
size: (w, h)
|
|
16
|
+
size: (w, h)
|
|
16
17
|
image: PhotoImage or PIL ImageTk.PhotoImage
|
|
17
|
-
color: fallback rectangle color
|
|
18
18
|
"""
|
|
19
19
|
self.pos = pos
|
|
20
20
|
self.x, self.y = pos
|
|
21
21
|
self.image = image
|
|
22
22
|
self.color = color
|
|
23
23
|
|
|
24
|
-
#
|
|
24
|
+
# drawn object id
|
|
25
|
+
self.canvas_id = None
|
|
26
|
+
|
|
27
|
+
# sprite states
|
|
28
|
+
self.visible = True # can draw
|
|
29
|
+
self.alive = True # can collide / move / draw
|
|
30
|
+
|
|
31
|
+
# save original spawn
|
|
32
|
+
self.spawn_point = pos
|
|
33
|
+
|
|
34
|
+
# width / height
|
|
25
35
|
if image is not None:
|
|
26
36
|
try:
|
|
27
|
-
# Tkinter PhotoImage
|
|
28
37
|
self.width = image.width()
|
|
29
38
|
self.height = image.height()
|
|
30
39
|
except Exception:
|
|
31
40
|
try:
|
|
32
|
-
# PIL ImageTk.PhotoImage
|
|
33
41
|
self.width = image.width
|
|
34
42
|
self.height = image.height
|
|
35
43
|
except Exception:
|
|
36
44
|
raise ValueError("Sprite image has no size info")
|
|
45
|
+
|
|
37
46
|
elif size is not None:
|
|
38
47
|
self.width, self.height = size
|
|
39
48
|
else:
|
|
40
49
|
raise ValueError("Sprite needs 'size' or 'image'")
|
|
41
50
|
|
|
42
|
-
#
|
|
51
|
+
# optional velocity
|
|
43
52
|
self.vel_x = 0
|
|
44
53
|
self.vel_y = 0
|
|
45
54
|
|
|
55
|
+
self._last_win = None
|
|
56
|
+
|
|
57
|
+
|
|
46
58
|
# === CLASS METHODS ===
|
|
47
59
|
@classmethod
|
|
48
60
|
def image(cls, img, pos):
|
|
49
|
-
"""
|
|
50
|
-
Create a sprite from an image.
|
|
51
|
-
Accepts fallback dict or actual PhotoImage.
|
|
52
|
-
"""
|
|
61
|
+
"""Create sprite from image OR fallback dict"""
|
|
53
62
|
if isinstance(img, dict) and img.get("fallback"):
|
|
54
63
|
w, h = img["size"]
|
|
55
|
-
|
|
56
|
-
return cls(pos, size=(w, h)
|
|
64
|
+
missing = generate_missing_texture((w, h))
|
|
65
|
+
return cls(pos, image=missing, size=(w, h))
|
|
66
|
+
# normal image
|
|
57
67
|
return cls(pos, image=img)
|
|
58
68
|
|
|
69
|
+
|
|
59
70
|
@classmethod
|
|
60
71
|
def rect(cls, pos, w, h, color=(255, 0, 0)):
|
|
61
|
-
"""Create sprite using a
|
|
72
|
+
"""Create sprite using a simple rectangle"""
|
|
62
73
|
return cls(pos, size=(w, h), color=color)
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
|
|
76
|
+
# === MOVE / DRAW ===
|
|
65
77
|
def draw(self, win, cam=None):
|
|
66
|
-
"""Draw sprite
|
|
78
|
+
"""Draw sprite onto Tk canvas. Skips when not visible/alive."""
|
|
79
|
+
if not self.alive or not self.visible:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
self._last_win = win
|
|
83
|
+
|
|
84
|
+
# delete previous drawing
|
|
85
|
+
if self.canvas_id is not None:
|
|
86
|
+
try:
|
|
87
|
+
win.canvas.delete(self.canvas_id)
|
|
88
|
+
except:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
# camera offset
|
|
67
92
|
if cam:
|
|
68
93
|
draw_x, draw_y = cam.apply(self)
|
|
69
94
|
else:
|
|
70
95
|
draw_x, draw_y = self.x, self.y
|
|
71
96
|
|
|
97
|
+
# draw img or rect
|
|
72
98
|
if self.image:
|
|
73
|
-
win.canvas.create_image(
|
|
99
|
+
self.canvas_id = win.canvas.create_image(
|
|
100
|
+
draw_x, draw_y, anchor="nw", image=self.image
|
|
101
|
+
)
|
|
74
102
|
else:
|
|
75
|
-
|
|
103
|
+
# draw_rect MUST return the canvas ID
|
|
104
|
+
self.canvas_id = win.draw_rect(
|
|
105
|
+
self.color, (draw_x, draw_y, self.width, self.height)
|
|
106
|
+
)
|
|
107
|
+
|
|
76
108
|
|
|
77
109
|
def move(self, dx, dy):
|
|
110
|
+
if not self.alive:
|
|
111
|
+
return
|
|
78
112
|
self.x += dx
|
|
79
113
|
self.y += dy
|
|
80
114
|
self.pos = (self.x, self.y)
|
|
81
115
|
|
|
116
|
+
|
|
82
117
|
def apply_velocity(self, dt=1):
|
|
118
|
+
if not self.alive:
|
|
119
|
+
return
|
|
83
120
|
self.x += self.vel_x * dt
|
|
84
121
|
self.y += self.vel_y * dt
|
|
85
122
|
self.pos = (self.x, self.y)
|
|
86
123
|
|
|
124
|
+
|
|
125
|
+
# === COLLISION ===
|
|
87
126
|
def colliderect(self, other):
|
|
127
|
+
if not self.alive or not other.alive:
|
|
128
|
+
return False
|
|
129
|
+
|
|
88
130
|
return (
|
|
89
|
-
self.x < other.x + getattr(other, "width", 0)
|
|
90
|
-
self.x + getattr(self, "width", 0) > other.x
|
|
91
|
-
self.y < other.y + getattr(other, "height", 0)
|
|
92
|
-
self.y + getattr(self, "height", 0) > other.y
|
|
93
|
-
)
|
|
131
|
+
self.x < other.x + getattr(other, "width", 0)
|
|
132
|
+
and self.x + getattr(self, "width", 0) > other.x
|
|
133
|
+
and self.y < other.y + getattr(other, "height", 0)
|
|
134
|
+
and self.y + getattr(self, "height", 0) > other.y
|
|
135
|
+
)
|
|
136
|
+
|
|
94
137
|
|
|
138
|
+
# === RESPAWN / REDRAW ===
|
|
139
|
+
def redraw(self, win, new_pos=None, flash=True):
|
|
140
|
+
if not self.alive:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
self._last_win = win
|
|
144
|
+
|
|
145
|
+
# save original spawn ONCE
|
|
146
|
+
if not hasattr(self, "spawn_pos"):
|
|
147
|
+
self.spawn_pos = (self.x, self.y)
|
|
148
|
+
|
|
149
|
+
# delete old draw
|
|
150
|
+
if self.canvas_id is not None:
|
|
151
|
+
try:
|
|
152
|
+
win.canvas.delete(self.canvas_id)
|
|
153
|
+
except:
|
|
154
|
+
pass
|
|
155
|
+
self.canvas_id = None
|
|
156
|
+
|
|
157
|
+
# teleport
|
|
158
|
+
if new_pos is not None:
|
|
159
|
+
self.x, self.y = new_pos
|
|
160
|
+
else:
|
|
161
|
+
self.x, self.y = self.spawn_pos
|
|
162
|
+
|
|
163
|
+
self.pos = (self.x, self.y)
|
|
164
|
+
|
|
165
|
+
# flash effect
|
|
166
|
+
if flash and self.image is None:
|
|
167
|
+
og_color = self.color
|
|
168
|
+
self.color = (255, 255, 255)
|
|
169
|
+
self.draw(win)
|
|
170
|
+
win.canvas.update()
|
|
171
|
+
self.color = og_color
|
|
172
|
+
|
|
173
|
+
self.draw(win)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# === IMAGE SWITCH ===
|
|
95
177
|
def set_image(self, img):
|
|
96
|
-
"""Update sprite's image at runtime (like flipping)"""
|
|
97
178
|
self.image = img
|
|
98
|
-
|
|
179
|
+
|
|
99
180
|
if img is not None:
|
|
100
181
|
try:
|
|
101
182
|
self.width = img.width()
|
|
@@ -106,3 +187,71 @@ class Sprite:
|
|
|
106
187
|
self.height = img.height
|
|
107
188
|
except:
|
|
108
189
|
pass
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# === FULL REMOVE ===
|
|
193
|
+
def remove(self):
|
|
194
|
+
"""
|
|
195
|
+
Removes from screen AND disables collisions/movement/drawing.
|
|
196
|
+
Fully out of the game world.
|
|
197
|
+
"""
|
|
198
|
+
self.visible = False
|
|
199
|
+
self.alive = False
|
|
200
|
+
|
|
201
|
+
# erase from canvas
|
|
202
|
+
if self.canvas_id is not None:
|
|
203
|
+
try:
|
|
204
|
+
win = self._last_win
|
|
205
|
+
win.canvas.delete(self.canvas_id)
|
|
206
|
+
except:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
self.canvas_id = None
|
|
210
|
+
self._last_win = None
|
|
211
|
+
|
|
212
|
+
class CollisionHandler:
|
|
213
|
+
def __init__(self):
|
|
214
|
+
"""Initialize the collision handler with an empty list of sprites."""
|
|
215
|
+
self.sprites = []
|
|
216
|
+
|
|
217
|
+
def add(self, sprite):
|
|
218
|
+
"""Add a sprite to the collision handler."""
|
|
219
|
+
self.sprites.append(sprite)
|
|
220
|
+
|
|
221
|
+
def check_collisions(self):
|
|
222
|
+
"""Check for collisions between all sprites in the handler."""
|
|
223
|
+
for i in range(len(self.sprites)):
|
|
224
|
+
for j in range(i + 1, len(self.sprites)):
|
|
225
|
+
sprite_a = self.sprites[i]
|
|
226
|
+
sprite_b = self.sprites[j]
|
|
227
|
+
|
|
228
|
+
if sprite_a.colliderect(sprite_b):
|
|
229
|
+
self.handle_collision(sprite_a, sprite_b)
|
|
230
|
+
|
|
231
|
+
def handle_collision(self, sprite_a, sprite_b):
|
|
232
|
+
"""Handle the collision between two sprites."""
|
|
233
|
+
|
|
234
|
+
# Prevent "noclip": Adjust positions or velocities to prevent overlap
|
|
235
|
+
|
|
236
|
+
# Stop both sprites' movement upon collision
|
|
237
|
+
if sprite_a.vel_x != 0 and sprite_b.vel_x != 0:
|
|
238
|
+
sprite_a.vel_x = 0
|
|
239
|
+
sprite_b.vel_x = 0
|
|
240
|
+
|
|
241
|
+
if sprite_a.vel_y != 0 and sprite_b.vel_y != 0:
|
|
242
|
+
sprite_a.vel_y = 0
|
|
243
|
+
sprite_b.vel_y = 0
|
|
244
|
+
|
|
245
|
+
# Prevent overlap by adjusting their positions after a collision
|
|
246
|
+
if sprite_a.x < sprite_b.x:
|
|
247
|
+
sprite_a.x = sprite_b.x - sprite_a.width
|
|
248
|
+
else:
|
|
249
|
+
sprite_b.x = sprite_a.x - sprite_b.width
|
|
250
|
+
|
|
251
|
+
if sprite_a.y < sprite_b.y:
|
|
252
|
+
sprite_a.y = sprite_b.y - sprite_a.height
|
|
253
|
+
else:
|
|
254
|
+
sprite_b.y = sprite_a.y - sprite_b.height
|
|
255
|
+
|
|
256
|
+
# You can add more sophisticated handling (e.g., bounce, adjust based on velocity)
|
|
257
|
+
# For now, this simple solution ensures no overlap and stops movement
|