pygkit 1.0.1__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.
pygkit/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ from .audio import *
2
+ from .transitions import *
3
+ from .ui import *
4
+ from .utils import *
5
+
6
+ __all__ = [
7
+ "SoundManager",
8
+ "SFX_CHANNELS",
9
+ "TransitionRunner",
10
+ "TransitionState",
11
+ "FadeToBlack",
12
+ "FadeFromBlack",
13
+ "Crossfade",
14
+ "Flash",
15
+ "IrisIn",
16
+ "IrisOut",
17
+ "Slide",
18
+ "PixelDissolve",
19
+ "Shake",
20
+ "Container",
21
+ "CooldownOverlay",
22
+ "ProgressBarUI",
23
+ "UIBase",
24
+ "UIOptions",
25
+ "BoxModel",
26
+ "BoxModelResult",
27
+ "generate_box_model",
28
+ "SimpleInterpolation",
29
+ "Timer",
30
+ "lerp",
31
+ "smoothstep",
32
+ "ease_in_out",
33
+ "ease_out",
34
+ "ease_in",
35
+ "load_font",
36
+ "outline",
37
+ "shadow",
38
+ "wrap",
39
+ "render_multiline",
40
+ "fit",
41
+ ]
@@ -0,0 +1,6 @@
1
+ from .soundmanager import SFX_CHANNELS, SoundManager
2
+
3
+ __all__ = [
4
+ "SoundManager",
5
+ "SFX_CHANNELS",
6
+ ]
@@ -0,0 +1,89 @@
1
+ from pathlib import Path
2
+ from typing import Dict, Literal
3
+
4
+ import pygame
5
+ from pygame import Channel, Sound
6
+
7
+ SFX_CHANNELS = 6
8
+
9
+
10
+ class SoundManager:
11
+ def __init__(self, num_channels: int = 8, main_channels: int = 2) -> None:
12
+ if pygame.mixer.get_init() is None:
13
+ raise RuntimeError("pygame.mixer not initialized")
14
+
15
+ if main_channels + SFX_CHANNELS > num_channels:
16
+ num_channels = main_channels + SFX_CHANNELS
17
+
18
+ self._main: Dict[str, Sound] = {}
19
+ self._sfx: Dict[str, Sound] = {}
20
+
21
+ self._num_channels = num_channels
22
+ self._main_count = main_channels
23
+ self._channels = tuple(pygame.Channel(i) for i in range(num_channels))
24
+ self._next_sfx = main_channels
25
+
26
+ def add_sound(
27
+ self, path: Path, key: str, kind: Literal["main", "sfx"] = "sfx"
28
+ ) -> bool:
29
+ scope = self._main if kind == "main" else self._sfx
30
+ try:
31
+ scope[key] = pygame.Sound(str(path))
32
+ return True
33
+ except (FileNotFoundError, TypeError, pygame.error) as e:
34
+ return False
35
+
36
+ def play(
37
+ self,
38
+ key: str,
39
+ kind: Literal["main", "sfx"] = "sfx",
40
+ channel_id: int | None = None,
41
+ loops: int | None = None,
42
+ ) -> None:
43
+ scope = self._main if kind == "main" else self._sfx
44
+ sound = scope.get(key)
45
+ if sound is None:
46
+ raise KeyError(f"Unknown sound '{key}'")
47
+
48
+ if loops is None:
49
+ loops = -1 if kind == "main" else 0
50
+
51
+ if channel_id is not None:
52
+ self._channels[channel_id].stop()
53
+ self._channels[channel_id].play(sound, loops)
54
+ return
55
+
56
+ channel = self._assign(kind)
57
+ channel.play(sound, loops)
58
+
59
+ def stop_all(self) -> None:
60
+ for ch in self._channels:
61
+ ch.stop()
62
+
63
+ def stop_channel(self, channel_id: int) -> None:
64
+ self._channels[channel_id].stop()
65
+
66
+ def get_channel(self, channel_id: int) -> Channel:
67
+ return self._channels[channel_id]
68
+
69
+ def _assign(self, kind: Literal["main", "sfx"]) -> Channel:
70
+ if kind == "main":
71
+ for idx in range(self._main_count):
72
+ ch = self._channels[idx]
73
+ if not ch.get_busy():
74
+ return ch
75
+ ch = self._channels[0]
76
+ ch.stop()
77
+ return ch
78
+
79
+ for idx in range(self._main_count, self._num_channels):
80
+ ch = self._channels[idx]
81
+ if not ch.get_busy():
82
+ return ch
83
+
84
+ ch = self._channels[self._next_sfx]
85
+ ch.stop()
86
+ self._next_sfx += 1
87
+ if self._next_sfx >= self._num_channels:
88
+ self._next_sfx = self._main_count
89
+ return ch
pygkit/protocols.py ADDED
@@ -0,0 +1,29 @@
1
+ from typing import Protocol
2
+
3
+ from pygame import Surface
4
+
5
+
6
+ class Renderable(Protocol):
7
+ def render(self, screen: Surface, pos_offset: tuple[int, int] = (0, 0)): ...
8
+
9
+ def update(self): ...
10
+
11
+
12
+ class UIElement(Protocol):
13
+ def render(self, screen: Surface): ...
14
+
15
+ def update(self): ...
16
+
17
+ @property
18
+ def size(self) -> tuple[int, int]: ...
19
+
20
+ @property
21
+ def pos(self) -> tuple[int, int]: ...
22
+
23
+
24
+ class Drawable(Protocol):
25
+ def draw(self, surface: Surface): ...
26
+
27
+
28
+ class Updateable(Protocol):
29
+ def update(self): ...
@@ -0,0 +1,18 @@
1
+ from .base import TransitionRunner, TransitionState
2
+ from .effects import PixelDissolve, Shake
3
+ from .fades import Crossfade, FadeFromBlack, FadeToBlack, Flash
4
+ from .wipes import IrisIn, IrisOut, Slide
5
+
6
+ __all__ = [
7
+ "TransitionRunner",
8
+ "TransitionState",
9
+ "FadeToBlack",
10
+ "FadeFromBlack",
11
+ "Crossfade",
12
+ "Flash",
13
+ "IrisIn",
14
+ "IrisOut",
15
+ "Slide",
16
+ "PixelDissolve",
17
+ "Shake",
18
+ ]
@@ -0,0 +1,97 @@
1
+ from enum import Enum, auto
2
+ from typing import Protocol
3
+
4
+ from pygame import Surface
5
+
6
+
7
+ class Transition(Protocol):
8
+ def start(self, duration: float) -> None: ...
9
+
10
+ def update(self, dt: float) -> None: ...
11
+
12
+ def render(
13
+ self,
14
+ screen: Surface,
15
+ source: Surface | None = None,
16
+ target: Surface | None = None,
17
+ ) -> None: ...
18
+
19
+ @property
20
+ def done(self) -> bool: ...
21
+
22
+ def reset(self) -> None: ...
23
+
24
+
25
+ class TransitionState(Enum):
26
+ IDLE = auto()
27
+ OUT = auto()
28
+ IN = auto()
29
+ DONE = auto()
30
+
31
+
32
+ class TransitionRunner:
33
+ __slots__ = ("fade_out", "fade_in", "state")
34
+
35
+ def __init__(
36
+ self, fade_out: Transition | None = None, fade_in: Transition | None = None
37
+ ) -> None:
38
+ self.fade_out = fade_out
39
+ self.fade_in = fade_in
40
+ self.state = TransitionState.IDLE
41
+
42
+ def start_out(self, duration: float = 0.5) -> None:
43
+ if self.fade_out is not None:
44
+ self.fade_out.start(duration)
45
+ self.state = TransitionState.OUT
46
+
47
+ def start_in(self, duration: float = 0.5) -> None:
48
+ if self.fade_in is not None:
49
+ self.fade_in.start(duration)
50
+ self.state = TransitionState.IN
51
+
52
+ def update(self, dt: float) -> None:
53
+ if self.state is TransitionState.OUT:
54
+ if self.fade_out is not None:
55
+ self.fade_out.update(dt)
56
+ if self.fade_out.done:
57
+ self.state = TransitionState.IDLE
58
+ else:
59
+ self.state = TransitionState.IDLE
60
+ elif self.state is TransitionState.IN:
61
+ if self.fade_in is not None:
62
+ self.fade_in.update(dt)
63
+ if self.fade_in.done:
64
+ self.state = TransitionState.DONE
65
+ else:
66
+ self.state = TransitionState.DONE
67
+
68
+ def render(
69
+ self,
70
+ screen: Surface,
71
+ source: Surface | None = None,
72
+ target: Surface | None = None,
73
+ ) -> None:
74
+ if self.state is TransitionState.OUT and self.fade_out is not None:
75
+ self.fade_out.render(screen, source=source, target=None)
76
+ elif self.state is TransitionState.IN and self.fade_in is not None:
77
+ self.fade_in.render(screen, source=None, target=target)
78
+ elif self.state is TransitionState.DONE and target is not None:
79
+ screen.blit(target, (0, 0))
80
+
81
+ @property
82
+ def done_out(self) -> bool:
83
+ return (
84
+ self.state is TransitionState.IDLE
85
+ and (self.fade_out is None or self.fade_out.done)
86
+ )
87
+
88
+ @property
89
+ def done(self) -> bool:
90
+ return self.state is TransitionState.DONE
91
+
92
+ def reset(self) -> None:
93
+ if self.fade_out is not None:
94
+ self.fade_out.reset()
95
+ if self.fade_in is not None:
96
+ self.fade_in.reset()
97
+ self.state = TransitionState.IDLE
@@ -0,0 +1,156 @@
1
+ import math
2
+ import random
3
+
4
+ from pygame import Surface
5
+
6
+ from ..utils.interpolation import ease_out
7
+
8
+
9
+ class PixelDissolve:
10
+ """
11
+ Transitions from source to target by revealing randomly-ordered tiles.
12
+ """
13
+
14
+ __slots__ = (
15
+ "_tile_size",
16
+ "_easing",
17
+ "_elapsed",
18
+ "_duration",
19
+ "_done",
20
+ "_order",
21
+ "_cols",
22
+ "_rows",
23
+ )
24
+
25
+ def __init__(self, tile_size: int = 8) -> None:
26
+ self._tile_size = tile_size
27
+ self._easing = ease_out
28
+ self._elapsed = 0.0
29
+ self._duration = 0.0
30
+ self._done = False
31
+ self._order: list[int] = []
32
+ self._cols = 0
33
+ self._rows = 0
34
+
35
+ def start(self, duration: float) -> None:
36
+ self._elapsed = 0.0
37
+ self._duration = duration
38
+ self._done = False
39
+ self._order = []
40
+
41
+ @property
42
+ def done(self) -> bool:
43
+ return self._done
44
+
45
+ def reset(self) -> None:
46
+ self._elapsed = 0.0
47
+ self._duration = 0.0
48
+ self._done = False
49
+ self._order.clear()
50
+ self._cols = 0
51
+ self._rows = 0
52
+
53
+ def update(self, dt: float) -> None:
54
+ if self._done:
55
+ return
56
+ self._elapsed += dt
57
+ if self._elapsed >= self._duration:
58
+ self._elapsed = self._duration
59
+ self._done = True
60
+
61
+ def render(
62
+ self,
63
+ screen: Surface,
64
+ source: Surface | None = None,
65
+ target: Surface | None = None,
66
+ ) -> None:
67
+ w, h = screen.get_size()
68
+ ts = self._tile_size
69
+ cols = max(1, int(math.ceil(w / ts)))
70
+ rows = max(1, int(math.ceil(h / ts)))
71
+
72
+ if not self._order:
73
+ self._cols = cols
74
+ self._rows = rows
75
+ self._order = list(range(cols * rows))
76
+ random.shuffle(self._order)
77
+
78
+ t = self._elapsed / self._duration if self._duration > 0 else 1.0
79
+ reveal_count = int(t * len(self._order))
80
+
81
+ revealed = set(self._order[:reveal_count])
82
+
83
+ for idx in range(cols * rows):
84
+ gx = idx % cols
85
+ gy = idx // cols
86
+ rx = gx * ts
87
+ ry = gy * ts
88
+
89
+ if idx in revealed and target is not None:
90
+ screen.blit(target, (rx, ry), (rx, ry, ts, ts))
91
+ elif source is not None:
92
+ screen.blit(source, (rx, ry), (rx, ry, ts, ts))
93
+
94
+ if self._done and target is not None:
95
+ screen.blit(target, (0, 0))
96
+
97
+
98
+ class Shake:
99
+ """
100
+ Camera shake effect.
101
+ Offsets the screen blit by a decaying random offset.
102
+ Can be used standalone or alongside another transition.
103
+ """
104
+
105
+ __slots__ = ("_intensity", "_elapsed", "_duration", "_done", "_offset")
106
+
107
+ def __init__(self, intensity: float = 8.0) -> None:
108
+ self._intensity = intensity
109
+ self._elapsed = 0.0
110
+ self._duration = 0.0
111
+ self._done = False
112
+ self._offset = (0, 0)
113
+
114
+ def start(self, duration: float = 0.3) -> None:
115
+ self._elapsed = 0.0
116
+ self._duration = duration
117
+ self._done = False
118
+
119
+ @property
120
+ def done(self) -> bool:
121
+ return self._done
122
+
123
+ def reset(self) -> None:
124
+ self._elapsed = 0.0
125
+ self._duration = 0.0
126
+ self._done = False
127
+ self._offset = (0, 0)
128
+
129
+ def update(self, dt: float) -> None:
130
+ if self._done:
131
+ return
132
+ self._elapsed += dt
133
+ if self._elapsed >= self._duration:
134
+ self._elapsed = self._duration
135
+ self._done = True
136
+ self._offset = (0, 0)
137
+ return
138
+
139
+ t = self._elapsed / self._duration
140
+ decay = 1.0 - t
141
+ current = self._intensity * decay
142
+
143
+ ox = random.uniform(-current, current)
144
+ oy = random.uniform(-current, current)
145
+ self._offset = (int(ox), int(oy))
146
+
147
+ def render(
148
+ self,
149
+ screen: Surface,
150
+ source: Surface | None = None,
151
+ target: Surface | None = None,
152
+ ) -> None:
153
+ surf = target if target is not None else source
154
+ if surf is not None:
155
+ ox, oy = self._offset
156
+ screen.blit(surf, (ox, oy))
@@ -0,0 +1,196 @@
1
+ from typing import Callable
2
+
3
+ from pygame import SRCALPHA, Surface
4
+
5
+ from ..utils.interpolation import ease_in_out
6
+
7
+
8
+ class _FadeBase:
9
+ __slots__ = ("_color", "_easing", "_overlay", "_elapsed", "_duration", "_done")
10
+
11
+ def __init__(
12
+ self,
13
+ color: tuple[int, int, int] = (0, 0, 0),
14
+ easing: Callable[[float], float] = ease_in_out,
15
+ ) -> None:
16
+ self._color = color
17
+ self._easing = easing
18
+ self._overlay: Surface | None = None
19
+ self._elapsed = 0.0
20
+ self._duration = 0.0
21
+ self._done = True
22
+
23
+ def start(self, duration: float) -> None:
24
+ self._elapsed = 0.0
25
+ self._duration = duration
26
+ self._done = False
27
+
28
+ @property
29
+ def done(self) -> bool:
30
+ return self._done
31
+
32
+ def reset(self) -> None:
33
+ self._elapsed = 0.0
34
+ self._duration = 0.0
35
+ self._done = False
36
+ self._overlay = None
37
+
38
+
39
+ class FadeToBlack(_FadeBase):
40
+ """
41
+ Fades the source surface to black.
42
+ """
43
+
44
+ def update(self, dt: float) -> None:
45
+ if self._done:
46
+ return
47
+ self._elapsed += dt
48
+ if self._elapsed >= self._duration:
49
+ self._elapsed = self._duration
50
+ self._done = True
51
+
52
+ def render(
53
+ self,
54
+ screen: Surface,
55
+ source: Surface | None = None,
56
+ target: Surface | None = None,
57
+ ) -> None:
58
+ if source is not None:
59
+ screen.blit(source, (0, 0))
60
+
61
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
62
+ alpha = int(t * 255)
63
+
64
+ if alpha > 0:
65
+ size = screen.get_size()
66
+ if (
67
+ self._overlay is None
68
+ or self._overlay.get_size() != size
69
+ ):
70
+ self._overlay = Surface(size, SRCALPHA)
71
+ self._overlay.fill((*self._color, alpha))
72
+ screen.blit(self._overlay, (0, 0))
73
+
74
+
75
+ class FadeFromBlack(_FadeBase):
76
+ """
77
+ Fades from black to reveal the target surface.
78
+ """
79
+
80
+ def update(self, dt: float) -> None:
81
+ if self._done:
82
+ return
83
+ self._elapsed += dt
84
+ if self._elapsed >= self._duration:
85
+ self._elapsed = self._duration
86
+ self._done = True
87
+
88
+ def render(
89
+ self,
90
+ screen: Surface,
91
+ source: Surface | None = None,
92
+ target: Surface | None = None,
93
+ ) -> None:
94
+ if target is not None:
95
+ screen.blit(target, (0, 0))
96
+
97
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
98
+ alpha = int((1.0 - t) * 255)
99
+
100
+ if alpha > 0:
101
+ size = screen.get_size()
102
+ if (
103
+ self._overlay is None
104
+ or self._overlay.get_size() != size
105
+ ):
106
+ self._overlay = Surface(size, SRCALPHA)
107
+ self._overlay.fill((*self._color, alpha))
108
+ screen.blit(self._overlay, (0, 0))
109
+
110
+
111
+ class Crossfade(_FadeBase):
112
+ """
113
+ Blends from source to target.
114
+ """
115
+
116
+ def __init__(self, easing: Callable[[float], float] = ease_in_out) -> None:
117
+ super().__init__(easing=easing)
118
+
119
+ def update(self, dt: float) -> None:
120
+ if self._done:
121
+ return
122
+ self._elapsed += dt
123
+ if self._elapsed >= self._duration:
124
+ self._elapsed = self._duration
125
+ self._done = True
126
+
127
+ def render(
128
+ self,
129
+ screen: Surface,
130
+ source: Surface | None = None,
131
+ target: Surface | None = None,
132
+ ) -> None:
133
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
134
+
135
+ if target is not None:
136
+ screen.blit(target, (0, 0))
137
+
138
+ if source is not None and t < 1.0:
139
+ alpha = int((1.0 - t) * 255)
140
+ size = screen.get_size()
141
+ if (
142
+ self._overlay is None
143
+ or self._overlay.get_size() != size
144
+ ):
145
+ self._overlay = Surface(size, SRCALPHA)
146
+ self._overlay.fill((0, 0, 0, 0))
147
+ self._overlay.blit(source, (0, 0))
148
+ self._overlay.set_alpha(alpha)
149
+ screen.blit(self._overlay, (0, 0))
150
+
151
+
152
+ class Flash(_FadeBase):
153
+ """
154
+ Brief white (or colored) flash that fades quickly.
155
+ Default duration 0.15s.
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ color: tuple[int, int, int] = (255, 255, 255),
161
+ easing: Callable[[float], float] = ease_in_out,
162
+ ) -> None:
163
+ super().__init__(color=color, easing=easing)
164
+
165
+ def start(self, duration: float | None = None) -> None:
166
+ super().start(0.15 if duration is None else duration)
167
+
168
+ def update(self, dt: float) -> None:
169
+ if self._done:
170
+ return
171
+ self._elapsed += dt
172
+ if self._elapsed >= self._duration:
173
+ self._elapsed = self._duration
174
+ self._done = True
175
+
176
+ def render(
177
+ self,
178
+ screen: Surface,
179
+ source: Surface | None = None,
180
+ target: Surface | None = None,
181
+ ) -> None:
182
+ if source is not None:
183
+ screen.blit(source, (0, 0))
184
+
185
+ t = self._easing(self._elapsed / self._duration) if self._duration > 0 else 1.0
186
+ alpha = int((1.0 - t) * 255)
187
+
188
+ if alpha > 0:
189
+ size = screen.get_size()
190
+ if (
191
+ self._overlay is None
192
+ or self._overlay.get_size() != size
193
+ ):
194
+ self._overlay = Surface(size, SRCALPHA)
195
+ self._overlay.fill((*self._color, alpha))
196
+ screen.blit(self._overlay, (0, 0))