saltpaper 0.0.2a0__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.
- saltpaper/__init__.py +6 -0
- saltpaper/assets/.DS_Store +0 -0
- saltpaper/assets/fonts/LibertinusMono-Regular.ttf +0 -0
- saltpaper/assets/image/missing.jpg +0 -0
- saltpaper/assets/music/catacomb.wav +0 -0
- saltpaper/assets/talk.wav +0 -0
- saltpaper/assets/tilemaps/test.aseprite +0 -0
- saltpaper/assets/tilemaps/test.png +0 -0
- saltpaper/assets/tilemaps/test.yaml +19 -0
- saltpaper/functions/__init__.py +2 -0
- saltpaper/functions/test.py +11 -0
- saltpaper/functions/vectortools.py +28 -0
- saltpaper/services/__init__.py +8 -0
- saltpaper/services/assetservice.py +68 -0
- saltpaper/services/displayservice.py +104 -0
- saltpaper/services/inputservice.py +166 -0
- saltpaper/services/layer.py +91 -0
- saltpaper/services/renderservice.py +34 -0
- saltpaper/services/stateservice.py +7 -0
- saltpaper/worldsystem/components/__init__.py +2 -0
- saltpaper/worldsystem/components/position.py +25 -0
- saltpaper/worldsystem/components/sprite.py +3 -0
- saltpaper/worldsystem/entity.py +32 -0
- saltpaper/worldsystem/world.py +14 -0
- saltpaper-0.0.2a0.dist-info/METADATA +32 -0
- saltpaper-0.0.2a0.dist-info/RECORD +29 -0
- saltpaper-0.0.2a0.dist-info/WHEEL +5 -0
- saltpaper-0.0.2a0.dist-info/licenses/LICENSE +21 -0
- saltpaper-0.0.2a0.dist-info/top_level.txt +1 -0
saltpaper/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from saltpaper.worldsystem.entity import Entity
|
|
2
|
+
from saltpaper.worldsystem.components import Position, Sprite
|
|
3
|
+
|
|
4
|
+
def make_test_entity(world, layer, x, y) -> Entity:
|
|
5
|
+
|
|
6
|
+
ent = Entity(world)
|
|
7
|
+
position = Position(layer, x, y)
|
|
8
|
+
sprite = Sprite(asset_id="image_joker")
|
|
9
|
+
ent.add_many(position, sprite)
|
|
10
|
+
|
|
11
|
+
return ent
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
class VectorTools:
|
|
4
|
+
"""I HATE MATHEMATICS"""
|
|
5
|
+
def __init__(self):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def distance_between(a:tuple[float, float], b:tuple[float, float]) -> float:
|
|
10
|
+
ax, ay = a
|
|
11
|
+
bx, by = b
|
|
12
|
+
|
|
13
|
+
xdiff = bx - ax
|
|
14
|
+
ydiff = by - ay
|
|
15
|
+
|
|
16
|
+
return math.sqrt((xdiff**2 + ydiff**2))
|
|
17
|
+
# i was worried that this would crash if given a negative number
|
|
18
|
+
# but both components are exponentiated so it can never be negative
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def lerp(a: tuple[float, float], b: tuple[float, float], progress: float = 0.5) -> tuple[float, float]:
|
|
22
|
+
ax, ay = a
|
|
23
|
+
bx, by = b
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
ax + (bx - ax) * progress,
|
|
27
|
+
ay + (by - ay) * progress
|
|
28
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from .assetservice import AssetService
|
|
2
|
+
from .displayservice import DisplayService
|
|
3
|
+
from .layer import Layer
|
|
4
|
+
from .inputservice import InputService
|
|
5
|
+
from .inputservice import Event
|
|
6
|
+
from .inputservice import Criteria
|
|
7
|
+
from .renderservice import RenderService
|
|
8
|
+
from .stateservice import StateService
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
cwd = Path.cwd()
|
|
6
|
+
filetypes = {
|
|
7
|
+
"image": [".png", ".jpg", ".bmp", ".gif"],
|
|
8
|
+
"music": [".wav", ".ogg", ".mp3"],
|
|
9
|
+
"sound": [".wav", ".ogg", ".mp3"],
|
|
10
|
+
"tilesheet": [".tls"], # todo
|
|
11
|
+
"room": [".room"] #todo
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class AssetService():
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.cache = {}
|
|
17
|
+
self.roots = []
|
|
18
|
+
|
|
19
|
+
def set_assets_folder(self, path):
|
|
20
|
+
path = Path(path)
|
|
21
|
+
self.roots.append(path)
|
|
22
|
+
|
|
23
|
+
def get_asset(self, id):
|
|
24
|
+
asset = self.cache.get(id)
|
|
25
|
+
if asset is not None:
|
|
26
|
+
return asset
|
|
27
|
+
|
|
28
|
+
kind, name = id.split("_", 1)
|
|
29
|
+
extensions = filetypes.get(kind)
|
|
30
|
+
|
|
31
|
+
if extensions is None:
|
|
32
|
+
raise ValueError(f"unknown asset type: {kind}")
|
|
33
|
+
|
|
34
|
+
searched = []
|
|
35
|
+
for root in self.roots:
|
|
36
|
+
for ext in extensions:
|
|
37
|
+
path = root / kind / f"{name}{ext}"
|
|
38
|
+
searched.append(str(path))
|
|
39
|
+
if path.exists():
|
|
40
|
+
asset = self._load_asset(kind, path)
|
|
41
|
+
self.cache[id] = asset
|
|
42
|
+
return asset
|
|
43
|
+
|
|
44
|
+
print(
|
|
45
|
+
f"asset not found: '{id}' (type='{kind}', name='{name}'). "
|
|
46
|
+
f"tried locations:" + '\n'.join(searched) + "\n",
|
|
47
|
+
f"ensure the asset exists in 'game/assets' or 'engine/assets' and that the id is formatted as '<type>_<name>'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
return self.get_asset("image_missing")
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
raise FileNotFoundError(f"No /image/missing.png is set")
|
|
54
|
+
|
|
55
|
+
def _load_asset(self, kind, path: Path):
|
|
56
|
+
if kind == "image":
|
|
57
|
+
return pygame.image.load(path).convert_alpha()
|
|
58
|
+
|
|
59
|
+
if kind == "music":
|
|
60
|
+
pygame.mixer.music.load(path)
|
|
61
|
+
return path # music is streamed so just return path
|
|
62
|
+
|
|
63
|
+
if kind == "sound":
|
|
64
|
+
return pygame.mixer.Sound(path)
|
|
65
|
+
|
|
66
|
+
# tilesheet etc later
|
|
67
|
+
return path
|
|
68
|
+
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import sys
|
|
3
|
+
from statistics import mean
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from saltpaper import InputService
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
12
|
+
|
|
13
|
+
class DisplayService():
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
dimensions,
|
|
18
|
+
inputservice,
|
|
19
|
+
target_frame_rate:int=120,
|
|
20
|
+
caption="saltpaper engine display",
|
|
21
|
+
vsync=True, # for testing
|
|
22
|
+
iconpath=None
|
|
23
|
+
):
|
|
24
|
+
self.dimensions = dimensions
|
|
25
|
+
self.caption = caption
|
|
26
|
+
self.inputservice:'InputService' = inputservice
|
|
27
|
+
self.target_frame_rate = target_frame_rate
|
|
28
|
+
|
|
29
|
+
self.layers = []
|
|
30
|
+
|
|
31
|
+
self.refresh_sorting()
|
|
32
|
+
|
|
33
|
+
pygame.init()
|
|
34
|
+
|
|
35
|
+
self.funcs = []
|
|
36
|
+
iconsurf = pygame.image.load(iconpath)
|
|
37
|
+
pygame.display.set_icon(iconsurf)
|
|
38
|
+
self.display = pygame.display.set_mode(dimensions, vsync=vsync)
|
|
39
|
+
|
|
40
|
+
pygame.display.set_caption(caption)
|
|
41
|
+
self.clock = pygame.time.Clock()
|
|
42
|
+
self.delta = 1
|
|
43
|
+
self.deltas = []
|
|
44
|
+
|
|
45
|
+
self.running = True
|
|
46
|
+
self.dirty = True
|
|
47
|
+
|
|
48
|
+
def mount(self, func=None):
|
|
49
|
+
if not func: return
|
|
50
|
+
self.funcs.append(func)
|
|
51
|
+
|
|
52
|
+
def refresh_sorting(self):
|
|
53
|
+
self.layers_by_tick = sorted(self.layers, key=lambda l: l.tick_priority)
|
|
54
|
+
self.layers_by_render = sorted(self.layers, key=lambda l: l.render_priority)
|
|
55
|
+
|
|
56
|
+
def add_layer(self, layer):
|
|
57
|
+
self.layers.append(layer)
|
|
58
|
+
self.refresh_sorting()
|
|
59
|
+
|
|
60
|
+
def add_many_layers(self, layers):
|
|
61
|
+
self.layers.extend(layers)
|
|
62
|
+
self.refresh_sorting
|
|
63
|
+
|
|
64
|
+
def remove_layer(self, layer):
|
|
65
|
+
for i, item in enumerate(self.layers):
|
|
66
|
+
if item is layer:
|
|
67
|
+
self.layers.pop(i)
|
|
68
|
+
self.refresh_sorting()
|
|
69
|
+
return True
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def get_layers(self):
|
|
73
|
+
return self.layers
|
|
74
|
+
|
|
75
|
+
def tick(self):
|
|
76
|
+
if len(self.layers) == 0:
|
|
77
|
+
raise ValueError("the display service has no layers to display. make sure they are added with displayservice.add_layer(layer)")
|
|
78
|
+
|
|
79
|
+
self.events = pygame.event.get()
|
|
80
|
+
self.inputservice.tick(self.events)
|
|
81
|
+
for event in self.events:
|
|
82
|
+
if event.type == pygame.QUIT:
|
|
83
|
+
self.running = False
|
|
84
|
+
|
|
85
|
+
for layer in self.layers_by_tick:
|
|
86
|
+
if layer.ticking:
|
|
87
|
+
layer.tick(self.delta)
|
|
88
|
+
|
|
89
|
+
for layer in self.layers_by_render:
|
|
90
|
+
if not layer.visible:
|
|
91
|
+
continue
|
|
92
|
+
surf = layer.render()
|
|
93
|
+
offset = layer.offset
|
|
94
|
+
self.display.blit(surf, offset)
|
|
95
|
+
|
|
96
|
+
for func in self.funcs:
|
|
97
|
+
func(self, self.delta)
|
|
98
|
+
|
|
99
|
+
pygame.display.flip()
|
|
100
|
+
delta_entry = self.clock.tick(self.target_frame_rate) / 1000
|
|
101
|
+
self.deltas.append(delta_entry)
|
|
102
|
+
if len(self.deltas) > 10:
|
|
103
|
+
self.deltas.pop(0)
|
|
104
|
+
self.delta = mean(self.deltas)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
|
|
2
|
+
# input mapping service
|
|
3
|
+
# initialises with a mapping dict of
|
|
4
|
+
# key trigger -> "event" (arbitrary string)
|
|
5
|
+
# i.e. K_up
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
12
|
+
|
|
13
|
+
import pygame
|
|
14
|
+
import pygame.locals as pl
|
|
15
|
+
import pygame._sdl2.controller as ctrl
|
|
16
|
+
|
|
17
|
+
KEY_VALUE_TO_NAME = {
|
|
18
|
+
value: name
|
|
19
|
+
for name, value in vars(pl).items()
|
|
20
|
+
if name.startswith("K_")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
BUTTON_VALUE_TO_NAME = {
|
|
24
|
+
value: name
|
|
25
|
+
for name, value in vars(pl).items()
|
|
26
|
+
if name.startswith("CONTROLLER_BUTTON_")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
MOUSE_VALUE_TO_NAME = {
|
|
30
|
+
1: "MOUSE_LEFT",
|
|
31
|
+
2: "MOUSE_MIDDLE",
|
|
32
|
+
3: "MOUSE_RIGHT",
|
|
33
|
+
4: "MOUSE_SCROLL_UP",
|
|
34
|
+
5: "MOUSE_SCROLL_DOWN"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
EVENT_TYPES_LISTENING = {
|
|
39
|
+
pygame.KEYDOWN: "key",
|
|
40
|
+
pygame.KEYUP: "key",
|
|
41
|
+
pygame.CONTROLLERBUTTONDOWN: "button",
|
|
42
|
+
pygame.CONTROLLERBUTTONUP: "button",
|
|
43
|
+
pygame.MOUSEBUTTONDOWN: "mouse",
|
|
44
|
+
pygame.MOUSEBUTTONUP: "mouse",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
with open("validevents.txt", "w") as f:
|
|
49
|
+
f.write("=== KEY EVENTS ===\n")
|
|
50
|
+
for item in KEY_VALUE_TO_NAME.values():
|
|
51
|
+
f.write(f"{item}\n")
|
|
52
|
+
f.write("=== BUTTON EVENTS ===\n")
|
|
53
|
+
for item in BUTTON_VALUE_TO_NAME.values():
|
|
54
|
+
f.write(f"{item}\n")
|
|
55
|
+
f.write("=== KEY EVENTS ===\n")
|
|
56
|
+
for item in MOUSE_VALUE_TO_NAME.values():
|
|
57
|
+
f.write(f"{item}\n")
|
|
58
|
+
|
|
59
|
+
class Criteria():
|
|
60
|
+
@staticmethod
|
|
61
|
+
def on_press(f):
|
|
62
|
+
return True if f==1 else False
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def on_held(f):
|
|
66
|
+
return True if f>0 else False
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def on_release(f):
|
|
70
|
+
return True if f==-1 else False
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def make_on_held_interval(x):
|
|
74
|
+
def on_held_interval(f):
|
|
75
|
+
return True if f % x == 0 else False
|
|
76
|
+
return on_held_interval
|
|
77
|
+
|
|
78
|
+
class Event():
|
|
79
|
+
def __init__(self, triggers: str | list, criteria: Callable, callback: Callable, args: list = None):
|
|
80
|
+
self.triggers = triggers if isinstance(triggers, list) else [triggers]
|
|
81
|
+
self.criteria = criteria
|
|
82
|
+
self.callback = callback
|
|
83
|
+
self.args = args if args else []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class InputService():
|
|
87
|
+
def __init__(self):
|
|
88
|
+
self.gamepad = None
|
|
89
|
+
self.input_roster = {}
|
|
90
|
+
self.events = []
|
|
91
|
+
try:
|
|
92
|
+
ctrl.init()
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
for item in KEY_VALUE_TO_NAME.values():
|
|
96
|
+
self.input_roster[item] = 0
|
|
97
|
+
for item in BUTTON_VALUE_TO_NAME.values():
|
|
98
|
+
self.input_roster[item] = 0
|
|
99
|
+
for item in MOUSE_VALUE_TO_NAME.values():
|
|
100
|
+
self.input_roster[item] = 0
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def mouse_pos(self):
|
|
104
|
+
return pygame.mouse.get_pos()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def register_event(self, event: Event):
|
|
108
|
+
self.events.append(event)
|
|
109
|
+
|
|
110
|
+
def check_events(self):
|
|
111
|
+
for trigger, frames in self.input_roster.items():
|
|
112
|
+
if frames == 0:
|
|
113
|
+
continue
|
|
114
|
+
for event in self.events:
|
|
115
|
+
if trigger not in event.triggers:
|
|
116
|
+
continue
|
|
117
|
+
if event.criteria(frames):
|
|
118
|
+
event.callback(frames, *event.args)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def controllercheck(self):
|
|
122
|
+
try:
|
|
123
|
+
count = ctrl.get_count()
|
|
124
|
+
except pygame.error:
|
|
125
|
+
count = 0
|
|
126
|
+
|
|
127
|
+
if count > 0 and self.gamepad is None:
|
|
128
|
+
try:
|
|
129
|
+
self.gamepad = ctrl.Controller(0)
|
|
130
|
+
except Exception:
|
|
131
|
+
self.gamepad = None
|
|
132
|
+
elif count == 0:
|
|
133
|
+
self.gamepad = None
|
|
134
|
+
|
|
135
|
+
def tick(self, events):
|
|
136
|
+
self.controllercheck()
|
|
137
|
+
self.process_events(events)
|
|
138
|
+
self.check_events()
|
|
139
|
+
for item in self.input_roster:
|
|
140
|
+
|
|
141
|
+
if self.input_roster[item] == 0: # never pressed
|
|
142
|
+
continue
|
|
143
|
+
if self.input_roster[item] > 0: # positive / pressed
|
|
144
|
+
self.input_roster[item] += 1
|
|
145
|
+
elif self.input_roster[item] < 0: # negative / unpressed after first press
|
|
146
|
+
self.input_roster[item] -= 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def process_events(self, events):
|
|
150
|
+
for event in events:
|
|
151
|
+
event_type = event.type
|
|
152
|
+
if not event_type in EVENT_TYPES_LISTENING.keys():
|
|
153
|
+
continue
|
|
154
|
+
target = EVENT_TYPES_LISTENING[event.type]
|
|
155
|
+
value = event.button if target == "mouse" else getattr(event, target, None)
|
|
156
|
+
if target == "key":
|
|
157
|
+
name = KEY_VALUE_TO_NAME.get(value, "unknown")
|
|
158
|
+
elif target == "button":
|
|
159
|
+
name = BUTTON_VALUE_TO_NAME.get(value, "unknown")
|
|
160
|
+
elif target == "mouse":
|
|
161
|
+
name = MOUSE_VALUE_TO_NAME.get(value, "unknown")
|
|
162
|
+
else:
|
|
163
|
+
name = "unknown"
|
|
164
|
+
updown = -1 if event_type in [pygame.KEYUP, pygame.CONTROLLERBUTTONUP, pygame.MOUSEBUTTONUP] else 1
|
|
165
|
+
self.input_roster[name] = updown
|
|
166
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class Layer:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
dimensions,
|
|
8
|
+
render_priority=0,
|
|
9
|
+
tick_priority=0,
|
|
10
|
+
opacity_percent=100,
|
|
11
|
+
surface=None,
|
|
12
|
+
):
|
|
13
|
+
self.dimensions = dimensions
|
|
14
|
+
self.surface = surface if surface else pygame.Surface(dimensions, pygame.HWSURFACE | pygame.SRCALPHA)
|
|
15
|
+
self.visible = True
|
|
16
|
+
self.ticking = True
|
|
17
|
+
self.opacity_percent = opacity_percent
|
|
18
|
+
self.offset = (0,0)
|
|
19
|
+
|
|
20
|
+
self.render_priority = render_priority
|
|
21
|
+
self.tick_priority = tick_priority
|
|
22
|
+
|
|
23
|
+
self.funcs = []
|
|
24
|
+
|
|
25
|
+
# loopscroll()
|
|
26
|
+
self._loopscroll_accum_x = 0.0
|
|
27
|
+
self._loopscroll_accum_y = 0.0
|
|
28
|
+
|
|
29
|
+
def mount(self, func=None):
|
|
30
|
+
"""mount a function to be called when this layer ticks. gives the layer and delta as arguments (i.e. `main(layer, delta)`)"""
|
|
31
|
+
if not func: return
|
|
32
|
+
self.funcs.append(func)
|
|
33
|
+
|
|
34
|
+
def tick(self, delta):
|
|
35
|
+
for func in self.funcs:
|
|
36
|
+
func(self, delta)
|
|
37
|
+
|
|
38
|
+
def render(self):
|
|
39
|
+
if self.opacity_percent >= 100:
|
|
40
|
+
return self.surface
|
|
41
|
+
|
|
42
|
+
surf = self.surface.copy()
|
|
43
|
+
surf.set_alpha(int(self.opacity_percent * 2.55))
|
|
44
|
+
return surf
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def loopscroll(self, dx, dy, dt=1.0):
|
|
48
|
+
self._loopscroll_accum_x += dx * dt
|
|
49
|
+
self._loopscroll_accum_y += dy * dt
|
|
50
|
+
|
|
51
|
+
scroll_x = int(self._loopscroll_accum_x)
|
|
52
|
+
scroll_y = int(self._loopscroll_accum_y)
|
|
53
|
+
|
|
54
|
+
self._loopscroll_accum_x -= scroll_x
|
|
55
|
+
self._loopscroll_accum_y -= scroll_y
|
|
56
|
+
|
|
57
|
+
if scroll_x == 0 and scroll_y == 0:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
surf = self.surface
|
|
61
|
+
scroll_area_x = (abs(scroll_x), surf.get_height())
|
|
62
|
+
scroll_area_y = (surf.get_width(), abs(scroll_y))
|
|
63
|
+
if scroll_x != 0:
|
|
64
|
+
tempx = pygame.Surface(scroll_area_x, pygame.SRCALPHA)
|
|
65
|
+
if scroll_y != 0:
|
|
66
|
+
tempy = pygame.Surface(scroll_area_y, pygame.SRCALPHA)
|
|
67
|
+
|
|
68
|
+
top = 0
|
|
69
|
+
left = 0
|
|
70
|
+
right = surf.get_width()
|
|
71
|
+
bottom = surf.get_height()
|
|
72
|
+
|
|
73
|
+
if scroll_x > 0: # image is moving right, copy rightmost chunk to left
|
|
74
|
+
tempx.blit(surf, (0,0), ((right-scroll_x, top), (scroll_x, bottom)))
|
|
75
|
+
surf.scroll(scroll_x, 0)
|
|
76
|
+
surf.blit(tempx, (left,top))
|
|
77
|
+
elif scroll_x < 0: # image is moving left, copy leftmost chunk to right
|
|
78
|
+
tempx.blit(surf, (0,0), ((0, top), (-scroll_x, bottom)))
|
|
79
|
+
surf.scroll(scroll_x, 0)
|
|
80
|
+
surf.blit(tempx, (right+scroll_x,top))
|
|
81
|
+
|
|
82
|
+
if scroll_y > 0: # image is moving down, copy bottom chunk to top
|
|
83
|
+
tempy.blit(surf, (0,0), ((left, bottom-scroll_y), (right, scroll_y)))
|
|
84
|
+
surf.scroll(0, scroll_y)
|
|
85
|
+
surf.blit(tempy, (left, top))
|
|
86
|
+
elif scroll_y < 0: # image is moving up, copy top chunk to bottom
|
|
87
|
+
tempy.blit(surf, (0,0), ((left, 0), (right, -scroll_y)))
|
|
88
|
+
surf.scroll(0, scroll_y)
|
|
89
|
+
surf.blit(tempy, (left, bottom+scroll_y))
|
|
90
|
+
|
|
91
|
+
self.surface = surf
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pygame
|
|
2
|
+
|
|
3
|
+
from saltpaper.services.layer import Layer
|
|
4
|
+
from saltpaper.services.assetservice import AssetService
|
|
5
|
+
from saltpaper.worldsystem.components.sprite import Sprite
|
|
6
|
+
|
|
7
|
+
class RenderService():
|
|
8
|
+
def __init__(self, world, assetservice: AssetService):
|
|
9
|
+
self.world = world
|
|
10
|
+
self.assetservice = assetservice
|
|
11
|
+
self.render_queue:dict[Layer, list] = {}
|
|
12
|
+
|
|
13
|
+
def _queue(self, layer:Layer, pos:tuple[int,int], surface:pygame.Surface):
|
|
14
|
+
item = (surface, pos)
|
|
15
|
+
if not self.render_queue.get(layer, None):
|
|
16
|
+
self.render_queue[layer] = []
|
|
17
|
+
self.render_queue[layer].append(item)
|
|
18
|
+
|
|
19
|
+
def _process_queue(self):
|
|
20
|
+
for layer, items in self.render_queue.items():
|
|
21
|
+
layer.surface.blits(items)
|
|
22
|
+
self.render_queue.clear()
|
|
23
|
+
|
|
24
|
+
def render(self, layer:Layer, pos:tuple[int,int], asset_id:str):
|
|
25
|
+
surf = self.assetservice.get_asset(asset_id)
|
|
26
|
+
self._queue(layer, pos, surf)
|
|
27
|
+
|
|
28
|
+
def _render_renderables(self, renderables):
|
|
29
|
+
for renderable in renderables:
|
|
30
|
+
self.render(renderable.layer, renderable.position, renderable.asset_id)
|
|
31
|
+
|
|
32
|
+
def tick(self):
|
|
33
|
+
self._render_renderables(self.world.collect_component_type(Sprite))
|
|
34
|
+
self._process_queue()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
class Position():
|
|
2
|
+
def __init__(
|
|
3
|
+
self,
|
|
4
|
+
layer:int=0,
|
|
5
|
+
x:int=0,
|
|
6
|
+
y:int=0,
|
|
7
|
+
height:int=0,
|
|
8
|
+
width:int=0
|
|
9
|
+
):
|
|
10
|
+
self.layer = layer
|
|
11
|
+
self.x = x
|
|
12
|
+
self.y = y
|
|
13
|
+
self.height = height
|
|
14
|
+
self.width = width
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def position(self):
|
|
18
|
+
return (self.x, self.y)
|
|
19
|
+
|
|
20
|
+
def set_layer(self, layer):
|
|
21
|
+
self.layer = layer
|
|
22
|
+
|
|
23
|
+
def move(self, dx, dy):
|
|
24
|
+
self.x += dx
|
|
25
|
+
self.y += dy
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
|
|
2
|
+
class Entity():
|
|
3
|
+
def __init__(self, world):
|
|
4
|
+
self.components = {}
|
|
5
|
+
world.entities.append(self)
|
|
6
|
+
|
|
7
|
+
def add(self, component):
|
|
8
|
+
self.components[type(component)] = component
|
|
9
|
+
|
|
10
|
+
def add_many(self, *components):
|
|
11
|
+
for component in components:
|
|
12
|
+
self.add(component)
|
|
13
|
+
|
|
14
|
+
def get(self, component_type):
|
|
15
|
+
for comp_cls, comp in self.components.items():
|
|
16
|
+
if issubclass(comp_cls, component_type):
|
|
17
|
+
return comp
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
def has(self, component_type):
|
|
21
|
+
return (self.get(component_type) is not None)
|
|
22
|
+
|
|
23
|
+
def remove(self, component_type):
|
|
24
|
+
comp = self.get(component_type)
|
|
25
|
+
if comp:
|
|
26
|
+
self.components.pop(type(comp), None)
|
|
27
|
+
|
|
28
|
+
def __getattr__(self, name):
|
|
29
|
+
for component in self.components.values():
|
|
30
|
+
if hasattr(component, name):
|
|
31
|
+
return getattr(component, name)
|
|
32
|
+
raise AttributeError(f"{type(self).__name__} has no attribute '{name}'")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from saltpaper.worldsystem.components.sprite import Sprite
|
|
2
|
+
from saltpaper.worldsystem.entity import Entity
|
|
3
|
+
|
|
4
|
+
class World():
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.entities: list[Entity] = []
|
|
7
|
+
|
|
8
|
+
def collect_component_type(self, component_type):
|
|
9
|
+
renderables = []
|
|
10
|
+
for entity in self.entities:
|
|
11
|
+
if not entity.has(component_type):
|
|
12
|
+
continue
|
|
13
|
+
renderables.append(entity)
|
|
14
|
+
return renderables
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: saltpaper
|
|
3
|
+
Version: 0.0.2a0
|
|
4
|
+
Summary: A small game-engine/framework built on top of pygame
|
|
5
|
+
Author-email: Lauren Kinder <mail@hermod.uk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pygame-ce>=2.4.0
|
|
13
|
+
Requires-Dist: numpy>=1.24.0
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# saltPAPER
|
|
17
|
+
|
|
18
|
+
a game engine built on pygame-ce
|
|
19
|
+
|
|
20
|
+
## usage
|
|
21
|
+
|
|
22
|
+
TODO
|
|
23
|
+
|
|
24
|
+
## requirements
|
|
25
|
+
|
|
26
|
+
- Python >= 3.8
|
|
27
|
+
- pygame-ce >= 2.4.0
|
|
28
|
+
- numpy >= 1.24.0
|
|
29
|
+
- moderngl >= 5.8.0
|
|
30
|
+
|
|
31
|
+
## attribution
|
|
32
|
+
pygame-ce (LGPL license)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
saltpaper/__init__.py,sha256=cZ3wNR0lUAwlyUgL4GWFUMvQ5Z1k6QkfIe1RD8ICbWg,173
|
|
2
|
+
saltpaper/assets/.DS_Store,sha256=7-Z-79uIc0CMx4UUj3sSlYE6KAWyk5qlKRTLRnrq8N8,8196
|
|
3
|
+
saltpaper/assets/talk.wav,sha256=rRmMxGFBFGiU59VqfmhIcwdZfuYdY9fUdkv7Os7S3hU,30624
|
|
4
|
+
saltpaper/assets/fonts/LibertinusMono-Regular.ttf,sha256=q9MJTt9ehi35gD8LH8jgr66CFH2oyxsylV3nqPsIOPA,153120
|
|
5
|
+
saltpaper/assets/image/missing.jpg,sha256=OLQF7lfMNkgyL62xxne6Os7NVomL7avXjgeMJJUSrhU,68783
|
|
6
|
+
saltpaper/assets/music/catacomb.wav,sha256=Rx3u6xe2YnST9c7KKJ6seF9bG6rJyC683Sc6j2EGtyQ,15360044
|
|
7
|
+
saltpaper/assets/tilemaps/test.aseprite,sha256=ZmWh2Ima2Lha5t4kjgNMsnlCamBNEKUsO547Hhtoa2w,416
|
|
8
|
+
saltpaper/assets/tilemaps/test.png,sha256=Q2LU88aK3DOLHrXBjuT86ZJTP_ckODwo1g5Pe9Jut7g,927
|
|
9
|
+
saltpaper/assets/tilemaps/test.yaml,sha256=8YWZeFfbxKxSK8MnEYxERjJIpSszMzRxp5lzGNl2yYw,220
|
|
10
|
+
saltpaper/functions/__init__.py,sha256=seVutHUkY36p2YR6oMHmh4RA9ofWhpMKjVxCl-YNxVA,71
|
|
11
|
+
saltpaper/functions/test.py,sha256=sNpRCjy1CrMrlHv770RTtN6iDwB3Hq7-rKdKaTi3b8s,323
|
|
12
|
+
saltpaper/functions/vectortools.py,sha256=I2ok4i0kZqGq5YXgY1nLblgA14mnlC53D2BBSHrbsHM,752
|
|
13
|
+
saltpaper/services/__init__.py,sha256=Sseu7zdeaB3InIIcs5DIRFK4hhGBqxwvTQaAQP8ksaw,292
|
|
14
|
+
saltpaper/services/assetservice.py,sha256=DQOEo3IXWYdrDXrvLztC1Z6f2QeCNk41oGP08w8Ya_c,1957
|
|
15
|
+
saltpaper/services/displayservice.py,sha256=ihyMK8gy3IDYl2HGFhsVMxbTAQo0_r8KiaEk644ummc,2992
|
|
16
|
+
saltpaper/services/inputservice.py,sha256=WNdD0wE2okTbRJDtMBPnxyHmkde0dIocbtSAlX-V408,4884
|
|
17
|
+
saltpaper/services/layer.py,sha256=YoJQ0fwQ4EgXkJWHlgwVd8uv9TnafnmQL1NljoRRxRQ,3089
|
|
18
|
+
saltpaper/services/renderservice.py,sha256=jwhnWoa1a_mHXkoHnK-wREGR0cIBKkGf-pVythHkU1E,1250
|
|
19
|
+
saltpaper/services/stateservice.py,sha256=dxA7H_ABTFyfoClXplFprfD45koN2PGTUQL8-UprO7s,138
|
|
20
|
+
saltpaper/worldsystem/entity.py,sha256=bRyFMfHblzMwzo81YshDkDjvpMXZrEVrvWjgXtdlS5c,987
|
|
21
|
+
saltpaper/worldsystem/world.py,sha256=-8ckweO6pcwaLb42PGKPveuuHNeR_o0cVqUDFcEt93w,442
|
|
22
|
+
saltpaper/worldsystem/components/__init__.py,sha256=ZYfSsWkkqC-Dkl9QwAjPBydca7ifBzxEhG9_OOcMgRs,57
|
|
23
|
+
saltpaper/worldsystem/components/position.py,sha256=lNNh6BoErEEq7SgM347ceLNMOAdnkq1e2LMr_ejZmwE,500
|
|
24
|
+
saltpaper/worldsystem/components/sprite.py,sha256=pQHkyNNKv_iy33LLbqZlOXTU-dqoPzFX7ojjYMOH_BY,102
|
|
25
|
+
saltpaper-0.0.2a0.dist-info/licenses/LICENSE,sha256=Fh0aFC8RSdix3JzjFyLJ3IuzsD1FfsbGFkyqDj5qvXQ,1075
|
|
26
|
+
saltpaper-0.0.2a0.dist-info/METADATA,sha256=bA-yImAorT8KaS85kmudg4tQZIJ7Ma3zquGG18eEmuY,646
|
|
27
|
+
saltpaper-0.0.2a0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
28
|
+
saltpaper-0.0.2a0.dist-info/top_level.txt,sha256=JACZnZhYYkgmdSz3T1JXacHS4L0UM6NbeReLoNLu6jw,10
|
|
29
|
+
saltpaper-0.0.2a0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Lauren Kinder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
saltpaper
|