kaypy 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.
Files changed (55) hide show
  1. kaplay/__init__.py +192 -0
  2. kaplay/assets.py +127 -0
  3. kaplay/callutil.py +54 -0
  4. kaplay/camera.py +44 -0
  5. kaplay/cli.py +86 -0
  6. kaplay/comps/__init__.py +0 -0
  7. kaplay/comps/area.py +24 -0
  8. kaplay/comps/body.py +50 -0
  9. kaplay/comps/move.py +67 -0
  10. kaplay/comps/pos.py +52 -0
  11. kaplay/comps/shapes.py +96 -0
  12. kaplay/comps/sprite.py +86 -0
  13. kaplay/comps/state.py +66 -0
  14. kaplay/comps/transform.py +104 -0
  15. kaplay/debugmod.py +10 -0
  16. kaplay/engine.py +311 -0
  17. kaplay/events.py +128 -0
  18. kaplay/gameobj.py +192 -0
  19. kaplay/geometry.py +53 -0
  20. kaplay/kaboom.py +42 -0
  21. kaplay/level.py +44 -0
  22. kaplay/physics.py +208 -0
  23. kaplay/render.py +119 -0
  24. kaplay/starter/CREDITS.md +134 -0
  25. kaplay/starter/dungeon/elf_m.png +0 -0
  26. kaplay/starter/dungeon.png +0 -0
  27. kaplay/starter/game.py +81 -0
  28. kaplay/starter/images/bean.png +0 -0
  29. kaplay/starter/images/coin.png +0 -0
  30. kaplay/starter/images/dino_0.png +0 -0
  31. kaplay/starter/images/dino_1.png +0 -0
  32. kaplay/starter/images/dino_2.png +0 -0
  33. kaplay/starter/images/dino_3.png +0 -0
  34. kaplay/starter/images/dino_4.png +0 -0
  35. kaplay/starter/images/dino_5.png +0 -0
  36. kaplay/starter/images/dino_6.png +0 -0
  37. kaplay/starter/images/dino_7.png +0 -0
  38. kaplay/starter/images/dino_8.png +0 -0
  39. kaplay/starter/images/ghosty.png +0 -0
  40. kaplay/starter/images/grass.png +0 -0
  41. kaplay/starter/images/portal.png +0 -0
  42. kaplay/starter/images/spike.png +0 -0
  43. kaplay/starter/images/steel.png +0 -0
  44. kaplay/starter/sounds/background.wav +0 -0
  45. kaplay/starter/sounds/ding.wav +0 -0
  46. kaplay/starter/sounds/screech.wav +0 -0
  47. kaplay/timers.py +57 -0
  48. kaplay/vec2.py +114 -0
  49. kaplay/webbuild.py +361 -0
  50. kaypy-0.1.0.dist-info/METADATA +437 -0
  51. kaypy-0.1.0.dist-info/RECORD +55 -0
  52. kaypy-0.1.0.dist-info/WHEEL +5 -0
  53. kaypy-0.1.0.dist-info/entry_points.txt +2 -0
  54. kaypy-0.1.0.dist-info/licenses/LICENSE +33 -0
  55. kaypy-0.1.0.dist-info/top_level.txt +1 -0
kaplay/__init__.py ADDED
@@ -0,0 +1,192 @@
1
+ """from kaplay import * — every name here is Kaplay's own name, in
2
+ Kaplay's own order, with Kaplay's own arguments. The Kaplay documentation
3
+ and every Kaplay example on the internet applies to what you write here;
4
+ only the language underneath (real, native Python, via pygame-ce) is new.
5
+ """
6
+ from .vec2 import vec2, Vec2
7
+ from .engine import Engine, current_engine, rand, randi, choose
8
+ from .debugmod import debug
9
+ from .level import addLevel, Level
10
+ from .kaboom import addKaboom
11
+
12
+ # ---- components ---------------------------------------------------------
13
+ from .comps.pos import pos
14
+ from .comps.sprite import sprite
15
+ from .comps.shapes import rect, circle, text
16
+ from .comps.area import area
17
+ from .comps.body import body
18
+ from .comps.transform import anchor, scale, color, opacity, outline, z, fixed
19
+ from .comps.state import state
20
+ from .comps.move import move, offscreen, tile
21
+ from .callutil import register_or_decorate
22
+
23
+ __all__ = [
24
+ "kaplay",
25
+ "loadSprite", "loadSpriteAtlas", "loadSound",
26
+ "setGravity", "setBackground",
27
+ "add", "get", "addLevel", "addKaboom",
28
+ "pos", "sprite", "rect", "circle", "text", "area", "body",
29
+ "anchor", "scale", "color", "opacity", "outline", "z", "fixed",
30
+ "move", "offscreen", "tile", "state",
31
+ "onUpdate", "onKeyDown", "onKeyPress", "onKeyRelease", "onClick",
32
+ "wait", "loop",
33
+ "scene", "go",
34
+ "width", "height", "center", "dt", "vec2",
35
+ "rand", "randi", "choose",
36
+ "mousePos", "toWorld",
37
+ "setCamPos", "setCamScale", "shake",
38
+ "play",
39
+ "debug",
40
+ "run",
41
+ ]
42
+
43
+
44
+ def kaplay(width=800, height=600, background=(0, 0, 0)):
45
+ """Starts the engine. It has to come first, before anything else."""
46
+ return Engine(width=width, height=height, background=background)
47
+
48
+
49
+ # ---- loading -------------------------------------------------------------
50
+
51
+ def loadSprite(name, path, sliceX=1, sliceY=1, anims=None):
52
+ return current_engine().assets.loadSprite(name, path, sliceX=sliceX, sliceY=sliceY, anims=anims)
53
+
54
+
55
+ def loadSpriteAtlas(path, atlas):
56
+ return current_engine().assets.loadSpriteAtlas(path, atlas)
57
+
58
+
59
+ def loadSound(name, path):
60
+ return current_engine().assets.loadSound(name, path)
61
+
62
+
63
+ def setGravity(n):
64
+ current_engine().setGravity(n)
65
+
66
+
67
+ def setBackground(r, g, b):
68
+ current_engine().setBackground(r, g, b)
69
+
70
+
71
+ # ---- making things --------------------------------------------------------
72
+
73
+ def add(comp_list):
74
+ return current_engine().add(comp_list)
75
+
76
+
77
+ def get(tag):
78
+ return current_engine().get(tag)
79
+
80
+
81
+ # ---- events ----------------------------------------------------------------
82
+
83
+ def onUpdate(*args):
84
+ """onUpdate(fn) / onUpdate(tag, fn), or as a decorator:
85
+
86
+ @onUpdate # every frame
87
+ @onUpdate("enemy") # every frame, once per tagged object
88
+ """
89
+ if len(args) == 2:
90
+ tag, fn = args
91
+ return register_or_decorate(fn, lambda f: current_engine().events.on_update(tag, f))
92
+ if len(args) == 1 and callable(args[0]):
93
+ return current_engine().events.on_update(args[0])
94
+ if len(args) == 1:
95
+ tag = args[0]
96
+ return register_or_decorate(None, lambda f: current_engine().events.on_update(tag, f))
97
+ raise TypeError("onUpdate() takes a function, or a tag and a function")
98
+
99
+
100
+ def onKeyDown(key, fn=None):
101
+ return register_or_decorate(fn, lambda f: current_engine().events.on_key_down(key, f))
102
+
103
+
104
+ def onKeyPress(key, fn=None):
105
+ return register_or_decorate(fn, lambda f: current_engine().events.on_key_press(key, f))
106
+
107
+
108
+ def onKeyRelease(key, fn=None):
109
+ return register_or_decorate(fn, lambda f: current_engine().events.on_key_release(key, f))
110
+
111
+
112
+ def onClick(fn=None):
113
+ return register_or_decorate(fn, lambda f: current_engine().events.on_click(f))
114
+
115
+
116
+ def wait(seconds, fn=None):
117
+ return register_or_decorate(fn, lambda f: current_engine().timers.wait(seconds, f))
118
+
119
+
120
+ def loop(seconds, fn=None):
121
+ return register_or_decorate(fn, lambda f: current_engine().timers.loop(seconds, f))
122
+
123
+
124
+ # ---- scenes -----------------------------------------------------------------
125
+
126
+ def scene(name, fn=None):
127
+ """scene(name, fn), or as a decorator:
128
+
129
+ @scene("game")
130
+ def build_game(): ...
131
+ """
132
+ def register(f):
133
+ current_engine().scene(name, f)
134
+ return f
135
+ return register_or_decorate(fn, register)
136
+
137
+
138
+ def go(name, *args):
139
+ current_engine().go(name, *args)
140
+
141
+
142
+ # ---- useful values ------------------------------------------------------
143
+
144
+ def width():
145
+ return current_engine().width()
146
+
147
+
148
+ def height():
149
+ return current_engine().height()
150
+
151
+
152
+ def center():
153
+ return current_engine().center()
154
+
155
+
156
+ def dt():
157
+ return current_engine().dt()
158
+
159
+
160
+ def mousePos():
161
+ return current_engine().mousePos()
162
+
163
+
164
+ def toWorld(pos_):
165
+ return current_engine().toWorld(pos_)
166
+
167
+
168
+ def setCamPos(pos_):
169
+ current_engine().setCamPos(pos_)
170
+
171
+
172
+ def setCamScale(n):
173
+ current_engine().setCamScale(n)
174
+
175
+
176
+ def shake(n=8):
177
+ current_engine().shake(n)
178
+
179
+
180
+ def play(name, loop=False, paused=False, volume=1.0):
181
+ from .engine import SoundHandle
182
+ engine = current_engine()
183
+ if name not in engine.assets.sounds:
184
+ raise KeyError(f"no sound called {name!r} — loadSound(\"{name}\", ...) first")
185
+ return SoundHandle(engine.assets.sounds[name], loop=loop, paused=paused, volume=volume)
186
+
187
+
188
+ def run():
189
+ """Not part of the Kaplay API — the loop starts on its own once your
190
+ script finishes. Exposed only so 'kaypy run game.py'-style tooling
191
+ (or a script that wants to be explicit) has something to call."""
192
+ current_engine().run()
kaplay/assets.py ADDED
@@ -0,0 +1,127 @@
1
+ """Sprite / sound loading: loadSprite, loadSpriteAtlas, loadSound.
2
+
3
+ Kaplay's own troubleshooting note applies here too: "the sprite is
4
+ invisible but the game runs" almost always means a bad path, and
5
+ "every frame is half one pose and half the next" means sliceX doesn't
6
+ match the sheet. We raise clear errors for the first and let the second
7
+ happen silently (cutting a rectangle of nothing without complaint),
8
+ exactly as Kaplay's own guide describes.
9
+ """
10
+ from __future__ import annotations
11
+ import os
12
+ import sys
13
+ import pygame
14
+
15
+
16
+ class SpriteAsset:
17
+ def __init__(self, frames, anims=None, frame_size=None):
18
+ self.frames = frames # list[pygame.Surface]
19
+ self.anims = anims or {} # name -> {"from","to","speed","loop"} or int
20
+ self.frame_size = frame_size or (frames[0].get_width(), frames[0].get_height())
21
+
22
+
23
+ def _load_image(path):
24
+ if not os.path.isfile(path):
25
+ raise FileNotFoundError(
26
+ f"loadSprite/loadSpriteAtlas: no such file {path!r} — "
27
+ f"check the path (the two sprite packs live in separate "
28
+ f"folders; a dungeon sprite is not under images/)"
29
+ )
30
+ return pygame.image.load(path).convert_alpha()
31
+
32
+
33
+ def _slice_surface(surface, slice_x, slice_y):
34
+ w = surface.get_width() // slice_x
35
+ h = surface.get_height() // slice_y
36
+ frames = []
37
+ for row in range(slice_y):
38
+ for col in range(slice_x):
39
+ rect = pygame.Rect(col * w, row * h, w, h)
40
+ frame = pygame.Surface((w, h), pygame.SRCALPHA)
41
+ frame.blit(surface, (0, 0), rect)
42
+ frames.append(frame)
43
+ return frames, (w, h)
44
+
45
+
46
+ def _normalize_anims(anims):
47
+ out = {}
48
+ for name, spec in (anims or {}).items():
49
+ if isinstance(spec, int):
50
+ out[name] = {"from": spec, "to": spec, "speed": 1, "loop": False}
51
+ else:
52
+ out[name] = {
53
+ "from": spec.get("from", 0),
54
+ "to": spec.get("to", 0),
55
+ "speed": spec.get("speed", 10),
56
+ "loop": spec.get("loop", False),
57
+ }
58
+ return out
59
+
60
+
61
+ class AssetManager:
62
+ def __init__(self):
63
+ self.sprites: dict[str, SpriteAsset] = {}
64
+ self.sounds: dict[str, "pygame.mixer.Sound"] = {}
65
+ self._atlas_cache: dict[str, "pygame.Surface"] = {}
66
+
67
+ def loadSprite(self, name, path, sliceX=1, sliceY=1, anims=None):
68
+ if isinstance(path, (list, tuple)):
69
+ frames = [_load_image(p) for p in path]
70
+ frame_size = (frames[0].get_width(), frames[0].get_height())
71
+ else:
72
+ surface = _load_image(path)
73
+ if sliceX > 1 or sliceY > 1:
74
+ frames, frame_size = _slice_surface(surface, sliceX, sliceY)
75
+ else:
76
+ frames = [surface]
77
+ frame_size = (surface.get_width(), surface.get_height())
78
+ self.sprites[name] = SpriteAsset(frames, _normalize_anims(anims), frame_size)
79
+ return self.sprites[name]
80
+
81
+ def loadSpriteAtlas(self, path, atlas):
82
+ if path not in self._atlas_cache:
83
+ self._atlas_cache[path] = _load_image(path)
84
+ sheet = self._atlas_cache[path]
85
+ for name, spec in atlas.items():
86
+ x, y = spec["x"], spec["y"]
87
+ w, h = spec["width"], spec["height"]
88
+ region = pygame.Surface((w, h), pygame.SRCALPHA)
89
+ region.blit(sheet, (0, 0), pygame.Rect(x, y, w, h))
90
+ slice_x = spec.get("sliceX", 1)
91
+ slice_y = spec.get("sliceY", 1)
92
+ if slice_x > 1 or slice_y > 1:
93
+ frames, frame_size = _slice_surface(region, slice_x, slice_y)
94
+ else:
95
+ frames = [region]
96
+ frame_size = (w, h)
97
+ self.sprites[name] = SpriteAsset(
98
+ frames, _normalize_anims(spec.get("anims")), frame_size
99
+ )
100
+ return self.sprites
101
+
102
+ def loadSound(self, name, path):
103
+ if sys.platform == "emscripten":
104
+ # Browsers' WASM SDL2-mixer build can't reliably decode most
105
+ # .wav/.mp3/.aiff encodings — pygbag's own --build step flags
106
+ # this (see tools/build_web.py, which converts each .wav it
107
+ # copies into a same-named .ogg via ffmpeg when available). If
108
+ # that sibling .ogg exists, prefer it silently, so scripts never
109
+ # need an if-web branch just to pick a sound file extension.
110
+ ogg_path = os.path.splitext(path)[0] + ".ogg"
111
+ if os.path.isfile(ogg_path):
112
+ path = ogg_path
113
+ if not os.path.isfile(path):
114
+ raise FileNotFoundError(f"loadSound: no such file {path!r}")
115
+ try:
116
+ self.sounds[name] = pygame.mixer.Sound(path)
117
+ except pygame.error:
118
+ self.sounds[name] = None # audio device unavailable (headless/CI)
119
+ return self.sounds[name]
120
+
121
+ def get_sprite(self, name) -> SpriteAsset:
122
+ if name not in self.sprites:
123
+ raise KeyError(
124
+ f"no sprite called {name!r} — loadSprite(\"{name}\", ...) "
125
+ f"must run before it is used"
126
+ )
127
+ return self.sprites[name]
kaplay/callutil.py ADDED
@@ -0,0 +1,54 @@
1
+ """Kaplay callbacks can ignore arguments they don't want: onKeyDown hands
2
+ the key that was pressed, but `lambda: ...` takes nothing and works
3
+ anyway, exactly as `() => ...` does in JavaScript. If you *do* want the
4
+ argument, `lambda key: ...` gets it."""
5
+ import inspect
6
+
7
+ _sig_cache = {}
8
+
9
+
10
+ def register_or_decorate(fn, register):
11
+ """Let one event function serve as both a callback and a decorator.
12
+
13
+ onKeyPress("space", jump) # hand it a function
14
+ @onKeyPress("space") # or put it above one
15
+ def jump(): ...
16
+
17
+ Kaplay's own form is the first one, and it stays the form every
18
+ Kaplay example and doc page translates into directly. The second
19
+ exists because a lambda can only hold one expression, and working
20
+ around that teaches bad habits: `lambda: p.jump() if p.isGrounded()
21
+ else None` has an `else None` that does nothing and exists purely to
22
+ satisfy a conditional expression, and `lambda: setattr(p, "pos", v)`
23
+ reaches for setattr only because a lambda can't assign. With a
24
+ decorator, both are just ordinary indented code — and the handler
25
+ gets a name, so it shows up in tracebacks as `jump` rather than
26
+ `<lambda>`.
27
+
28
+ `register` does the actual work and returns the function; this only
29
+ decides whether to call it now or hand back something that will.
30
+ """
31
+ if fn is not None:
32
+ return register(fn)
33
+
34
+ def decorator(f):
35
+ register(f)
36
+ return f # leave the name bound to the function itself
37
+
38
+ return decorator
39
+
40
+
41
+ def call_flexible(fn, *args):
42
+ n = _sig_cache.get(fn)
43
+ if n is None:
44
+ try:
45
+ params = inspect.signature(fn).parameters.values()
46
+ has_varargs = any(p.kind == p.VAR_POSITIONAL for p in params)
47
+ n = len(args) if has_varargs else sum(
48
+ 1 for p in params
49
+ if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
50
+ )
51
+ except (TypeError, ValueError):
52
+ n = len(args)
53
+ _sig_cache[fn] = n
54
+ return fn(*args[:n])
kaplay/camera.py ADDED
@@ -0,0 +1,44 @@
1
+ from .vec2 import Vec2
2
+
3
+
4
+ class Camera:
5
+ def __init__(self, width, height):
6
+ self.width = width
7
+ self.height = height
8
+ self.pos = Vec2(width / 2, height / 2)
9
+ self.scale = 1.0
10
+ self.shake_amount = 0.0
11
+ self._frame_shake_offset = Vec2(0, 0)
12
+
13
+ def setPos(self, pos):
14
+ self.pos = Vec2(pos.x, pos.y)
15
+
16
+ def setScale(self, n):
17
+ self.scale = n
18
+
19
+ def shake(self, amount=8):
20
+ self.shake_amount = amount
21
+
22
+ def begin_frame(self):
23
+ """Called once per frame by the render system: decays shake and
24
+ freezes this frame's random offset so every object drawn this
25
+ frame shakes together."""
26
+ if self.shake_amount <= 0:
27
+ self._frame_shake_offset = Vec2(0, 0)
28
+ return
29
+ import random
30
+ a = self.shake_amount
31
+ self._frame_shake_offset = Vec2(random.uniform(-a, a), random.uniform(-a, a))
32
+ self.shake_amount *= 0.9
33
+ if self.shake_amount < 0.2:
34
+ self.shake_amount = 0
35
+
36
+ def world_to_screen(self, world_pos: Vec2) -> Vec2:
37
+ center = Vec2(self.width / 2, self.height / 2)
38
+ return (world_pos - self.pos) * self.scale + center + self._frame_shake_offset
39
+
40
+ def screen_to_world(self, screen_pos) -> Vec2:
41
+ if not isinstance(screen_pos, Vec2):
42
+ screen_pos = Vec2(screen_pos[0], screen_pos[1])
43
+ center = Vec2(self.width / 2, self.height / 2)
44
+ return (screen_pos - center) / self.scale + self.pos
kaplay/cli.py ADDED
@@ -0,0 +1,86 @@
1
+ """The `kaypy` command.
2
+
3
+ kaypy new mygame make a folder with a working game and its assets
4
+ kaypy web game.py build that game as a web page and serve it
5
+
6
+ Both exist because a pip install has no repo to stand in. `kaypy new` is
7
+ what gets a student from `pip install kaypy` to something moving on
8
+ screen without hunting for sprite files, and `kaypy web` is the same
9
+ build tool the checkout calls `webbuild.py`.
10
+ """
11
+ from __future__ import annotations
12
+ import argparse
13
+ import shutil
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ STARTER = Path(__file__).resolve().parent / "starter"
18
+
19
+
20
+ def cmd_new(args) -> int:
21
+ dest = Path(args.name).resolve()
22
+ if dest.exists() and any(dest.iterdir()):
23
+ print(f"{dest} already exists and isn't empty — pick another name, "
24
+ f"or delete it first.")
25
+ return 1
26
+ if not STARTER.is_dir():
27
+ print("this kaypy install has no starter files in it, which shouldn't "
28
+ "happen — reinstall, or copy a game from the project's examples/.")
29
+ return 1
30
+
31
+ shutil.copytree(
32
+ STARTER, dest, dirs_exist_ok=True,
33
+ ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
34
+ )
35
+
36
+ here = Path.cwd()
37
+ try:
38
+ shown = dest.relative_to(here)
39
+ except ValueError:
40
+ shown = dest
41
+
42
+ print(f"Created {shown}/ — a game, and the sprites and sounds to build on.\n")
43
+ print("Try it:\n")
44
+ print(f" cd {shown}")
45
+ print(" python game.py\n")
46
+ print("Arrow keys to move, space to jump. Then open game.py and change it.")
47
+ print(f"To put it on the web instead: kaypy web game.py")
48
+ return 0
49
+
50
+
51
+ def cmd_web(args) -> int:
52
+ from . import webbuild
53
+ # webbuild parses its own arguments, so hand it the ones meant for it.
54
+ sys.argv = ["kaypy web"] + args.rest
55
+ webbuild.main()
56
+ return 0
57
+
58
+
59
+ def main(argv=None) -> int:
60
+ parser = argparse.ArgumentParser(
61
+ prog="kaypy",
62
+ description="kaypy — a real-Python game engine with Kaplay's API.",
63
+ )
64
+ sub = parser.add_subparsers(dest="command")
65
+
66
+ p_new = sub.add_parser("new", help="create a new game folder, ready to run")
67
+ p_new.add_argument("name", help="folder to create, e.g. mygame")
68
+ p_new.set_defaults(func=cmd_new)
69
+
70
+ p_web = sub.add_parser(
71
+ "web", help="build a game as a web page and serve it",
72
+ add_help=False,
73
+ )
74
+ p_web.add_argument("rest", nargs=argparse.REMAINDER,
75
+ help="the game file, plus any webbuild options")
76
+ p_web.set_defaults(func=cmd_web)
77
+
78
+ args = parser.parse_args(argv)
79
+ if not getattr(args, "command", None):
80
+ parser.print_help()
81
+ return 1
82
+ return args.func(args)
83
+
84
+
85
+ if __name__ == "__main__":
86
+ raise SystemExit(main())
File without changes
kaplay/comps/area.py ADDED
@@ -0,0 +1,24 @@
1
+ """area(): the collision box. Both objects in a collision need one —
2
+ an object without area() is invisible to collision, and that's the
3
+ single most common reason a collision "doesn't work"."""
4
+ from ..gameobj import Comp
5
+
6
+
7
+ class AreaComp(Comp):
8
+ id = "area"
9
+
10
+ def add(self, obj):
11
+ self._obj = obj
12
+
13
+ def get_rect(self):
14
+ from ..geometry import get_world_rect
15
+ return get_world_rect(self._obj)
16
+
17
+ def isHovering(self) -> bool:
18
+ from ..engine import current_engine
19
+ mx, my = current_engine().mousePos()
20
+ return self.get_rect().collidepoint(mx, my)
21
+
22
+
23
+ def area():
24
+ return AreaComp()
kaplay/comps/body.py ADDED
@@ -0,0 +1,50 @@
1
+ """body(): makes an object respond to physics. isStatic=True makes it a
2
+ wall — solid, and never moved by anything. mass=n makes it heavy but
3
+ still pushable. Gives .jump(), .isGrounded(), .onGround()."""
4
+ from ..gameobj import Comp
5
+ from ..vec2 import Vec2
6
+
7
+
8
+ class BodyComp(Comp):
9
+ id = "body"
10
+
11
+ def __init__(self, isStatic=False, mass=1, jumpForce=800):
12
+ self.isStatic = isStatic
13
+ self.mass = mass
14
+ self.jumpForce = jumpForce
15
+ self.vel = Vec2(0, 0)
16
+ self._grounded = False
17
+ self._pending_grounded = False
18
+ self._ground_handlers = []
19
+
20
+ def add(self, obj):
21
+ self._obj = obj
22
+
23
+ def jump(self, force=None):
24
+ if force is None:
25
+ force = self.jumpForce
26
+ self.vel.y = -force
27
+ self._grounded = False
28
+
29
+ def isGrounded(self) -> bool:
30
+ return self._grounded
31
+
32
+ def onGround(self, fn=None):
33
+ from ..callutil import register_or_decorate
34
+
35
+ def register(f):
36
+ self._ground_handlers.append(f)
37
+ return f
38
+
39
+ return register_or_decorate(fn, register)
40
+
41
+ def _apply_grounded(self, value: bool):
42
+ was = self._grounded
43
+ self._grounded = value
44
+ if value and not was:
45
+ for fn in self._ground_handlers:
46
+ fn()
47
+
48
+
49
+ def body(isStatic=False, mass=1, jumpForce=800):
50
+ return BodyComp(isStatic=isStatic, mass=mass, jumpForce=jumpForce)
kaplay/comps/move.py ADDED
@@ -0,0 +1,67 @@
1
+ """move(dir, speed): drifts an object in a straight line forever.
2
+ offscreen(destroy=True): cleans it up once it leaves the screen.
3
+ tile(isObstacle=): marks an object as occupying a square on a grid."""
4
+ from ..gameobj import Comp
5
+ from ..vec2 import vec2, Vec2
6
+
7
+
8
+ class MoveComp(Comp):
9
+ id = "move"
10
+
11
+ def __init__(self, direction, speed):
12
+ if isinstance(direction, Vec2):
13
+ self.direction = direction.unit()
14
+ else:
15
+ self.direction = vec2(*direction).unit()
16
+ self.speed = speed
17
+
18
+ def add(self, obj):
19
+ self._obj = obj
20
+
21
+ def update(self, obj):
22
+ if obj.has("pos"):
23
+ obj.comp("pos").move(self.direction * self.speed)
24
+
25
+
26
+ def move(direction, speed):
27
+ return MoveComp(direction, speed)
28
+
29
+
30
+ class OffscreenComp(Comp):
31
+ id = "offscreen"
32
+
33
+ def __init__(self, destroy=False, distance=64):
34
+ self.destroy = destroy
35
+ self.distance = distance
36
+
37
+ def add(self, obj):
38
+ self._obj = obj
39
+
40
+ def update(self, obj):
41
+ from ..engine import current_engine
42
+ from ..geometry import get_world_pos
43
+ engine = current_engine()
44
+ p = get_world_pos(obj)
45
+ d = self.distance
46
+ if (p.x < -d or p.x > engine.width() + d or
47
+ p.y < -d or p.y > engine.height() + d):
48
+ if self.destroy:
49
+ obj.destroy()
50
+
51
+
52
+ def offscreen(destroy=False, distance=64):
53
+ return OffscreenComp(destroy=destroy, distance=distance)
54
+
55
+
56
+ class TileComp(Comp):
57
+ id = "tile"
58
+
59
+ def __init__(self, isObstacle=False):
60
+ self.isObstacle = isObstacle
61
+
62
+ def add(self, obj):
63
+ self._obj = obj
64
+
65
+
66
+ def tile(isObstacle=False):
67
+ return TileComp(isObstacle=isObstacle)