crystalwindow 4.5__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/gui_ext.py CHANGED
@@ -1,28 +1,20 @@
1
1
  import time
2
2
  from .window import Window
3
- from .assets import load_image
4
- from .sprites import Sprite
5
- from .gui import Button
6
- from .color_handler import Colors, Color
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, color="gray", hover_color="white", cooldown=0.1):
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.color = parse_color(color)
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
- now = time.time()
37
- can_toggle = now - self._last_toggle >= self.cooldown
27
+ # hover check
28
+ self.hovered = (x <= mx <= x + w and y <= my <= y + h)
38
29
 
39
- if self.hovered and win.mouse_pressed(1) and can_toggle:
40
- self.value = not self.value
41
- self._last_toggle = now
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, color="lightgray", handle_color="red", handle_radius=10):
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 = parse_color(color)
66
- self.handle_color = parse_color(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
- handle_x = x + ((self.value - self.min_val) / (self.max_val - self.min_val)) * w
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
- if not self.dragging and (x <= mx <= x+w and y <= my <= y+h):
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
- rel_x = max(0, min(mx - x, w))
86
- self.value = self.min_val + (rel_x / w) * (self.max_val - self.min_val)
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) / (self.max_val - self.min_val)) * w
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, (int(handle_x), int(handle_y)), self.handle_radius)
108
+ win.draw_circle(self.handle_color,
109
+ (int(handle_x), int(handle_y)),
110
+ self.handle_radius)
@@ -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) optional
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
- # Determine width/height
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
- # Optional velocity
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
- color = img["color"]
56
- return cls(pos, size=(w, h), color=color)
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 plain colored rectangle"""
72
+ """Create sprite using a simple rectangle"""
62
73
  return cls(pos, size=(w, h), color=color)
63
74
 
64
- # === METHODS ===
75
+
76
+ # === MOVE / DRAW ===
65
77
  def draw(self, win, cam=None):
66
- """Draw sprite on CrystalWindow / Tk canvas"""
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(draw_x, draw_y, anchor="nw", image=self.image)
99
+ self.canvas_id = win.canvas.create_image(
100
+ draw_x, draw_y, anchor="nw", image=self.image
101
+ )
74
102
  else:
75
- win.draw_rect(self.color, (draw_x, draw_y, self.width, self.height))
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) and
90
- self.x + getattr(self, "width", 0) > other.x and
91
- self.y < other.y + getattr(other, "height", 0) and
92
- self.y + getattr(self, "height", 0) > other.y
93
- ) # returns tuple for consistency
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
- # update width/height
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
crystalwindow/tilemap.py CHANGED
@@ -6,7 +6,7 @@ class TileMap:
6
6
  self.tiles = []
7
7
 
8
8
  def add_tile(self, image, x, y):
9
- self.tiles.append(Sprite(image, x, y))
9
+ self.tiles.append(Sprite.image(image, x, y))
10
10
 
11
11
  def draw(self, win):
12
12
  for t in self.tiles: