saltpaper 0.0.2a0__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.
Files changed (36) hide show
  1. saltpaper-0.0.2a0/LICENSE +21 -0
  2. saltpaper-0.0.2a0/MANIFEST.in +4 -0
  3. saltpaper-0.0.2a0/PKG-INFO +32 -0
  4. saltpaper-0.0.2a0/README.md +17 -0
  5. saltpaper-0.0.2a0/pyproject.toml +30 -0
  6. saltpaper-0.0.2a0/saltpaper/__init__.py +6 -0
  7. saltpaper-0.0.2a0/saltpaper/assets/.DS_Store +0 -0
  8. saltpaper-0.0.2a0/saltpaper/assets/fonts/LibertinusMono-Regular.ttf +0 -0
  9. saltpaper-0.0.2a0/saltpaper/assets/image/missing.jpg +0 -0
  10. saltpaper-0.0.2a0/saltpaper/assets/music/catacomb.wav +0 -0
  11. saltpaper-0.0.2a0/saltpaper/assets/talk.wav +0 -0
  12. saltpaper-0.0.2a0/saltpaper/assets/tilemaps/test.aseprite +0 -0
  13. saltpaper-0.0.2a0/saltpaper/assets/tilemaps/test.png +0 -0
  14. saltpaper-0.0.2a0/saltpaper/assets/tilemaps/test.yaml +19 -0
  15. saltpaper-0.0.2a0/saltpaper/functions/__init__.py +2 -0
  16. saltpaper-0.0.2a0/saltpaper/functions/test.py +11 -0
  17. saltpaper-0.0.2a0/saltpaper/functions/vectortools.py +28 -0
  18. saltpaper-0.0.2a0/saltpaper/services/__init__.py +8 -0
  19. saltpaper-0.0.2a0/saltpaper/services/assetservice.py +68 -0
  20. saltpaper-0.0.2a0/saltpaper/services/displayservice.py +104 -0
  21. saltpaper-0.0.2a0/saltpaper/services/inputservice.py +166 -0
  22. saltpaper-0.0.2a0/saltpaper/services/layer.py +91 -0
  23. saltpaper-0.0.2a0/saltpaper/services/renderservice.py +34 -0
  24. saltpaper-0.0.2a0/saltpaper/services/stateservice.py +7 -0
  25. saltpaper-0.0.2a0/saltpaper/worldsystem/components/__init__.py +2 -0
  26. saltpaper-0.0.2a0/saltpaper/worldsystem/components/position.py +25 -0
  27. saltpaper-0.0.2a0/saltpaper/worldsystem/components/sprite.py +3 -0
  28. saltpaper-0.0.2a0/saltpaper/worldsystem/entity.py +32 -0
  29. saltpaper-0.0.2a0/saltpaper/worldsystem/world.py +14 -0
  30. saltpaper-0.0.2a0/saltpaper.egg-info/PKG-INFO +32 -0
  31. saltpaper-0.0.2a0/saltpaper.egg-info/SOURCES.txt +34 -0
  32. saltpaper-0.0.2a0/saltpaper.egg-info/dependency_links.txt +1 -0
  33. saltpaper-0.0.2a0/saltpaper.egg-info/requires.txt +2 -0
  34. saltpaper-0.0.2a0/saltpaper.egg-info/top_level.txt +1 -0
  35. saltpaper-0.0.2a0/setup.cfg +4 -0
  36. saltpaper-0.0.2a0/setup.py +3 -0
