olympus-engine 0.1.0__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.
- olympus/__init__.py +22 -0
- olympus/anim.py +20 -0
- olympus/collide.py +10 -0
- olympus/editor.py +191 -0
- olympus/font.py +43 -0
- olympus/palette.py +26 -0
- olympus/screen.py +199 -0
- olympus/sound.py +69 -0
- olympus/sprites.py +31 -0
- olympus/tilemap.py +65 -0
- olympus_engine-0.1.0.dist-info/METADATA +116 -0
- olympus_engine-0.1.0.dist-info/RECORD +16 -0
- olympus_engine-0.1.0.dist-info/WHEEL +5 -0
- olympus_engine-0.1.0.dist-info/entry_points.txt +2 -0
- olympus_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
- olympus_engine-0.1.0.dist-info/top_level.txt +1 -0
olympus/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .screen import Screen, LEFT, RIGHT, UP, DOWN, O, X
|
|
2
|
+
from .palette import (
|
|
3
|
+
PALETTE, BLACK, DARKBLUE, DARKPURPLE, DARKGREEN, BROWN, DARKGREY,
|
|
4
|
+
LIGHTGREY, WHITE, RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, PINK, PEACH,
|
|
5
|
+
)
|
|
6
|
+
from .sprites import make_sprite, load_sprite
|
|
7
|
+
from .sound import Sound, load_wav
|
|
8
|
+
from .anim import Anim
|
|
9
|
+
from .tilemap import Tilemap, TILE
|
|
10
|
+
from .collide import overlap, hits_any
|
|
11
|
+
from .font import FONT
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Screen", "LEFT", "RIGHT", "UP", "DOWN", "O", "X",
|
|
17
|
+
"PALETTE", "BLACK", "DARKBLUE", "DARKPURPLE", "DARKGREEN", "BROWN",
|
|
18
|
+
"DARKGREY", "LIGHTGREY", "WHITE", "RED", "ORANGE", "YELLOW", "GREEN",
|
|
19
|
+
"BLUE", "INDIGO", "PINK", "PEACH",
|
|
20
|
+
"make_sprite", "load_sprite", "Sound", "load_wav", "Anim",
|
|
21
|
+
"Tilemap", "TILE", "overlap", "hits_any", "FONT",
|
|
22
|
+
]
|
olympus/anim.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class Anim:
|
|
2
|
+
def __init__(self, frames, speed=6):
|
|
3
|
+
self.frames = frames
|
|
4
|
+
self.speed = speed
|
|
5
|
+
self.t = 0
|
|
6
|
+
self.i = 0
|
|
7
|
+
|
|
8
|
+
def reset(self):
|
|
9
|
+
self.t = 0
|
|
10
|
+
self.i = 0
|
|
11
|
+
|
|
12
|
+
def update(self):
|
|
13
|
+
self.t += 1
|
|
14
|
+
if self.t >= self.speed:
|
|
15
|
+
self.t = 0
|
|
16
|
+
self.i = (self.i + 1) % len(self.frames)
|
|
17
|
+
|
|
18
|
+
def frame(self):
|
|
19
|
+
return self.frames[self.i]
|
|
20
|
+
|
olympus/collide.py
ADDED
olympus/editor.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import sdl2
|
|
3
|
+
import numpy as np
|
|
4
|
+
from .screen import Screen
|
|
5
|
+
|
|
6
|
+
VW = VH = 128
|
|
7
|
+
SCALE = 5
|
|
8
|
+
GRID = 16
|
|
9
|
+
CANVAS = 96
|
|
10
|
+
CELL = CANVAS // GRID
|
|
11
|
+
CANVAS_X = CANVAS_Y = 4
|
|
12
|
+
PAL_X, PAL_Y, SW = 4, 104, 7
|
|
13
|
+
PV_X, PV_Y = 104, 4
|
|
14
|
+
TRANSPARENT = 255
|
|
15
|
+
SAVE_FILE = "sprite.sav"
|
|
16
|
+
|
|
17
|
+
grid = np.full((GRID, GRID), TRANSPARENT, dtype=np.uint8)
|
|
18
|
+
selected = 8
|
|
19
|
+
undo_stack = []
|
|
20
|
+
onion = None
|
|
21
|
+
show_onion = True
|
|
22
|
+
|
|
23
|
+
def in_canvas(mx, my):
|
|
24
|
+
return (CANVAS_X <= mx < CANVAS_X + GRID * CELL and CANVAS_Y <= my < CANVAS_Y + GRID * CELL)
|
|
25
|
+
|
|
26
|
+
def canvas_cell(mx, my):
|
|
27
|
+
return (my - CANVAS_Y) // CELL, (mx - CANVAS_X) // CELL
|
|
28
|
+
|
|
29
|
+
def palette_index(mx, my):
|
|
30
|
+
if PAL_Y <= my < PAL_Y + SW and mx >= PAL_X:
|
|
31
|
+
i = (mx - PAL_X) // SW
|
|
32
|
+
if 0 <= i < 16:
|
|
33
|
+
return i
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
def grid_rows():
|
|
37
|
+
out = []
|
|
38
|
+
for r in range(GRID):
|
|
39
|
+
line = "".join("." if grid[r, c] == TRANSPARENT else format(grid[r, c], "x") for c in range(GRID))
|
|
40
|
+
out.append(line)
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
def to_code():
|
|
44
|
+
body = "\n".join(' "%s",' % row for row in grid_rows())
|
|
45
|
+
return "make_sprite([\n" + body + "\n])"
|
|
46
|
+
|
|
47
|
+
def push_undo():
|
|
48
|
+
undo_stack.append(grid.copy())
|
|
49
|
+
if len(undo_stack) > 50:
|
|
50
|
+
undo_stack.pop(0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_grid():
|
|
54
|
+
with open(SAVE_FILE, "w") as f:
|
|
55
|
+
f.write("\n".join(grid_rows()) + "\n")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_grid():
|
|
59
|
+
try:
|
|
60
|
+
with open(SAVE_FILE) as f:
|
|
61
|
+
lines = [ln.rstrip("\n") for ln in f if ln.strip()]
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
return
|
|
64
|
+
push_undo()
|
|
65
|
+
grid[:, :] = TRANSPARENT
|
|
66
|
+
for r in range(min(GRID, len(lines))):
|
|
67
|
+
for c in range(min(GRID, len(lines[r]))):
|
|
68
|
+
ch = lines[r][c]
|
|
69
|
+
grid[r, c] = TRANSPARENT if ch == "." else int(ch, 16)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def draw(screen):
|
|
73
|
+
screen.cls(1) # dark-blue background = grid lines
|
|
74
|
+
# canvas
|
|
75
|
+
for r in range(GRID):
|
|
76
|
+
for c in range(GRID):
|
|
77
|
+
x = CANVAS_X + c * CELL
|
|
78
|
+
y = CANVAS_Y + r * CELL
|
|
79
|
+
v = grid[r, c]
|
|
80
|
+
if v != TRANSPARENT:
|
|
81
|
+
col = v
|
|
82
|
+
elif show_onion and onion is not None and onion[r, c] != TRANSPARENT:
|
|
83
|
+
col = 2 # ghost of the onion frame (dark purple)
|
|
84
|
+
else:
|
|
85
|
+
col = 6 if (r + c) % 2 == 0 else 5 # empty = checkerboard
|
|
86
|
+
screen.rectfill(x, y, x + CELL - 2, y + CELL - 2, col)
|
|
87
|
+
# palette strip
|
|
88
|
+
for i in range(16):
|
|
89
|
+
x = PAL_X + i * SW
|
|
90
|
+
screen.rectfill(x, PAL_Y, x + SW - 2, PAL_Y + SW - 2, i)
|
|
91
|
+
sx = PAL_X + selected * SW
|
|
92
|
+
screen.rect(sx - 1, PAL_Y - 1, sx + SW - 1, PAL_Y + SW - 1, 7)
|
|
93
|
+
# 1x live preview (actual size)
|
|
94
|
+
for r in range(GRID):
|
|
95
|
+
for c in range(GRID):
|
|
96
|
+
if grid[r, c] != TRANSPARENT:
|
|
97
|
+
screen.pset(PV_X + c, PV_Y + r, grid[r, c])
|
|
98
|
+
screen.rect(PV_X - 1, PV_Y - 1, PV_X + GRID, PV_Y + GRID, 6)
|
|
99
|
+
screen.text("C:" + str(selected), 104, 26, 7)
|
|
100
|
+
screen.text("ON" if show_onion else "OFF", 104, 34, 7)
|
|
101
|
+
# help
|
|
102
|
+
screen.text("E:CODE S:SAVE L:LOAD", 4, 114, 7)
|
|
103
|
+
screen.text("Z:UNDO C:CLR O/H:ONION", 4, 120, 7)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main():
|
|
107
|
+
global selected, onion, show_onion
|
|
108
|
+
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
|
|
109
|
+
window = sdl2.SDL_CreateWindow(
|
|
110
|
+
b"Olympus Sprite Editor",
|
|
111
|
+
sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED,
|
|
112
|
+
VW * SCALE, VH * SCALE, sdl2.SDL_WINDOW_SHOWN)
|
|
113
|
+
renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.SDL_RENDERER_ACCELERATED)
|
|
114
|
+
sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"0")
|
|
115
|
+
texture = sdl2.SDL_CreateTexture(
|
|
116
|
+
renderer, sdl2.SDL_PIXELFORMAT_RGB24,
|
|
117
|
+
sdl2.SDL_TEXTUREACCESS_STREAMING, VW, VH)
|
|
118
|
+
|
|
119
|
+
screen = Screen(VW, VH)
|
|
120
|
+
event = sdl2.SDL_Event()
|
|
121
|
+
mxp, myp = ctypes.c_int(0), ctypes.c_int(0)
|
|
122
|
+
prev_held = False
|
|
123
|
+
running = True
|
|
124
|
+
|
|
125
|
+
while running:
|
|
126
|
+
while sdl2.SDL_PollEvent(ctypes.byref(event)):
|
|
127
|
+
if event.type == sdl2.SDL_QUIT:
|
|
128
|
+
running = False
|
|
129
|
+
elif event.type == sdl2.SDL_KEYDOWN:
|
|
130
|
+
k = event.key.keysym.sym
|
|
131
|
+
if k == sdl2.SDLK_ESCAPE:
|
|
132
|
+
running = False
|
|
133
|
+
elif k == sdl2.SDLK_c:
|
|
134
|
+
push_undo(); grid[:, :] = TRANSPARENT
|
|
135
|
+
elif k == sdl2.SDLK_e:
|
|
136
|
+
code = to_code()
|
|
137
|
+
print("\n" + code + "\n")
|
|
138
|
+
with open("sprite_out.txt", "w") as f:
|
|
139
|
+
f.write(code + "\n")
|
|
140
|
+
elif k == sdl2.SDLK_s:
|
|
141
|
+
save_grid()
|
|
142
|
+
elif k == sdl2.SDLK_l:
|
|
143
|
+
load_grid()
|
|
144
|
+
elif k == sdl2.SDLK_z:
|
|
145
|
+
if undo_stack:
|
|
146
|
+
grid[:, :] = undo_stack.pop()
|
|
147
|
+
elif k == sdl2.SDLK_o:
|
|
148
|
+
onion = grid.copy(); show_onion = True
|
|
149
|
+
elif k == sdl2.SDLK_h:
|
|
150
|
+
show_onion = not show_onion
|
|
151
|
+
|
|
152
|
+
buttons = sdl2.SDL_GetMouseState(ctypes.byref(mxp), ctypes.byref(myp))
|
|
153
|
+
mx, my = mxp.value // SCALE, myp.value // SCALE
|
|
154
|
+
left = buttons & sdl2.SDL_BUTTON_LMASK
|
|
155
|
+
right = buttons & sdl2.SDL_BUTTON_RMASK
|
|
156
|
+
held = bool(left or right)
|
|
157
|
+
|
|
158
|
+
# snapshot once at the start of each stroke that begins on the canvas
|
|
159
|
+
if held and not prev_held and in_canvas(mx, my):
|
|
160
|
+
push_undo()
|
|
161
|
+
prev_held = held
|
|
162
|
+
|
|
163
|
+
if left:
|
|
164
|
+
if in_canvas(mx, my):
|
|
165
|
+
r, c = canvas_cell(mx, my)
|
|
166
|
+
grid[r, c] = selected
|
|
167
|
+
else:
|
|
168
|
+
pi = palette_index(mx, my)
|
|
169
|
+
if pi is not None:
|
|
170
|
+
selected = pi
|
|
171
|
+
if right and in_canvas(mx, my):
|
|
172
|
+
r, c = canvas_cell(mx, my)
|
|
173
|
+
grid[r, c] = TRANSPARENT
|
|
174
|
+
|
|
175
|
+
draw(screen)
|
|
176
|
+
rgb = screen.to_rgb()
|
|
177
|
+
sdl2.SDL_UpdateTexture(texture, None, ctypes.c_void_p(rgb.ctypes.data), VW *
|
|
178
|
+
3)
|
|
179
|
+
sdl2.SDL_RenderClear(renderer)
|
|
180
|
+
sdl2.SDL_RenderCopy(renderer, texture, None, None)
|
|
181
|
+
sdl2.SDL_RenderPresent(renderer)
|
|
182
|
+
sdl2.SDL_Delay(16)
|
|
183
|
+
|
|
184
|
+
sdl2.SDL_DestroyTexture(texture)
|
|
185
|
+
sdl2.SDL_DestroyRenderer(renderer)
|
|
186
|
+
sdl2.SDL_DestroyWindow(window)
|
|
187
|
+
sdl2.SDL_Quit()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
olympus/font.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
FONT = {
|
|
2
|
+
'A': ["XXX", "X.X", "XXX", "X.X", "X.X"],
|
|
3
|
+
'B': ["XX.", "X.X", "XX.", "X.X", "XX."],
|
|
4
|
+
'C': ["XXX", "X..", "X..", "X..", "XXX"],
|
|
5
|
+
'D': ["XX.", "X.X", "X.X", "X.X", "XX."],
|
|
6
|
+
'E': ["XXX", "X..", "XX.", "X..", "XXX"],
|
|
7
|
+
'F': ["XXX", "X..", "XX.", "X..", "X.."],
|
|
8
|
+
'G': ["XXX", "X..", "X.X", "X.X", "XXX"],
|
|
9
|
+
'H': ["X.X", "X.X", "XXX", "X.X", "X.X"],
|
|
10
|
+
'I': ["XXX", ".X.", ".X.", ".X.", "XXX"],
|
|
11
|
+
'J': ["..X", "..X", "..X", "X.X", "XXX"],
|
|
12
|
+
'K': ["X.X", "X.X", "XX.", "X.X", "X.X"],
|
|
13
|
+
'L': ["X..", "X..", "X..", "X..", "XXX"],
|
|
14
|
+
'M': ["X.X", "XXX", "XXX", "X.X", "X.X"],
|
|
15
|
+
'N': ["X.X", "XXX", "XXX", "XXX", "X.X"],
|
|
16
|
+
'O': ["XXX", "X.X", "X.X", "X.X", "XXX"],
|
|
17
|
+
'P': ["XXX", "X.X", "XXX", "X..", "X.."],
|
|
18
|
+
'Q': ["XXX", "X.X", "X.X", "XXX", "..X"],
|
|
19
|
+
'R': ["XX.", "X.X", "XX.", "X.X", "X.X"],
|
|
20
|
+
'S': ["XXX", "X..", "XXX", "..X", "XXX"],
|
|
21
|
+
'T': ["XXX", ".X.", ".X.", ".X.", ".X."],
|
|
22
|
+
'U': ["X.X", "X.X", "X.X", "X.X", "XXX"],
|
|
23
|
+
'V': ["X.X", "X.X", "X.X", "X.X", ".X."],
|
|
24
|
+
'W': ["X.X", "X.X", "XXX", "XXX", "X.X"],
|
|
25
|
+
'X': ["X.X", "X.X", ".X.", "X.X", "X.X"],
|
|
26
|
+
'Y': ["X.X", "X.X", ".X.", ".X.", ".X."],
|
|
27
|
+
'Z': ["XXX", "..X", ".X.", "X..", "XXX"],
|
|
28
|
+
'0': ["XXX", "X.X", "X.X", "X.X", "XXX"],
|
|
29
|
+
'1': [".X.", "XX.", ".X.", ".X.", "XXX"],
|
|
30
|
+
'2': ["XXX", "..X", "XXX", "X..", "XXX"],
|
|
31
|
+
'3': ["XXX", "..X", "XXX", "..X", "XXX"],
|
|
32
|
+
'4': ["X.X", "X.X", "XXX", "..X", "..X"],
|
|
33
|
+
'5': ["XXX", "X..", "XXX", "..X", "XXX"],
|
|
34
|
+
'6': ["XXX", "X..", "XXX", "X.X", "XXX"],
|
|
35
|
+
'7': ["XXX", "..X", "..X", "..X", "..X"],
|
|
36
|
+
'8': ["XXX", "X.X", "XXX", "X.X", "XXX"],
|
|
37
|
+
'9': ["XXX", "X.X", "XXX", "..X", "XXX"],
|
|
38
|
+
'.': ["...", "...", "...", "...", ".X."],
|
|
39
|
+
':': ["...", ".X.", "...", ".X.", "..."],
|
|
40
|
+
'!': [".X.", ".X.", ".X.", "...", ".X."],
|
|
41
|
+
'-': ["...", "...", "XXX", "...", "..."],
|
|
42
|
+
' ': ["...", "...", "...", "...", "..."],
|
|
43
|
+
}
|
olympus/palette.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
PALETTE = np.array([
|
|
4
|
+
(0, 0, 0), # 0 black
|
|
5
|
+
(29, 43, 83), # 1 dark-blue
|
|
6
|
+
(126, 37, 83), # 2 dark-purple
|
|
7
|
+
(0, 135, 81), # 3 dark-green
|
|
8
|
+
(171, 82, 54), # 4 brown
|
|
9
|
+
(95, 87, 79), # 5 dark-grey
|
|
10
|
+
(194, 195, 199), # 6 light-grey
|
|
11
|
+
(255, 241, 232), # 7 white
|
|
12
|
+
(255, 0, 77), # 8 red
|
|
13
|
+
(255, 163, 0), # 9 orange
|
|
14
|
+
(255, 236, 39), # 10 yellow
|
|
15
|
+
(0, 228, 54), # 11 green
|
|
16
|
+
(41, 173, 255), # 12 blue
|
|
17
|
+
(131, 118, 156), # 13 indigo
|
|
18
|
+
(255, 119, 168), # 14 pink
|
|
19
|
+
(255, 204, 170), # 15 peach
|
|
20
|
+
], dtype=np.uint8)
|
|
21
|
+
|
|
22
|
+
# friendly names, so you can write screen.pset(x, y, RED)
|
|
23
|
+
BLACK, DARKBLUE, DARKPURPLE, DARKGREEN = 0, 1, 2, 3
|
|
24
|
+
BROWN, DARKGREY, LIGHTGREY, WHITE = 4, 5, 6, 7
|
|
25
|
+
RED, ORANGE, YELLOW, GREEN = 8, 9, 10, 11
|
|
26
|
+
BLUE, INDIGO, PINK, PEACH = 12, 13, 14, 15
|
olympus/screen.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .palette import PALETTE
|
|
3
|
+
import ctypes, sdl2
|
|
4
|
+
from .font import FONT
|
|
5
|
+
|
|
6
|
+
LEFT, RIGHT, UP, DOWN, O, X = 0, 1, 2, 3, 4, 5
|
|
7
|
+
|
|
8
|
+
_SCANCODES = [
|
|
9
|
+
sdl2.SDL_SCANCODE_LEFT,
|
|
10
|
+
sdl2.SDL_SCANCODE_RIGHT,
|
|
11
|
+
sdl2.SDL_SCANCODE_UP,
|
|
12
|
+
sdl2.SDL_SCANCODE_DOWN,
|
|
13
|
+
sdl2.SDL_SCANCODE_Z,
|
|
14
|
+
sdl2.SDL_SCANCODE_X,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
class Screen:
|
|
18
|
+
|
|
19
|
+
def __init__(self, w, h):
|
|
20
|
+
self.w = w
|
|
21
|
+
self.h = h
|
|
22
|
+
self.fb = np.zeros((h, w), dtype=np.uint8)
|
|
23
|
+
self._btn = [False] * 6
|
|
24
|
+
self._prev = [False] * 6
|
|
25
|
+
|
|
26
|
+
def cls(self, color):
|
|
27
|
+
self.fb[:, :] = color
|
|
28
|
+
|
|
29
|
+
def pset(self, x, y, color):
|
|
30
|
+
if 0 <= x < self.w and 0 <= y < self.h:
|
|
31
|
+
self.fb[y, x] = color
|
|
32
|
+
|
|
33
|
+
def line(self, x0, y0, x1, y1, color):
|
|
34
|
+
x0, y0, x1, y1 = int(x0), int(y0), int(x1), int(y1)
|
|
35
|
+
dx = abs(x1 - x0)
|
|
36
|
+
dy = -abs(y1 - y0)
|
|
37
|
+
sx = 1 if x0 < x1 else -1
|
|
38
|
+
sy = 1 if y0 < y1 else -1
|
|
39
|
+
err = dx + dy
|
|
40
|
+
while True:
|
|
41
|
+
self.pset(x0, y0, color)
|
|
42
|
+
if x0 == x1 and y0 == y1:
|
|
43
|
+
break
|
|
44
|
+
e2 = 2 * err
|
|
45
|
+
if e2 >= dy:
|
|
46
|
+
err += dy
|
|
47
|
+
x0 += sx
|
|
48
|
+
if e2 <= dx:
|
|
49
|
+
err += dx
|
|
50
|
+
y0 += sy
|
|
51
|
+
|
|
52
|
+
def rectfill(self, x0, y0, x1, y1, color):
|
|
53
|
+
x0, x1 = sorted((int(x0), int(x1)))
|
|
54
|
+
y0, y1 = sorted((int(y0), int(y1)))
|
|
55
|
+
x0 = max(0, x0)
|
|
56
|
+
y0 = max(0, y0)
|
|
57
|
+
x1 = min(self.w - 1, x1)
|
|
58
|
+
y1 = min(self.h - 1, y1)
|
|
59
|
+
if x0 <= x1 and y0 <= y1:
|
|
60
|
+
self.fb[y0:y1 + 1, x0:x1 + 1] = color
|
|
61
|
+
|
|
62
|
+
def rect(self, x0, y0, x1, y1, color):
|
|
63
|
+
self.line(x0, y0, x1, y0, color)
|
|
64
|
+
self.line(x0, y1, x1, y1, color)
|
|
65
|
+
self.line(x0, y0, x0, y1, color)
|
|
66
|
+
self.line(x1, y0, x1, y1, color)
|
|
67
|
+
|
|
68
|
+
def circ(self, cx, cy, r, color):
|
|
69
|
+
cx, cy, r = int(cx), int(cy), int(r)
|
|
70
|
+
if r < 0:
|
|
71
|
+
return
|
|
72
|
+
x = r
|
|
73
|
+
y = 0
|
|
74
|
+
err = 1 - r
|
|
75
|
+
while x >= y:
|
|
76
|
+
self.pset(cx + x, cy + y, color)
|
|
77
|
+
self.pset(cx + y, cy + x, color)
|
|
78
|
+
self.pset(cx - y, cy + x, color)
|
|
79
|
+
self.pset(cx - x, cy + y, color)
|
|
80
|
+
self.pset(cx - x, cy - y, color)
|
|
81
|
+
self.pset(cx - y, cy - x, color)
|
|
82
|
+
self.pset(cx + y, cy - x, color)
|
|
83
|
+
self.pset(cx + x, cy - y, color)
|
|
84
|
+
y += 1
|
|
85
|
+
if err < 0:
|
|
86
|
+
err += 2 * y + 1
|
|
87
|
+
else:
|
|
88
|
+
x -= 1
|
|
89
|
+
err += 2 * (y - x) + 1
|
|
90
|
+
|
|
91
|
+
def circfill(self, cx, cy, r, color):
|
|
92
|
+
cx, cy, r = int(cx), int(cy), int(r)
|
|
93
|
+
if r < 0:
|
|
94
|
+
return
|
|
95
|
+
x = r
|
|
96
|
+
y = 0
|
|
97
|
+
err = 1 - r
|
|
98
|
+
while x >= y:
|
|
99
|
+
# instead of plotting 8 edge points, draw horizontal spans between them
|
|
100
|
+
self.rectfill(cx - x, cy + y, cx + x, cy + y, color)
|
|
101
|
+
self.rectfill(cx - x, cy - y, cx + x, cy - y, color)
|
|
102
|
+
self.rectfill(cx - y, cy + x, cx + y, cy + x, color)
|
|
103
|
+
self.rectfill(cx - y, cy - x, cx + y, cy - x, color)
|
|
104
|
+
y += 1
|
|
105
|
+
if err < 0:
|
|
106
|
+
err += 2 * y + 1
|
|
107
|
+
else:
|
|
108
|
+
x -= 1
|
|
109
|
+
err += 2 * (y - x) + 1
|
|
110
|
+
|
|
111
|
+
def to_rgb(self):
|
|
112
|
+
return np.ascontiguousarray(PALETTE[self.fb])
|
|
113
|
+
|
|
114
|
+
def btn(self, i):
|
|
115
|
+
return self._btn[i]
|
|
116
|
+
|
|
117
|
+
def btnp(self, i):
|
|
118
|
+
return self._btn[i] and not self._prev[i]
|
|
119
|
+
|
|
120
|
+
def spr(self, sprite, x, y, flip_x=False, flip_y=False):
|
|
121
|
+
s = sprite
|
|
122
|
+
if flip_x:
|
|
123
|
+
s = s[:, ::-1]
|
|
124
|
+
if flip_y:
|
|
125
|
+
s = s[::-1, :]
|
|
126
|
+
sh, sw = s.shape
|
|
127
|
+
x, y = int(x), int(y)
|
|
128
|
+
|
|
129
|
+
sx0 = max(0, -x)
|
|
130
|
+
sy0 = max(0, -y)
|
|
131
|
+
sx1 = min(sw, self.w - x)
|
|
132
|
+
sy1 = min(sh, self.h - y)
|
|
133
|
+
if sx0 >= sx1 or sy0 >= sy1:
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
sub = s[sy0:sy1, sx0:sx1]
|
|
137
|
+
dst = self.fb[y + sy0:y + sy1, x + sx0:x + sx1]
|
|
138
|
+
mask = sub != 255
|
|
139
|
+
dst[mask] = sub[mask]
|
|
140
|
+
|
|
141
|
+
def text(self, s, x, y, color):
|
|
142
|
+
cx = x
|
|
143
|
+
for ch in str(s).upper():
|
|
144
|
+
glyph = FONT.get(ch)
|
|
145
|
+
if glyph is not None:
|
|
146
|
+
for j in range(5):
|
|
147
|
+
row = glyph[j]
|
|
148
|
+
for i in range(3):
|
|
149
|
+
if row[i] != '.':
|
|
150
|
+
self.pset(cx + i, y + j, color)
|
|
151
|
+
cx += 4
|
|
152
|
+
return cx
|
|
153
|
+
|
|
154
|
+
def run(self, update, draw, scale=5, title=b"Olympus"):
|
|
155
|
+
sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO)
|
|
156
|
+
win_w, win_h = self.w * scale, self.h * scale
|
|
157
|
+
|
|
158
|
+
window = sdl2.SDL_CreateWindow(
|
|
159
|
+
title,
|
|
160
|
+
sdl2.SDL_WINDOWPOS_CENTERED, sdl2.SDL_WINDOWPOS_CENTERED,
|
|
161
|
+
win_w, win_h,
|
|
162
|
+
sdl2.SDL_WINDOW_SHOWN)
|
|
163
|
+
|
|
164
|
+
renderer = sdl2.SDL_CreateRenderer(window, -1, sdl2.SDL_RENDERER_ACCELERATED)
|
|
165
|
+
sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"0")
|
|
166
|
+
texture = sdl2.SDL_CreateTexture(
|
|
167
|
+
renderer,
|
|
168
|
+
sdl2.SDL_PIXELFORMAT_RGB24,
|
|
169
|
+
sdl2.SDL_TEXTUREACCESS_STREAMING,
|
|
170
|
+
self.w, self.h)
|
|
171
|
+
|
|
172
|
+
event = sdl2.SDL_Event()
|
|
173
|
+
running = True
|
|
174
|
+
while running:
|
|
175
|
+
while sdl2.SDL_PollEvent(ctypes.byref(event)):
|
|
176
|
+
if event.type == sdl2.SDL_QUIT:
|
|
177
|
+
running = False
|
|
178
|
+
elif event.type == sdl2.SDL_KEYDOWN and \
|
|
179
|
+
event.key.keysym.sym == sdl2.SDLK_ESCAPE:
|
|
180
|
+
running = False
|
|
181
|
+
|
|
182
|
+
keystate = sdl2.SDL_GetKeyboardState(None)
|
|
183
|
+
self._prev = self._btn
|
|
184
|
+
self._btn = [bool(keystate[sc]) for sc in _SCANCODES]
|
|
185
|
+
|
|
186
|
+
update()
|
|
187
|
+
draw()
|
|
188
|
+
|
|
189
|
+
rgb = self.to_rgb()
|
|
190
|
+
sdl2.SDL_UpdateTexture(texture, None, ctypes.c_void_p(rgb.ctypes.data), self.w * 3)
|
|
191
|
+
sdl2.SDL_RenderClear(renderer)
|
|
192
|
+
sdl2.SDL_RenderCopy(renderer, texture, None, None)
|
|
193
|
+
sdl2.SDL_RenderPresent(renderer)
|
|
194
|
+
sdl2.SDL_Delay(16)
|
|
195
|
+
|
|
196
|
+
sdl2.SDL_DestroyTexture(texture)
|
|
197
|
+
sdl2.SDL_DestroyRenderer(renderer)
|
|
198
|
+
sdl2.SDL_DestroyWindow(window)
|
|
199
|
+
sdl2.SDL_Quit()
|
olympus/sound.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import wave
|
|
3
|
+
import numpy as np
|
|
4
|
+
import sdl2
|
|
5
|
+
|
|
6
|
+
SAMPLE_RATE = 22050 # audio samples per second
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_wav(path, rate=SAMPLE_RATE):
|
|
10
|
+
"""Load a .wav file and convert it to the engine's format
|
|
11
|
+
(mono, 16-bit, SAMPLE_RATE). Returns an int16 numpy array."""
|
|
12
|
+
wv = wave.open(path, "rb")
|
|
13
|
+
nch = wv.getnchannels()
|
|
14
|
+
width = wv.getsampwidth()
|
|
15
|
+
fr = wv.getframerate()
|
|
16
|
+
raw = wv.readframes(wv.getnframes())
|
|
17
|
+
wv.close()
|
|
18
|
+
|
|
19
|
+
# raw bytes -> float samples in -1..1, whatever the bit depth was
|
|
20
|
+
if width == 1:
|
|
21
|
+
a = (np.frombuffer(raw, np.uint8).astype(np.float32) - 128) / 128.0
|
|
22
|
+
elif width == 2:
|
|
23
|
+
a = np.frombuffer(raw, np.int16).astype(np.float32) / 32768.0
|
|
24
|
+
elif width == 4:
|
|
25
|
+
a = np.frombuffer(raw, np.int32).astype(np.float32) / 2147483648.0
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError("unsupported WAV sample width: %d bytes" % width)
|
|
28
|
+
|
|
29
|
+
if nch > 1: # stereo -> mono: average the channels
|
|
30
|
+
a = a.reshape(-1, nch).mean(axis=1)
|
|
31
|
+
|
|
32
|
+
if fr != rate: # resample to our rate
|
|
33
|
+
m = len(a)
|
|
34
|
+
new = max(1, int(m * rate / fr))
|
|
35
|
+
a = np.interp(np.linspace(0, 1, new), np.linspace(0, 1, m), a)
|
|
36
|
+
|
|
37
|
+
return (a * 32767).astype(np.int16)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Sound:
|
|
41
|
+
def __init__(self):
|
|
42
|
+
sdl2.SDL_InitSubSystem(sdl2.SDL_INIT_AUDIO)
|
|
43
|
+
spec = sdl2.SDL_AudioSpec(SAMPLE_RATE, sdl2.AUDIO_S16SYS, 1, 1024)
|
|
44
|
+
self.dev = sdl2.SDL_OpenAudioDevice(None, 0, spec, None, 0)
|
|
45
|
+
if self.dev:
|
|
46
|
+
sdl2.SDL_PauseAudioDevice(self.dev, 0)
|
|
47
|
+
|
|
48
|
+
def play(self, freq, ms, volume=0.3):
|
|
49
|
+
"""Synthesize and play a square-wave beep."""
|
|
50
|
+
if not self.dev:
|
|
51
|
+
return
|
|
52
|
+
n = int(SAMPLE_RATE * ms / 1000)
|
|
53
|
+
t = np.arange(n)
|
|
54
|
+
phase = (freq * t / SAMPLE_RATE) % 1.0
|
|
55
|
+
square = np.where(phase < 0.5, 1.0, -1.0)
|
|
56
|
+
fade = max(1, int(SAMPLE_RATE * 0.005))
|
|
57
|
+
if n > 2 * fade:
|
|
58
|
+
square[:fade] *= np.linspace(0, 1, fade)
|
|
59
|
+
square[-fade:] *= np.linspace(1, 0, fade)
|
|
60
|
+
data = (square * volume * 32767).astype(np.int16)
|
|
61
|
+
sdl2.SDL_QueueAudio(self.dev, data.ctypes.data_as(ctypes.c_void_p), data.nbytes)
|
|
62
|
+
|
|
63
|
+
def play_clip(self, clip, volume=1.0):
|
|
64
|
+
"""Play a clip loaded with load_wav()."""
|
|
65
|
+
if not self.dev or clip is None:
|
|
66
|
+
return
|
|
67
|
+
if volume != 1.0:
|
|
68
|
+
clip = (clip.astype(np.float32) * volume).astype(np.int16)
|
|
69
|
+
sdl2.SDL_QueueAudio(self.dev, clip.ctypes.data_as(ctypes.c_void_p), clip.nbytes)
|
olympus/sprites.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
from PIL import Image
|
|
4
|
+
from .palette import PALETTE
|
|
5
|
+
|
|
6
|
+
TRANSPARENT = 255
|
|
7
|
+
|
|
8
|
+
def make_sprite(rows):
|
|
9
|
+
h = len(rows)
|
|
10
|
+
w = len(rows[0])
|
|
11
|
+
s = np.full((h, w), TRANSPARENT, dtype=np.uint8)
|
|
12
|
+
for j, row in enumerate(rows):
|
|
13
|
+
for i, ch in enumerate(row):
|
|
14
|
+
if ch != '.':
|
|
15
|
+
s[j, i] = int(ch, 16)
|
|
16
|
+
return s
|
|
17
|
+
|
|
18
|
+
def load_sprite(path):
|
|
19
|
+
img = Image.open(path).convert("RGBA")
|
|
20
|
+
arr = np.array(img)
|
|
21
|
+
rgb = arr[:, :, :3].astype(np.int32)
|
|
22
|
+
alpha = arr[:, :, 3]
|
|
23
|
+
|
|
24
|
+
pal = PALETTE.astype(np.int32)
|
|
25
|
+
|
|
26
|
+
diff = rgb[:, :, None, :] - pal[None, None, :, :]
|
|
27
|
+
dist = (diff * diff).sum(axis=3)
|
|
28
|
+
idx = dist.argmin(axis=2).astype(np.uint8)
|
|
29
|
+
|
|
30
|
+
idx[alpha < 128] = TRANSPARENT
|
|
31
|
+
return idx
|
olympus/tilemap.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from .sprites import make_sprite
|
|
2
|
+
|
|
3
|
+
TILE = 8
|
|
4
|
+
|
|
5
|
+
WALL = make_sprite([
|
|
6
|
+
"55555555",
|
|
7
|
+
"56666665",
|
|
8
|
+
"56666665",
|
|
9
|
+
"56666665",
|
|
10
|
+
"56666665",
|
|
11
|
+
"56666665",
|
|
12
|
+
"56666665",
|
|
13
|
+
"55555555",
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
COIN = make_sprite([
|
|
17
|
+
"........",
|
|
18
|
+
"..aaaa..",
|
|
19
|
+
".aaaaaa.",
|
|
20
|
+
".aaaaaa.",
|
|
21
|
+
".aaaaaa.",
|
|
22
|
+
".aaaaaa.",
|
|
23
|
+
"..aaaa..",
|
|
24
|
+
"........",
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
TILES = {
|
|
28
|
+
'W': WALL,
|
|
29
|
+
'C': COIN,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SOLID = {'W'}
|
|
33
|
+
|
|
34
|
+
class Tilemap:
|
|
35
|
+
def __init__(self, rows, tiles, solid):
|
|
36
|
+
self.rows = rows
|
|
37
|
+
self.tiles = tiles
|
|
38
|
+
self.solid = solid
|
|
39
|
+
self.h = len(rows)
|
|
40
|
+
self.w = len(rows[0])
|
|
41
|
+
|
|
42
|
+
def get(self, col, row):
|
|
43
|
+
if 0 <= row < self.h and 0 <= col < self.w:
|
|
44
|
+
return self.rows[row][col]
|
|
45
|
+
|
|
46
|
+
def set(self, col, row, ch):
|
|
47
|
+
line = self.rows[row]
|
|
48
|
+
self.rows[row] = line[:col] + ch + line[col + 1:]
|
|
49
|
+
|
|
50
|
+
def draw(self, screen):
|
|
51
|
+
for row in range(self.h):
|
|
52
|
+
for col in range(self.w):
|
|
53
|
+
sprite = self.tiles.get(self.rows[row][col])
|
|
54
|
+
if sprite is not None:
|
|
55
|
+
screen.spr(sprite, col * TILE, row * TILE)
|
|
56
|
+
|
|
57
|
+
def solid_at(self, x, y):
|
|
58
|
+
return self.get(x // TILE, y // TILE) in self.solid
|
|
59
|
+
|
|
60
|
+
def box_hits_solid(self, x, y, w, h):
|
|
61
|
+
return (self.solid_at(x, y) or
|
|
62
|
+
self.solid_at(x + w - 1, y) or
|
|
63
|
+
self.solid_at(x, y + h - 1) or
|
|
64
|
+
self.solid_at(x + w - 1, y + h - 1))
|
|
65
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: olympus-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A from-scratch 2D game engine for Python
|
|
5
|
+
Author: Nithil Gadde
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nithilgadde/Olympus
|
|
8
|
+
Project-URL: Repository, https://github.com/nithilgadde/Olympus
|
|
9
|
+
Project-URL: Issues, https://github.com/nithilgadde/Olympus/issues
|
|
10
|
+
Keywords: game-engine,fantasy-console,pico-8,pixel-art,gamedev,retro,2d,sdl2
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Topic :: Games/Entertainment
|
|
14
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Operating System :: OS Independent
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: pysdl2
|
|
29
|
+
Requires-Dist: pysdl2-dll
|
|
30
|
+
Requires-Dist: pillow
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Olympus
|
|
34
|
+
|
|
35
|
+
A from-scratch 2D fantasy-console game engine for Python. Build tiny pixel games on a 128×128 screen with a fixed 16-color palette and a few lines of code.
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
## Try it
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install olympus-engine
|
|
43
|
+
olympus-editor # draw sprites with the mouse, export to code
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then make a tiny game in about 15 lines — see **Quick start** below.
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
Make a game in about 15 lines:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import olympus as ol
|
|
54
|
+
|
|
55
|
+
screen = ol.Screen(128, 128)
|
|
56
|
+
x, y = 60, 60
|
|
57
|
+
|
|
58
|
+
def update():
|
|
59
|
+
global x, y
|
|
60
|
+
if screen.btn(ol.LEFT): x -= 1
|
|
61
|
+
if screen.btn(ol.RIGHT): x += 1
|
|
62
|
+
if screen.btn(ol.UP): y -= 1
|
|
63
|
+
if screen.btn(ol.DOWN): y += 1
|
|
64
|
+
|
|
65
|
+
def draw():
|
|
66
|
+
screen.cls(ol.BLACK)
|
|
67
|
+
screen.rectfill(x, y, x + 7, y + 7, ol.RED)
|
|
68
|
+
screen.text("HELLO OLYMPUS", 18, 40, ol.WHITE)
|
|
69
|
+
|
|
70
|
+
screen.run(update, draw)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You write `update` (game logic) and `draw` (pixels); the engine runs them 60 times a second and handles the window, input, and rendering.
|
|
74
|
+
|
|
75
|
+
## Features
|
|
76
|
+
|
|
77
|
+
- **128×128 indexed framebuffer** with a fixed 16-color palette — the classic fantasy-console look.
|
|
78
|
+
- **Hand-written rasterizers**: pixels, lines (Bresenham), rectangles, and circles (midpoint), outlined or filled.
|
|
79
|
+
- **Sprites** you can hand-draw as text or load from a PNG — loaded images are automatically quantized down to the 16-color palette.
|
|
80
|
+
- **Animation, tilemaps, and AABB collision** — cycle frames for walk cycles, build levels as grids of characters, and check what's solid.
|
|
81
|
+
- **A built-in pixel font** for scores, labels, and menus.
|
|
82
|
+
- **Sound**: square-wave synthesis for retro blips, plus loading and playing your own `.wav` files.
|
|
83
|
+
- **A built-in sprite editor** (`olympus-editor`) — draw with the mouse and export straight to engine code, with undo, save/load, and onion-skinning.
|
|
84
|
+
|
|
85
|
+
## Running locally
|
|
86
|
+
|
|
87
|
+
Requirements:
|
|
88
|
+
|
|
89
|
+
- **Python 3.9+**
|
|
90
|
+
- No system libraries to install by hand — `pysdl2-dll` ships the SDL2 binaries, and `pip` installs everything else (numpy, PySDL2, Pillow) automatically.
|
|
91
|
+
|
|
92
|
+
From the project root (the folder with `pyproject.toml`):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
python -m venv venv
|
|
96
|
+
source venv/bin/activate # Windows: venv\Scripts\activate
|
|
97
|
+
pip install -e .
|
|
98
|
+
|
|
99
|
+
python examples/demo.py # run the demo game
|
|
100
|
+
olympus-editor # open the sprite editor
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
Olympus is "from scratch" in a specific, deliberate way: **no graphics or game library does the drawing.** The screen is a NumPy array of palette *indices* (not RGB), and every shape is rasterized by hand — Bresenham lines, midpoint circles, a sprite blitter, a pixel font, and square-wave audio synthesized sample by sample. SDL2 is only used to open a window, present the finished pixel buffer, and read input. It is **not** a wrapper around pygame.
|
|
106
|
+
|
|
107
|
+
A couple of decisions worth calling out:
|
|
108
|
+
|
|
109
|
+
- **The framebuffer stores color indices, not RGB.** Drawing just writes small integers (0–15) into a 2D array, which is fast and keeps the palette constraint baked into the design. The conversion to real RGB happens once per frame, with a single NumPy lookup, right before handing the buffer to SDL.
|
|
110
|
+
- **Loading a PNG means quantizing it.** Because the framebuffer is indexed, imported images can't be copied pixel-for-pixel — every pixel is snapped to its nearest palette color in a vectorized NumPy pass. That preserves the console's look no matter what art you throw at it.
|
|
111
|
+
|
|
112
|
+
## Credits
|
|
113
|
+
|
|
114
|
+
Built with [PySDL2](https://github.com/py-sdl/py-sdl2) (windowing, input, audio output), [NumPy](https://numpy.org/) (the framebuffer and rasterization), and [Pillow](https://python-pillow.org/) (PNG decoding). Inspired by [PICO-8](https://www.lexaloffle.com/pico-8.php) and the fantasy-console idea.
|
|
115
|
+
|
|
116
|
+
Created by Nithil Gadde · MIT License
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
olympus/__init__.py,sha256=eozMHDlDWspmZdlDud9OJOOSMB3_T3Oq44O80k_ZgyU,843
|
|
2
|
+
olympus/anim.py,sha256=4xKqzwiRjXs5AyqpFV_SqeHitxMYTwOwzbYbSjtQQzc,417
|
|
3
|
+
olympus/collide.py,sha256=xI3lQ82jsF08H6qKcXs36n4Tyv43-79AY6jZXpohIQo,258
|
|
4
|
+
olympus/editor.py,sha256=h7HI4bzl2G4pFf39E7DKCQYFfNvFhZDX0KGyEjRoquw,6387
|
|
5
|
+
olympus/font.py,sha256=D5OOXiEumNPclHow6YLl0_rMhuXIStfNuMTgScTmUfw,1980
|
|
6
|
+
olympus/palette.py,sha256=avdRvIa5cPZUX5mIaK2Tu1bs9CkoefcaV1Ww1e6fOZg,857
|
|
7
|
+
olympus/screen.py,sha256=aJLYjFnJO3gO-p4Sj2yU87V6yqd5hG4BdO1Us7fiz9s,6109
|
|
8
|
+
olympus/sound.py,sha256=HpZawQOXul9gWxYtF04Vk85HtQHCHZshnUT6dRMBPWE,2619
|
|
9
|
+
olympus/sprites.py,sha256=8rxr269xqwEob4HOargLLD1TMJA48VNp7x40IqB2iSw,747
|
|
10
|
+
olympus/tilemap.py,sha256=BKzWFBQNBCUFcmSKYsXM9vfCqxu408TceDXyYyxOP5M,1446
|
|
11
|
+
olympus_engine-0.1.0.dist-info/licenses/LICENSE,sha256=af-qcTTYnbK2yszBGf_6FQxObp3Q8J0WdYxiAxoMS78,1069
|
|
12
|
+
olympus_engine-0.1.0.dist-info/METADATA,sha256=XcBU31veRS5iYuSo4H8V3h65gE0dNJR2G2KS7EOWfdY,5095
|
|
13
|
+
olympus_engine-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
14
|
+
olympus_engine-0.1.0.dist-info/entry_points.txt,sha256=Q9--T-GPzHlFI6xaVbycF3zSSci2EH2oFVuEMjh9iYQ,55
|
|
15
|
+
olympus_engine-0.1.0.dist-info/top_level.txt,sha256=vO7aGDG7Or1P6gE-RGLFbywAeAl1kP4mgVXYkmD2lG0,8
|
|
16
|
+
olympus_engine-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nithil Gadde
|
|
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
|
+
olympus
|