@@ -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,4 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include saltpaper/assets *
4
+ recursive-include saltpaper *.py
@@ -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,17 @@
1
+ # saltPAPER
2
+
3
+ a game engine built on pygame-ce
4
+
5
+ ## usage
6
+
7
+ TODO
8
+
9
+ ## requirements
10
+
11
+ - Python >= 3.8
12
+ - pygame-ce >= 2.4.0
13
+ - numpy >= 1.24.0
14
+ - moderngl >= 5.8.0
15
+
16
+ ## attribution
17
+ pygame-ce (LGPL license)
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "saltpaper"
7
+ version = "0.0.2a"
8
+ authors = [
9
+ { name="Lauren Kinder", email="mail@hermod.uk" },
10
+ ]
11
+ description = "A small game-engine/framework built on top of pygame"
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ dependencies = [
15
+ "pygame-ce>=2.4.0",
16
+ "numpy>=1.24.0",
17
+ ]
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent",
21
+ ]
22
+ license = "MIT"
23
+ license-files = ["LICEN[CS]E*"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["."]
27
+ include = ["saltpaper*"]
28
+
29
+ [tool.setuptools]
30
+ include-package-data = true
@@ -0,0 +1,6 @@
1
+ from saltpaper.services import *
2
+ from saltpaper.functions import *
3
+ from saltpaper.worldsystem.world import World
4
+
5
+ SALTPAPER_VER = "0.0.1"
6
+ print(f"saltPAPER {SALTPAPER_VER}")
@@ -0,0 +1,19 @@
1
+ tiles:
2
+ 0:
3
+ type: grass
4
+ solid: no
5
+ 1:
6
+ type: yellow
7
+ solid: yes
8
+ 2:
9
+ type: purple
10
+ solid: yes
11
+ 3:
12
+ type: black
13
+ solid: no
14
+ 4:
15
+ type: white
16
+ solid: yes
17
+ 5:
18
+ type: gay
19
+ solid: no
@@ -0,0 +1,2 @@
1
+ from .test import make_test_entity
2
+ from .vectortools import VectorTools
@@ -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,7 @@
1
+
2
+
3
+ class StateService:
4
+ def __init__(self):
5
+ ...
6
+
7
+ # empty box for user to put shared variables without cluttering globals
@@ -0,0 +1,2 @@
1
+ from .position import Position
2
+ from .sprite import Sprite
@@ -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,3 @@
1
+ class Sprite():
2
+ def __init__(self, asset_id:str="image_missing"):
3
+ self.asset_id = asset_id
@@ -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,34 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ setup.py
6
+ saltpaper/__init__.py
7
+ saltpaper.egg-info/PKG-INFO
8
+ saltpaper.egg-info/SOURCES.txt
9
+ saltpaper.egg-info/dependency_links.txt
10
+ saltpaper.egg-info/requires.txt
11
+ saltpaper.egg-info/top_level.txt
12
+ saltpaper/assets/.DS_Store
13
+ saltpaper/assets/talk.wav
14
+ saltpaper/assets/fonts/LibertinusMono-Regular.ttf
15
+ saltpaper/assets/image/missing.jpg
16
+ saltpaper/assets/music/catacomb.wav
17
+ saltpaper/assets/tilemaps/test.aseprite
18
+ saltpaper/assets/tilemaps/test.png
19
+ saltpaper/assets/tilemaps/test.yaml
20
+ saltpaper/functions/__init__.py
21
+ saltpaper/functions/test.py
22
+ saltpaper/functions/vectortools.py
23
+ saltpaper/services/__init__.py
24
+ saltpaper/services/assetservice.py
25
+ saltpaper/services/displayservice.py
26
+ saltpaper/services/inputservice.py
27
+ saltpaper/services/layer.py
28
+ saltpaper/services/renderservice.py
29
+ saltpaper/services/stateservice.py
30
+ saltpaper/worldsystem/entity.py
31
+ saltpaper/worldsystem/world.py
32
+ saltpaper/worldsystem/components/__init__.py
33
+ saltpaper/worldsystem/components/position.py
34
+ saltpaper/worldsystem/components/sprite.py
@@ -0,0 +1,2 @@
1
+ pygame-ce>=2.4.0
2
+ numpy>=1.24.0
@@ -0,0 +1 @@
1
+ saltpaper
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from setuptools import setup
2
+
3
+ setup()