├── docs
├── .gitignore
├── samples
│ ├── maze
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── beep.mp3
│ │ │ ├── font.png
│ │ │ └── goal.mp3
│ │ └── index.html
│ ├── pong
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── beep.mp3
│ │ │ └── font.png
│ │ └── index.html
│ ├── racing
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── font.png
│ │ │ ├── music.mp3
│ │ │ ├── plunge.mp3
│ │ │ └── sprites.png
│ │ └── index.html
│ ├── adventure
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── font.png
│ │ │ ├── scene1.png
│ │ │ ├── scene2.png
│ │ │ └── scene3.png
│ │ └── index.html
│ ├── pseudo3d
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── bomb.mp3
│ │ │ ├── font.png
│ │ │ ├── jump.mp3
│ │ │ ├── pick.mp3
│ │ │ ├── sprites.png
│ │ │ └── tiles.png
│ │ └── index.html
│ ├── scramble
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── bomb.mp3
│ │ │ ├── font.png
│ │ │ ├── pew.mp3
│ │ │ ├── sprites.png
│ │ │ └── explosion.mp3
│ │ └── index.html
│ ├── shooter
│ │ ├── assets
│ │ │ ├── pew.mp3
│ │ │ ├── font.png
│ │ │ ├── sprites.png
│ │ │ └── explosion.mp3
│ │ ├── gameplay.gif
│ │ └── index.html
│ ├── platformer
│ │ ├── gameplay.gif
│ │ ├── assets
│ │ │ ├── font.png
│ │ │ ├── jump.mp3
│ │ │ ├── pick.mp3
│ │ │ ├── tiles.png
│ │ │ ├── ending.mp3
│ │ │ └── sprites.png
│ │ └── index.html
│ └── index.html
├── index.html
├── Makefile
├── userguide.html
└── cheatsheet.html
├── skel
├── .gitignore
├── assets
│ ├── .gitignore
│ ├── beep.mp3
│ ├── font.png
│ ├── sprites.png
│ └── Makefile
├── tsconfig.json
├── Makefile
├── embed.html
├── index.html
└── src
│ └── game.ts
├── samples
├── pong
│ ├── .gitignore
│ ├── assets
│ │ ├── .gitignore
│ │ ├── beep.mp3
│ │ ├── font.png
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── shooter
│ ├── .gitignore
│ ├── assets
│ │ ├── .gitignore
│ │ ├── pew.mp3
│ │ ├── font.png
│ │ ├── sprites.png
│ │ ├── explosion.mp3
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── platformer
│ ├── .gitignore
│ ├── assets
│ │ ├── .gitignore
│ │ ├── font.png
│ │ ├── jump.mp3
│ │ ├── pick.mp3
│ │ ├── tiles.png
│ │ ├── ending.mp3
│ │ ├── sprites.png
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── pseudo3d
│ ├── .gitignore
│ ├── assets
│ │ ├── .gitignore
│ │ ├── bomb.mp3
│ │ ├── font.png
│ │ ├── jump.mp3
│ │ ├── pick.mp3
│ │ ├── sprites.png
│ │ ├── tiles.png
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ └── index.html
├── maze
│ ├── assets
│ │ ├── beep.mp3
│ │ ├── font.png
│ │ ├── goal.mp3
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── racing
│ ├── assets
│ │ ├── font.png
│ │ ├── music.mp3
│ │ ├── plunge.mp3
│ │ ├── sprites.png
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── scramble
│ ├── assets
│ │ ├── bomb.mp3
│ │ ├── font.png
│ │ ├── pew.mp3
│ │ ├── sprites.png
│ │ ├── explosion.mp3
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── adventure
│ ├── assets
│ │ ├── font.png
│ │ ├── scene1.png
│ │ ├── scene2.png
│ │ ├── scene3.png
│ │ └── Makefile
│ ├── tsconfig.json
│ ├── Makefile
│ ├── index.html
│ └── src
│ │ └── game.ts
├── Makefile
└── index.html
├── Makefile
├── tools
├── doit.py
├── mktiles.py
├── setup.sh
└── watcher.py
├── LICENSE
├── base
├── animation.ts
├── entity.ts
├── task.ts
├── scene.ts
├── tilemap.ts
├── sprite.ts
└── pathfind.ts
└── README.md
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | api
2 |
--------------------------------------------------------------------------------
/skel/.gitignore:
--------------------------------------------------------------------------------
1 | js
2 |
--------------------------------------------------------------------------------
/samples/pong/.gitignore:
--------------------------------------------------------------------------------
1 | js
2 |
--------------------------------------------------------------------------------
/samples/shooter/.gitignore:
--------------------------------------------------------------------------------
1 | js
2 |
--------------------------------------------------------------------------------
/skel/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *.wav
2 |
--------------------------------------------------------------------------------
/samples/platformer/.gitignore:
--------------------------------------------------------------------------------
1 | js
2 |
--------------------------------------------------------------------------------
/samples/pseudo3d/.gitignore:
--------------------------------------------------------------------------------
1 | js
2 |
--------------------------------------------------------------------------------
/samples/pong/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *.wav
2 |
--------------------------------------------------------------------------------
/samples/shooter/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *.wav
2 |
--------------------------------------------------------------------------------
/samples/platformer/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *.wav
2 |
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *.wav
2 |
--------------------------------------------------------------------------------
/skel/assets/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/skel/assets/beep.mp3
--------------------------------------------------------------------------------
/skel/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/skel/assets/font.png
--------------------------------------------------------------------------------
/skel/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/skel/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/maze/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/maze/gameplay.gif
--------------------------------------------------------------------------------
/docs/samples/pong/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pong/gameplay.gif
--------------------------------------------------------------------------------
/samples/maze/assets/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/maze/assets/beep.mp3
--------------------------------------------------------------------------------
/samples/maze/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/maze/assets/font.png
--------------------------------------------------------------------------------
/samples/maze/assets/goal.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/maze/assets/goal.mp3
--------------------------------------------------------------------------------
/samples/pong/assets/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pong/assets/beep.mp3
--------------------------------------------------------------------------------
/samples/pong/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pong/assets/font.png
--------------------------------------------------------------------------------
/samples/racing/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/racing/assets/font.png
--------------------------------------------------------------------------------
/samples/shooter/assets/pew.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/shooter/assets/pew.mp3
--------------------------------------------------------------------------------
/docs/samples/racing/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/racing/gameplay.gif
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/bomb.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/bomb.mp3
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/font.png
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/jump.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/jump.mp3
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/pick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/pick.mp3
--------------------------------------------------------------------------------
/samples/racing/assets/music.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/racing/assets/music.mp3
--------------------------------------------------------------------------------
/samples/racing/assets/plunge.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/racing/assets/plunge.mp3
--------------------------------------------------------------------------------
/samples/scramble/assets/bomb.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/scramble/assets/bomb.mp3
--------------------------------------------------------------------------------
/samples/scramble/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/scramble/assets/font.png
--------------------------------------------------------------------------------
/samples/scramble/assets/pew.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/scramble/assets/pew.mp3
--------------------------------------------------------------------------------
/samples/shooter/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/shooter/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/adventure/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/adventure/gameplay.gif
--------------------------------------------------------------------------------
/docs/samples/maze/assets/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/maze/assets/beep.mp3
--------------------------------------------------------------------------------
/docs/samples/maze/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/maze/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/maze/assets/goal.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/maze/assets/goal.mp3
--------------------------------------------------------------------------------
/docs/samples/pong/assets/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pong/assets/beep.mp3
--------------------------------------------------------------------------------
/docs/samples/pong/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pong/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/gameplay.gif
--------------------------------------------------------------------------------
/docs/samples/racing/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/racing/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/scramble/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/gameplay.gif
--------------------------------------------------------------------------------
/docs/samples/shooter/assets/pew.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/shooter/assets/pew.mp3
--------------------------------------------------------------------------------
/docs/samples/shooter/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/shooter/gameplay.gif
--------------------------------------------------------------------------------
/samples/adventure/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/adventure/assets/font.png
--------------------------------------------------------------------------------
/samples/adventure/assets/scene1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/adventure/assets/scene1.png
--------------------------------------------------------------------------------
/samples/adventure/assets/scene2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/adventure/assets/scene2.png
--------------------------------------------------------------------------------
/samples/adventure/assets/scene3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/adventure/assets/scene3.png
--------------------------------------------------------------------------------
/samples/platformer/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/font.png
--------------------------------------------------------------------------------
/samples/platformer/assets/jump.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/jump.mp3
--------------------------------------------------------------------------------
/samples/platformer/assets/pick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/pick.mp3
--------------------------------------------------------------------------------
/samples/platformer/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/tiles.png
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/sprites.png
--------------------------------------------------------------------------------
/samples/pseudo3d/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/pseudo3d/assets/tiles.png
--------------------------------------------------------------------------------
/samples/racing/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/racing/assets/sprites.png
--------------------------------------------------------------------------------
/samples/scramble/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/scramble/assets/sprites.png
--------------------------------------------------------------------------------
/samples/shooter/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/shooter/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/platformer/gameplay.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/gameplay.gif
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/bomb.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/bomb.mp3
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/jump.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/jump.mp3
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/pick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/pick.mp3
--------------------------------------------------------------------------------
/docs/samples/racing/assets/music.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/racing/assets/music.mp3
--------------------------------------------------------------------------------
/docs/samples/racing/assets/plunge.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/racing/assets/plunge.mp3
--------------------------------------------------------------------------------
/docs/samples/scramble/assets/bomb.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/assets/bomb.mp3
--------------------------------------------------------------------------------
/docs/samples/scramble/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/scramble/assets/pew.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/assets/pew.mp3
--------------------------------------------------------------------------------
/docs/samples/shooter/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/shooter/assets/font.png
--------------------------------------------------------------------------------
/samples/platformer/assets/ending.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/ending.mp3
--------------------------------------------------------------------------------
/samples/platformer/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/platformer/assets/sprites.png
--------------------------------------------------------------------------------
/samples/scramble/assets/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/scramble/assets/explosion.mp3
--------------------------------------------------------------------------------
/samples/shooter/assets/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/samples/shooter/assets/explosion.mp3
--------------------------------------------------------------------------------
/docs/samples/adventure/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/adventure/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/adventure/assets/scene1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/adventure/assets/scene1.png
--------------------------------------------------------------------------------
/docs/samples/adventure/assets/scene2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/adventure/assets/scene2.png
--------------------------------------------------------------------------------
/docs/samples/adventure/assets/scene3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/adventure/assets/scene3.png
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/font.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/font.png
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/jump.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/jump.mp3
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/pick.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/pick.mp3
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/tiles.png
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/pseudo3d/assets/tiles.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/pseudo3d/assets/tiles.png
--------------------------------------------------------------------------------
/docs/samples/racing/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/racing/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/scramble/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/shooter/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/shooter/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/ending.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/ending.mp3
--------------------------------------------------------------------------------
/docs/samples/platformer/assets/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/platformer/assets/sprites.png
--------------------------------------------------------------------------------
/docs/samples/scramble/assets/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/scramble/assets/explosion.mp3
--------------------------------------------------------------------------------
/docs/samples/shooter/assets/explosion.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/euske/euskit/HEAD/docs/samples/shooter/assets/explosion.mp3
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile
2 |
3 | all:
4 | cd skel; $(MAKE) $@
5 | cd samples; $(MAKE) $@
6 | # -cd docs; $(MAKE) $@
7 |
8 | clean:
9 | -cd skel; $(MAKE) $@
10 | -cd samples; $(MAKE) $@
11 | # -cd docs; $(MAKE) $@
12 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
"$dst"/src/game.ts
38 |
39 | echo "done."
40 |
--------------------------------------------------------------------------------
/skel/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 |
8 | /// game.ts
9 | ///
10 |
11 |
12 | // Initialize the resources.
13 | let FONT: Font;
14 | let SPRITES:ImageSpriteSheet;
15 | function main() {
16 | APP = new App(320, 240);
17 | FONT = new Font(APP.images['font'], 'white');
18 | SPRITES = new ImageSpriteSheet(
19 | APP.images['sprites'], new Vec2(16,16), new Vec2(8,8));
20 | APP.init(new Game());
21 | }
22 |
23 |
24 | // Player
25 | //
26 | class Player extends Entity {
27 |
28 | usermove: Vec2;
29 |
30 | constructor(pos: Vec2) {
31 | super(pos);
32 | let sprite = SPRITES.get(0);
33 | this.sprites = [sprite];
34 | this.collider = sprite.getBounds();
35 | this.usermove = new Vec2();
36 | }
37 |
38 | getCollider() {
39 | return this.collider.add(this.pos);
40 | }
41 |
42 | onTick() {
43 | super.onTick();
44 | let v = this.getMove(this.usermove);
45 | this.pos = this.pos.add(v);
46 | }
47 |
48 | setMove(v: Vec2) {
49 | this.usermove = v.scale(4);
50 | }
51 | }
52 |
53 |
54 | // Game
55 | //
56 | class Game extends GameScene {
57 |
58 | player: Player;
59 | scoreBox: TextBox;
60 | score: number;
61 |
62 | onStart() {
63 | super.onStart();
64 | this.scoreBox = new TextBox(this.screen.inflate(-8,-8), FONT);
65 | this.player = new Player(this.world.area.center());
66 | this.player.fences = [this.world.area];
67 | this.add(this.player);
68 | this.score = 0;
69 | this.updateScore();
70 | }
71 |
72 | onTick() {
73 | super.onTick();
74 | }
75 |
76 | onDirChanged(v: Vec2) {
77 | this.player.setMove(v);
78 | }
79 |
80 | render(ctx: CanvasRenderingContext2D) {
81 | ctx.fillStyle = 'rgb(0,0,0)';
82 | fillRect(ctx, this.screen);
83 | super.render(ctx);
84 | this.scoreBox.render(ctx);
85 | }
86 |
87 | updateScore() {
88 | this.scoreBox.clear();
89 | this.scoreBox.putText(['SCORE: '+this.score]);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tools/watcher.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 | import time
4 | import stat
5 | import os.path
6 | import subprocess
7 |
8 | def ansi(code, s):
9 | return ('\033[%dm' % code)+s+'\033[m'
10 |
11 | class Watcher:
12 |
13 | def __init__(self, cmd, args):
14 | self.cmd = cmd
15 | paths = []
16 | for arg in args:
17 | if os.path.isdir(arg):
18 | for (root,dirs,files) in os.walk(arg):
19 | paths.extend( os.path.join(root,name) for name in files )
20 | elif os.path.isfile(arg):
21 | paths.append(arg)
22 | else:
23 | raise OSError('file not found: %r' % arg)
24 | self._lastmod = { path:0 for path in paths }
25 | return
26 |
27 | def run(self, debug=0):
28 | while True:
29 | updated = []
30 | for (path,mtime0) in self._lastmod.items():
31 | try:
32 | mtime1 = os.stat(path)[stat.ST_MTIME]
33 | if mtime0 < mtime1:
34 | self._lastmod[path] = mtime1
35 | updated.append(path)
36 | except OSError:
37 | raise
38 | if updated:
39 | if debug:
40 | print(self._lastmod, file=sys.stderr)
41 | print()
42 | print(ansi(93, '*** updated: %r' % updated))
43 | self.invoke(updated)
44 | time.sleep(1)
45 | return
46 |
47 | def invoke(self, paths):
48 | popen = subprocess.Popen(self.cmd, shell=True)
49 | status = popen.wait()
50 | if status == 0:
51 | print(ansi(92, '*** succeeded ***'))
52 | else:
53 | print(ansi(91, '*** failed (status=%r) ***' % status)+chr(7))
54 | return
55 |
56 | def main(argv):
57 | import getopt
58 | def usage():
59 | print('usage: %s [-d] [-c cmd] [path ...]' % argv[0])
60 | return 100
61 | try:
62 | (opts, args) = getopt.getopt(argv[1:], 'dc:')
63 | except getopt.GetoptError:
64 | return usage()
65 | debug = 0
66 | cmd = 'make'
67 | for (k, v) in opts:
68 | if k == '-d': debug += 1
69 | elif k == '-c': cmd = v
70 | watcher = Watcher(cmd, args)
71 | return watcher.run(debug=debug)
72 |
73 | if __name__ == '__main__': sys.exit(main(sys.argv))
74 |
--------------------------------------------------------------------------------
/base/animation.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 |
7 | // Animator
8 | // Base class for all animator.
9 | //
10 | class Animator extends Task {
11 |
12 | entity: Entity;
13 |
14 | constructor(entity: Entity) {
15 | super();
16 | this.entity = entity;
17 | }
18 | }
19 |
20 |
21 | // Blinker
22 | //
23 | class Blinker extends Entity {
24 |
25 | interval: number = 1.0;
26 | target: Entity;
27 |
28 | constructor(entity: Entity) {
29 | super(entity.pos);
30 | this.target = entity;
31 | }
32 |
33 | onTick() {
34 | super.onTick();
35 | this.pos = this.target.pos;
36 | this.sprites = this.target.sprites;
37 | }
38 |
39 | isVisible() {
40 | return (this.isRunning() &&
41 | ((this.interval <= 0) ||
42 | (phase(this.getTime(), this.interval) != 0)));
43 | }
44 | }
45 |
46 |
47 | // Tweener
48 | //
49 | class Tweener extends Animator {
50 |
51 | srcpos: Vec2 = null;
52 | dstpos: Vec2 = null;
53 |
54 | onStart() {
55 | super.onStart();
56 | this.srcpos = this.entity.pos.copy();
57 | }
58 |
59 | onTick() {
60 | super.onTick();
61 | if (this.srcpos !== null && this.dstpos !== null) {
62 | let t = this.getTime() / this.lifetime;
63 | this.entity.pos = this.getPos(t);
64 | }
65 | }
66 |
67 | getPos(t: number) {
68 | return this.srcpos.lerp(this.dstpos, t);
69 | }
70 | }
71 |
72 |
73 | // PolyTweener
74 | //
75 | class PolyTweener extends Tweener {
76 |
77 | n: number;
78 |
79 | constructor(entity: Entity, n=2) {
80 | super(entity);
81 | this.n = n;
82 | }
83 | }
84 |
85 |
86 | // PolyTweenerIn
87 | //
88 | class PolyTweenerIn extends PolyTweener {
89 |
90 | getPos(t: number) {
91 | t = Math.pow(t, this.n);
92 | return this.srcpos.lerp(this.dstpos, t);
93 | }
94 | }
95 |
96 |
97 | // PolyTweenerOut
98 | //
99 | class PolyTweenerOut extends PolyTweener {
100 |
101 | getPos(t: number) {
102 | t = 1.0 - Math.pow(1.0-t, this.n)
103 | return this.srcpos.lerp(this.dstpos, t);
104 | }
105 | }
106 |
107 |
108 | // PolyTweenerInOut
109 | //
110 | class PolyTweenerInOut extends PolyTweener {
111 |
112 | getPos(t: number) {
113 | if (t < 0.5) {
114 | t = 0.5*Math.pow(2*t, this.n); // in
115 | } else {
116 | t = 0.5*(2.0 - Math.pow(2.0-2*t, this.n)); // out
117 | }
118 | return this.srcpos.lerp(this.dstpos, t);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/samples/pong/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 |
8 | // Pong
9 | //
10 | // A very basic example of using Euskit.
11 | //
12 | // Some parts are made intentionally simplistic to
13 | // facilitate the understanding.
14 | //
15 |
16 |
17 | // Paddle
18 | //
19 | class Paddle extends Entity {
20 |
21 | bounds = new Rect(-20,-5,40,10);
22 | screen: Rect; // Screen bounds.
23 | vx: number; // Moving direction.
24 |
25 | constructor(screen: Rect) {
26 | // Initializes the position and shape.
27 | super(screen.anchor('s').move(0,-20));
28 | this.sprites = [new RectSprite('green', this.bounds)];
29 | this.collider = this.bounds;
30 | this.screen = screen;
31 | this.vx = 0;
32 | }
33 |
34 | onTick() {
35 | super.onTick();
36 | // Updates the position.
37 | let pos = this.pos.move(this.vx*4, 0);
38 | let bounds = this.bounds.add(pos);
39 | if (0 <= bounds.x && bounds.x1() <= this.screen.x1()) {
40 | this.pos = pos;
41 | }
42 | }
43 | }
44 |
45 |
46 | // Ball
47 | //
48 | class Ball extends Entity {
49 |
50 | bounds = new Rect(-5,-5,10,10);
51 | screen: Rect; // Screen bounds.
52 | v: Vec2; // Moving direction.
53 |
54 | constructor(screen: Rect) {
55 | // Initializes the position and shape.
56 | super(screen.center());
57 | this.sprites = [new OvalSprite('white', this.bounds)];
58 | this.collider = this.bounds;
59 | this.screen = screen;
60 | this.v = new Vec2(rnd(2)*8-4, -4);
61 | }
62 |
63 | onTick() {
64 | super.onTick();
65 | // Updates the position.
66 | let pos = this.pos.add(this.v);
67 | let bounds = this.bounds.add(pos);
68 | if (bounds.x < 0 || this.screen.x1() < bounds.x1()) {
69 | APP.playSound('beep');
70 | this.v.x = -this.v.x;
71 | }
72 | if (bounds.y < 0) {
73 | APP.playSound('beep');
74 | this.v.y = -this.v.y;
75 | }
76 | this.pos = this.pos.add(this.v);
77 | }
78 |
79 | onCollided(entity: Entity) {
80 | // Bounces when hit the paddle.
81 | if (entity instanceof Paddle) {
82 | APP.playSound('beep');
83 | this.v.y = -4;
84 | }
85 | }
86 | }
87 |
88 |
89 | // Pong
90 | //
91 | class Pong extends GameScene {
92 |
93 | paddle: Paddle;
94 | ball: Ball;
95 |
96 | onStart() {
97 | super.onStart();
98 | console.info("pong.start");
99 | // Places the objects.
100 | this.paddle = new Paddle(this.screen);
101 | this.add(this.paddle);
102 | this.ball = new Ball(this.screen);
103 | this.add(this.ball);
104 | }
105 |
106 | onTick() {
107 | super.onTick();
108 | // Restarts when the ball goes out of screen.
109 | if (this.screen.height < this.ball.pos.y) {
110 | this.reset();
111 | }
112 | }
113 |
114 | onDirChanged(v: Vec2) {
115 | // Changes the paddle direction.
116 | this.paddle.vx = v.x;
117 | }
118 |
119 | render(ctx: CanvasRenderingContext2D) {
120 | // Paints the background.
121 | ctx.fillStyle = 'rgb(0,0,64)';
122 | fillRect(ctx, this.screen);
123 | // Paints everything else.
124 | super.render(ctx);
125 | }
126 | }
127 |
128 |
129 | // main: sets up the browser interaction.
130 | function main() {
131 | APP = new App(320, 240);
132 | APP.init(new Pong());
133 | }
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Euskit
2 | ======
3 |
4 | Euskit is a game engine designed for game jams.
5 | It is suitable for quick prototyping of 2D games.
6 |
7 | When participating a jam, you don't wanna spend the first three hours
8 | for basic plumbing stuff, and your finished game gotta be lean and
9 | playable with a minimal requirement. With Euskit, you can get a simple
10 | game up and running with just 200 lines of code. And it's pretty darn
11 | lightweight. Everyone can play it on a browser. Plus it's written in
12 | TypeScript, so you don't have to sweat in the last minutes while
13 | you're making a tiny change which has a typo and causes the entire
14 | program flooded with `NaN` or `undefined`. It only supports old-timey
15 | 2D games, but hey, I can be opinionated, right?
16 |
17 | By the way, there's no special editor or metadata needed. You only
18 | need Emacs (or vim) for writing a game (other than `tsc` of course).
19 | Everything is simple and straightforward and transparent, and there's
20 | absolutely no magic OH GOD I HATE MAGIC. The library is standalone,
21 | i.e. there's no external dependency, no `node_modules` hell or webpack
22 | crap either. A compiled game is just one `.js` file and one `.html`
23 | file (and pngs and mp3s when you need them). I've created more than
24 | 50 games with this thing, so this isn't entirely a pipe dream. And you
25 | can do it too.
26 |
27 | This engine was named by Mr. Rat King.
28 |
29 | * HTML5 + TypeScript.
30 | * Good for old-school pixel art games.
31 | * Simple and straightforward API .
32 |
33 | Samples
34 | -------
35 |
36 | These games are actually playable.
37 | Click the "(Code)" to see the actual source code.
38 | Be amazed at how it's simple and straightforward.
39 |
40 | * Pong (Code)
41 | * Shooter (Code)
42 | * Racing (Code)
43 | * Maze (Code)
44 | * Platformer (Code)
45 | * Pseudo3d (Code)
46 | * Adventure (Code)
47 | * Scramble (Code)
48 |
49 | Documents
50 | ---------
51 | Still work in progress...
52 |
53 | * User Guide
54 | * Quick Reference
55 | * Cheat Sheet
56 |
57 | Prerequisites
58 | -------------
59 | * TypeScript
60 | * (Optional) GNU Make
61 | * (Optional) TypeDoc http://typedoc.org/
62 |
--------------------------------------------------------------------------------
/samples/adventure/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 |
7 | // Adventure
8 | //
9 | // A simple text adventure game with multiple scenes.
10 | //
11 |
12 |
13 | // Initialize the resources.
14 | let FONT: Font;
15 | let HIFONT: Font;
16 | function main() {
17 | APP = new App(320, 240);
18 | FONT = new Font(APP.images['font'], 'white');
19 | HIFONT = new InvertedFont(APP.images['font'], 'white');
20 | APP.init(new Adventure());
21 | }
22 |
23 |
24 | // PictureScene
25 | //
26 | class PictureScene extends GameScene {
27 |
28 | dialogBox: DialogBox;
29 | image0: HTMLImageElement = null;
30 | image1: HTMLImageElement = null;
31 | alpha: number = 0;
32 |
33 | constructor() {
34 | super();
35 | let lineHeight = 8;
36 | let lineSpace = 4;
37 | let padding = 8;
38 | let width = this.screen.width-16;
39 | let height = (lineHeight+lineSpace)*6-lineSpace+padding*2;
40 | let rect = this.screen.resize(width, height, 's').move(0,-8);
41 | let textbox = new TextBox(rect, FONT);
42 | textbox.padding = padding;
43 | textbox.lineSpace = lineSpace;
44 | textbox.background = 'rgba(0,0,0,0.5)'
45 | this.dialogBox = new DialogBox(textbox, HIFONT);
46 | }
47 |
48 | onStart() {
49 | super.onStart();
50 | this.add(this.dialogBox);
51 | }
52 |
53 | onTick() {
54 | super.onTick();
55 | if (this.alpha < 1.0) {
56 | this.alpha = upperbound(1.0, this.alpha+0.05);
57 | }
58 | }
59 |
60 | onKeyDown(key: number) {
61 | super.onKeyDown(key);
62 | this.dialogBox.onKeyDown(key);
63 | }
64 |
65 | onMouseDown(p: Vec2, button: number) {
66 | super.onMouseDown(p, button);
67 | this.dialogBox.onMouseDown(p, button);
68 | }
69 |
70 | onMouseUp(p: Vec2, button: number) {
71 | super.onMouseUp(p, button);
72 | this.dialogBox.onMouseUp(p, button);
73 | }
74 |
75 | onMouseMove(p: Vec2) {
76 | super.onMouseMove(p);
77 | this.dialogBox.onMouseMove(p);
78 | }
79 |
80 | render(ctx: CanvasRenderingContext2D) {
81 | ctx.fillStyle = 'rgb(0,0,0)';
82 | fillRect(ctx, this.screen);
83 | ctx.save();
84 | if (this.image0 !== null) {
85 | ctx.globalAlpha = 1.0-this.alpha;
86 | ctx.drawImage(this.image0, this.screen.x, this.screen.y,
87 | this.screen.width, this.screen.height);
88 | }
89 | if (this.image1 !== null) {
90 | ctx.globalAlpha = this.alpha;
91 | ctx.drawImage(this.image1, this.screen.x, this.screen.y,
92 | this.screen.width, this.screen.height);
93 | }
94 | ctx.restore();
95 | super.render(ctx);
96 | // draw a textbox border.
97 | let rect = this.dialogBox.textbox.frame.inflate(-2,-2);
98 | ctx.strokeStyle = 'white';
99 | ctx.lineWidth = 2;
100 | strokeRect(ctx, rect);
101 | }
102 |
103 | changeScene(scene: Scene) {
104 | if (scene instanceof PictureScene) {
105 | scene.image0 = this.image1;
106 | }
107 | super.changeScene(scene);
108 | }
109 | }
110 |
111 |
112 | // Scene1
113 | //
114 | class Scene1 extends PictureScene {
115 | constructor() {
116 | super();
117 | this.image1 = APP.images['scene1'];
118 | }
119 | onStart() {
120 | super.onStart();
121 | this.dialogBox.addDisplay(
122 | 'It was a perfect sunny day. '+
123 | 'I was driving a sleepy countryside.', 10);
124 | let menu = this.dialogBox.addMenu();
125 | menu.addItem(new Vec2(20,30), 'I like an eggplant.');
126 | menu.addItem(new Vec2(20,40), 'This is nuts.');
127 | menu.addItem(new Vec2(20,50), 'Gimme a cucumber.');
128 | menu.selected.subscribe(() => {
129 | this.changeScene(new Scene2());
130 | });
131 | }
132 | }
133 |
134 | // Scene2
135 | //
136 | class Scene2 extends PictureScene {
137 | constructor() {
138 | super();
139 | this.image1 = APP.images['scene2'];
140 | }
141 | onStart() {
142 | super.onStart();
143 | this.dialogBox.addDisplay(
144 | 'I was fed up with cities. The beauty of '+
145 | 'a city makes everyone anonymous.', 10);
146 | let menu = this.dialogBox.addMenu();
147 | menu.addItem(new Vec2(20,40), 'O RLY?');
148 | menu.addItem(new Vec2(20,50), 'Beautiful quote.');
149 | menu.addItem(new Vec2(20,60), '43914745.');
150 | menu.selected.subscribe(() => {
151 | this.changeScene(new Scene3());
152 | });
153 | }
154 | }
155 |
156 | // Scene3
157 | //
158 | class Scene3 extends PictureScene {
159 | constructor() {
160 | super();
161 | this.image1 = APP.images['scene3'];
162 | }
163 | onStart() {
164 | super.onStart();
165 | this.dialogBox.addDisplay(
166 | 'But eventually, people can\'t really '+
167 | 'forget about their loved ones.', 10);
168 | let menu = this.dialogBox.addMenu();
169 | menu.addItem(new Vec2(20,30), 'ZZzzz.');
170 | menu.addItem(new Vec2(20,40), 'Only if what I think is what you think.');
171 | menu.addItem(new Vec2(20,50), 'xxThisSucks1729xx');
172 | menu.selected.subscribe(() => {
173 | this.changeScene(new Scene1());
174 | });
175 | }
176 | }
177 |
178 |
179 | // Adventure
180 | //
181 | class Adventure extends Scene {
182 |
183 | onStart() {
184 | super.onStart();
185 | this.changeScene(new Scene1());
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/samples/maze/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 | ///
9 |
10 | // Maze
11 | //
12 | const TILES = [
13 | null, // 0
14 | new RectSprite('white', new Rect(0,0,16,16)), // 1
15 | new RectSprite('yellow', new Rect(0,0,16,16)), // 2
16 | ];
17 | let isObstacle = ((c:number) => { return c == 1; });
18 | let isGoal = ((c:number) => { return c == 2; });
19 |
20 |
21 | // makeMaze
22 | function makeMaze(tilemap: TileMap, tile=0, ratio=0)
23 | {
24 | let pts = [] as Vec2[];
25 | let dirs = [new Vec2(-1,0), new Vec2(+1,0), new Vec2(0,-1), new Vec2(0,+1)];
26 | pts.push(new Vec2(1,1));
27 | while (0 < pts.length) {
28 | let i = rnd(pts.length);
29 | let p0 = pts[i];
30 | pts.splice(i, 1);
31 | let j = rnd(dirs.length);
32 | let t = dirs[0];
33 | dirs[0] = dirs[j];
34 | dirs[j] = t;
35 | for (let d of dirs) {
36 | let p1 = p0.add(d);
37 | let p2 = p1.add(d);
38 | if (p2.x < 0 || p2.y < 0 ||
39 | tilemap.width <= p2.x || tilemap.height <= p2.y) continue;
40 | let hole = (tilemap.get(p2.x, p2.y) != tile);
41 | if (hole || Math.random() < ratio) {
42 | tilemap.set(p1.x, p1.y, tile);
43 | tilemap.set(p2.x, p2.y, tile);
44 | }
45 | if (hole) {
46 | pts.push(p2);
47 | }
48 | }
49 | }
50 | }
51 |
52 |
53 | // Player
54 | //
55 | class Player extends TileMapEntity {
56 |
57 | goaled: Signal;
58 | usermove: Vec2;
59 | prevmove: Vec2;
60 |
61 | constructor(tilemap: TileMap, pos: Vec2) {
62 | super(tilemap, pos);
63 | this.goaled = new Signal(this);
64 | let sprite = new RectSprite('#0f0', new Rect(-8,-8,16,16));
65 | this.sprites = [sprite];
66 | this.collider = sprite.getBounds();
67 | this.isObstacle = isObstacle;
68 | this.usermove = new Vec2();
69 | this.prevmove = this.usermove;
70 | }
71 |
72 | onCollided(entity: Entity) {
73 | if (entity instanceof Enemy) {
74 | APP.playSound('beep');
75 | }
76 | }
77 |
78 | onTick() {
79 | super.onTick();
80 | if (!this.usermove.isZero()) {
81 | let v = this.getMove(this.usermove);
82 | if (v.isZero()) {
83 | v = this.getMove(this.prevmove);
84 | } else {
85 | this.prevmove = this.usermove.copy();
86 | }
87 | this.pos = this.pos.add(v);
88 | let bounds = this.getCollider() as Rect;
89 | if (this.tilemap.findTileByCoord(isGoal, bounds) !== null) {
90 | this.goaled.fire();
91 | }
92 | }
93 | }
94 | }
95 |
96 |
97 | // Enemy
98 | //
99 | class Enemy extends WalkerEntity {
100 |
101 | speedlimit = new Vec2(2,2);
102 | target: Entity;
103 |
104 | constructor(grid: GridConfig, objmap: RangeMap,
105 | sprite: Sprite, target: Entity, pos: Vec2) {
106 | super(grid, objmap, sprite.getBounds(), pos, 4);
107 | this.sprites = [sprite];
108 | this.collider = sprite.getBounds();
109 | this.target = target;
110 | }
111 |
112 | onTick() {
113 | super.onTick();
114 | let start = this.grid.coord2grid(this.pos);
115 | let goal = this.grid.coord2grid(this.target.pos);
116 | if (this.runner instanceof WalkerActionRunner) {
117 | if (!this.runner.goal.equals(goal)) {
118 | // abandon an obsolete plan.
119 | this.setRunner(null);
120 | }
121 | }
122 | if (this.runner === null) {
123 | let action = this.buildPlan(goal, start, 0, 40);
124 | if (action !== null) {
125 | this.setRunner(new WalkerActionRunner(this, action, goal));
126 | }
127 | }
128 | }
129 |
130 | getMove(v: Vec2) {
131 | v = super.getMove(v);
132 | return v.clamp(this.speedlimit);
133 | }
134 | }
135 |
136 |
137 | // Game
138 | //
139 | class Game extends GameScene {
140 |
141 | tilemap: TileMap;
142 | player: Player;
143 |
144 | onStart() {
145 | super.onStart();
146 | this.tilemap = new TileMap(16, 39, 29);
147 | this.tilemap.fill(1);
148 | makeMaze(this.tilemap, 0, 0.1);
149 | this.tilemap.set(this.tilemap.width-2, this.tilemap.height-2, 2);
150 | let rect = this.tilemap.map2coord(new Vec2(1,1));
151 | this.player = new Player(this.tilemap, rect.center());
152 | this.player.goaled.subscribe((e) => { this.changeScene(new GoalScene()); });
153 | this.add(this.player);
154 | let grid = new GridConfig(this.tilemap);
155 | let objmap = this.tilemap.getRangeMap('obstacle', isObstacle);
156 | let sprite = new RectSprite('#f80', new Rect(-8,-8,16,16));
157 | for (let i = 0; i < 10; i++) {
158 | let x = 3+2*rnd(int((this.tilemap.width-5)/2));
159 | let y = 3+2*rnd(int((this.tilemap.height-5)/2));
160 | let r = this.tilemap.map2coord(new Vec2(x,y));
161 | let enemy = new Enemy(grid, objmap, sprite, this.player, r.center());
162 | this.add(enemy);
163 | }
164 | }
165 |
166 | onTick() {
167 | super.onTick();
168 | let target = this.player.getCollider() as Rect
169 | this.world.setCenter(target.inflate(96,96), this.tilemap.bounds);
170 | }
171 |
172 | onDirChanged(v: Vec2) {
173 | this.player.usermove = v.scale(4);
174 | }
175 |
176 | render(ctx: CanvasRenderingContext2D) {
177 | ctx.fillStyle = 'rgb(0,0,64)';
178 | fillRect(ctx, this.screen);
179 | this.tilemap.renderWindowFromBottomLeft(
180 | ctx, this.world.window, (x,y,c)=>{return TILES[(c<0)? 1 : c];});
181 | super.render(ctx);
182 | }
183 | }
184 |
185 | class GoalScene extends HTMLScene {
186 |
187 | constructor() {
188 | super('Goal! ')
189 | }
190 |
191 | onStart() {
192 | super.onStart();
193 | APP.lockKeys();
194 | APP.playSound('goal');
195 | }
196 |
197 | change() {
198 | this.changeScene(new Game());
199 | }
200 | }
201 |
202 |
203 | function main() {
204 | APP = new App(320, 240);
205 | APP.init(new Game());
206 | }
207 |
--------------------------------------------------------------------------------
/samples/shooter/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 |
9 | // Shooter
10 | //
11 | // A basic shoot-em up using multiple enemy types.
12 | //
13 |
14 |
15 | // Initialize the resources.
16 | let FONT: Font;
17 | let SPRITES:ImageSpriteSheet;
18 | function main() {
19 | APP = new App(320, 240);
20 | FONT = new Font(APP.images['font'], 'white');
21 | SPRITES = new ImageSpriteSheet(
22 | APP.images['sprites'], new Vec2(16,16), new Vec2(8,8));
23 | APP.init(new Shooter());
24 | }
25 |
26 |
27 | // Bullet
28 | //
29 | class Bullet extends Particle {
30 |
31 | bounds = new Rect(-4, -1, 8, 2);
32 |
33 | constructor(pos: Vec2) {
34 | super(pos);
35 | this.sprites = [new RectSprite('white', this.bounds)];
36 | this.collider = this.bounds;
37 | this.movement = new Vec2(8, 0);
38 | }
39 |
40 | getFrame() {
41 | return this.world.area;
42 | }
43 | }
44 |
45 |
46 | // Explosion
47 | //
48 | class Explosion extends Entity {
49 | constructor(pos: Vec2) {
50 | super(pos);
51 | this.sprites = [SPRITES.get(4)];
52 | this.lifetime = 0.2;
53 | }
54 | }
55 |
56 |
57 | // Player
58 | //
59 | class Player extends Entity {
60 |
61 | usermove: Vec2 = new Vec2();
62 | firing: boolean = false;
63 | nextfire: number = 0; // Firing counter
64 |
65 | constructor(pos: Vec2) {
66 | super(pos);
67 | let sprite = SPRITES.get(0);
68 | this.sprites = [sprite];
69 | this.collider = sprite.getBounds();
70 | }
71 |
72 | onCollided(entity: Entity) {
73 | if (entity instanceof EnemyBase) {
74 | APP.playSound('explosion');
75 | this.chain(new Explosion(this.pos));
76 | this.stop();
77 | }
78 | }
79 |
80 | onTick() {
81 | super.onTick();
82 | // Restrict its position within the screen.
83 | let v = this.getMove(this.usermove);
84 | this.pos = this.pos.add(v);
85 | if (this.firing) {
86 | if (this.nextfire == 0) {
87 | // Shoot a bullet at a certain interval.
88 | let bullet = new Bullet(this.pos);
89 | this.world.add(bullet);
90 | APP.playSound('pew');
91 | this.nextfire = 4;
92 | }
93 | this.nextfire--;
94 | }
95 | }
96 |
97 | setFire(firing: boolean) {
98 | this.firing = firing;
99 | if (!this.firing) {
100 | // Reset the counter when start shooting.
101 | this.nextfire = 0;
102 | }
103 | }
104 |
105 | setMove(v: Vec2) {
106 | this.usermove = v.scale(4);
107 | }
108 | }
109 |
110 |
111 | // EnemyBase
112 | // This class has the common methods for all enemies.
113 | // They can be mixed in with applyMixins().
114 | //
115 | class EnemyBase extends Particle {
116 |
117 | killed: Signal;
118 |
119 | constructor(pos: Vec2) {
120 | super(pos);
121 | this.killed = new Signal(this);
122 | }
123 |
124 | getFrame() {
125 | return this.world.area;
126 | }
127 |
128 | onCollided(entity: Entity) {
129 | if (entity instanceof Bullet) {
130 | APP.playSound('explosion');
131 | this.stop();
132 | this.killed.fire();
133 | this.chain(new Explosion(this.pos));
134 | }
135 | }
136 | }
137 |
138 |
139 | // Enemy1
140 | //
141 | class Enemy1 extends EnemyBase {
142 |
143 | constructor(pos: Vec2) {
144 | super(pos);
145 | let sprite = SPRITES.get(1);
146 | this.sprites = [sprite];
147 | this.collider = sprite.getBounds();
148 | this.movement = new Vec2(-rnd(1,8), rnd(3)-1);
149 | }
150 | }
151 |
152 |
153 | // Enemy2
154 | //
155 | class Enemy2 extends EnemyBase {
156 |
157 | constructor(pos: Vec2) {
158 | super(pos);
159 | let sprite = SPRITES.get(2);
160 | this.sprites = [sprite];
161 | this.collider = sprite.getBounds();
162 | this.movement = new Vec2(-rnd(1,4), 0);
163 | }
164 |
165 | onTick() {
166 | super.onTick();
167 | // Move wiggly vertically.
168 | if (rnd(4) == 0) {
169 | this.movement.y = rnd(5)-2;
170 | }
171 | }
172 | }
173 |
174 |
175 | // Shooter
176 | //
177 | class Shooter extends GameScene {
178 |
179 | player: Player;
180 | stars: StarSprite;
181 | nextenemy: number; // Enemy spawning counter.
182 | score: number;
183 | scoreBox: TextBox;
184 |
185 | constructor() {
186 | super();
187 | this.scoreBox = new TextBox(this.screen.inflate(-8,-8), FONT);
188 | }
189 |
190 | onStart() {
191 | super.onStart();
192 | this.player = new Player(this.world.area.center());
193 | this.player.fences = [this.world.area];
194 | let task = new Task();
195 | task.lifetime = 2;
196 | task.stopped.subscribe(() => { this.reset(); });
197 | this.player.chain(task);
198 | this.add(this.player);
199 | this.stars = new StarSprite(this.screen, 100);
200 | this.nextenemy = 0;
201 | this.score = 0;
202 | this.updateScore();
203 | }
204 |
205 | onTick() {
206 | super.onTick();
207 | this.stars.move(new Vec2(-4, 0));
208 | // Spawn an enemy at a random interval.
209 | if (this.nextenemy == 0) {
210 | let area = this.world.area;
211 | let pos = new Vec2(area.width, rnd(area.height));
212 | let enemy:EnemyBase;
213 | if (rnd(2) == 0) {
214 | enemy = new Enemy1(pos);
215 | } else {
216 | enemy = new Enemy2(pos);
217 | }
218 | // Increase the score when it's killed.
219 | enemy.killed.subscribe(() => { this.score++; this.updateScore(); });
220 | this.add(enemy);
221 | this.nextenemy = 10+rnd(20);
222 | }
223 | this.nextenemy--;
224 | }
225 |
226 | onButtonPressed(keysym: KeySym) {
227 | this.player.setFire(true);
228 | }
229 | onButtonReleased(keysym: KeySym) {
230 | this.player.setFire(false);
231 | }
232 | onDirChanged(v: Vec2) {
233 | this.player.setMove(v);
234 | }
235 |
236 | updateScore() {
237 | // Update the text in the score box.
238 | this.scoreBox.clear();
239 | this.scoreBox.putText(['SCORE:'+format(this.score)]);
240 | }
241 |
242 | render(ctx: CanvasRenderingContext2D) {
243 | ctx.fillStyle = 'rgb(0,0,32)';
244 | fillRect(ctx, this.screen);
245 | super.render(ctx);
246 | this.stars.render(ctx);
247 | this.scoreBox.render(ctx);
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/samples/racing/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 | ///
9 | ///
10 |
11 | // Racing
12 | //
13 | // A simple racing game with a circular map.
14 | //
15 |
16 |
17 | // Initialize the resources.
18 | let FONT: Font;
19 | let SPRITES:ImageSpriteSheet;
20 | function main() {
21 | APP = new App(128, 160);
22 | FONT = new Font(APP.images['font'], 'white');
23 | SPRITES = new ImageSpriteSheet(
24 | APP.images['sprites'], new Vec2(16,32), new Vec2(0,0));
25 | APP.init(new Racing());
26 | }
27 |
28 |
29 | // Player
30 | //
31 | class Player extends Entity {
32 |
33 | usermove: Vec2 = new Vec2();
34 |
35 | constructor(pos: Vec2) {
36 | super(pos);
37 | let sprite = SPRITES.get(0, 0, 1, 1, new Vec2(8,8));
38 | this.sprites = [sprite]
39 | this.collider = sprite.getBounds();
40 | }
41 |
42 | onTick() {
43 | super.onTick();
44 | // Restrict its position within the screen.
45 | let v = this.getMove(this.usermove);
46 | this.pos = this.pos.add(v);
47 | }
48 |
49 | setMove(v: Vec2) {
50 | this.usermove = v.scale(4);
51 | }
52 | }
53 |
54 |
55 | // Track
56 | // Random generated track with a bridge.
57 | //
58 | const FLOOR = 1;
59 | const WATER = 2;
60 | const GREEN = 3;
61 | class Track extends TileMap {
62 |
63 | offset: number;
64 | brx: number; // Bridge position
65 | brw: number; // Bridge width
66 | brmw: number; // Bridge maximum width
67 | bre: number; // Bridge background
68 |
69 | constructor(width: number, height: number) {
70 | let map = [] as Int32Array[];
71 | for (let y = 0; y < height; y++) {
72 | map.push(new Int32Array(width).fill(FLOOR));
73 | }
74 | super(16, width, height, map);
75 | this.offset = 0;
76 | this.brx = 1;
77 | this.brw = width-2;
78 | this.brmw = width;
79 | this.bre = WATER;
80 | }
81 |
82 | // isFloor: returns true if there's a floor below the car.
83 | isFloor(rect: Rect) {
84 | return this.findTileByCoord((c:number) => { return c == FLOOR; }, rect);
85 | }
86 |
87 | proceed(speed: number) {
88 | this.offset += speed;
89 | if (16 <= this.offset) {
90 | let dy = (this.offset % 16);
91 | let dh = int((this.offset-dy)/16);
92 | this.shift(0, dh);
93 | // Generate new tiles.
94 | for (let y = 0; y < dh; y++) {
95 | for (let x = 0; x < this.width; x++) {
96 | this.set(x, y, this.bre);
97 | }
98 | for (let dx = 0; dx < this.brw; dx++) {
99 | this.set(this.brx+dx, y, FLOOR);
100 | }
101 | if (4 <= this.brw) {
102 | this.set(rnd(this.width), y, this.bre);
103 | }
104 | if (rnd(10) == 0) {
105 | this.bre = (this.bre == WATER)? GREEN : WATER;
106 | }
107 | if (rnd(10) == 0) {
108 | this.brw += rnd(3)-1;
109 | this.brw = clamp(2, this.brw, this.brmw);
110 | this.brx = clamp(0, this.brx, this.brmw-this.brw);
111 | } else {
112 | this.brx += rnd(3)-1;
113 | this.brx = clamp(0, this.brx, this.brmw-this.brw);
114 | }
115 | }
116 | this.offset = dy;
117 | }
118 | }
119 |
120 | render(ctx: CanvasRenderingContext2D) {
121 | // Render the background.
122 | ctx.save();
123 | ctx.translate(0, -32+this.offset);
124 | this.renderFromTopRight(
125 | ctx, (x,y,c) => { return (c == FLOOR)? null : SPRITES.get(c); });
126 | // Render the bridge.
127 | this.renderFromTopRight(
128 | ctx, (x,y,c) => { return (c != FLOOR)? null : SPRITES.get(c); });
129 | ctx.restore();
130 | }
131 | }
132 |
133 |
134 | // Racing
135 | //
136 | class Racing extends GameScene {
137 |
138 | player: Player;
139 | track: Track;
140 |
141 | score: number;
142 | scoreBox: TextBox;
143 | highScore: number;
144 | highScoreBox: TextBox;
145 |
146 | constructor() {
147 | super();
148 | this.scoreBox = new TextBox(this.screen.inflate(-2,-2), FONT);
149 | this.highScoreBox = new TextBox(this.screen.inflate(-2,-2), FONT);
150 | this.highScore = -1;
151 | }
152 |
153 | onStart() {
154 | super.onStart();
155 |
156 | this.player = new Player(this.world.area.center());
157 | this.player.fences = [this.world.area];
158 | this.add(this.player);
159 |
160 | this.track = new Track(int(this.screen.width/16),
161 | int(this.screen.height/16)+2);
162 |
163 | this.score = 0;
164 | this.updateScore();
165 | APP.setMusic('music', 0, 19.1);
166 | }
167 |
168 | onTick() {
169 | super.onTick();
170 | if (this.player.isRunning()) {
171 | let collider = this.player.getCollider();
172 | let b = collider.move(0, this.track.offset) as Rect;
173 | if (this.track.isFloor(b)) {
174 | let speed = int((1.0-this.player.pos.y/this.screen.height)*16);
175 | this.track.proceed(speed);
176 | this.score += int(lowerbound(0, Math.sqrt(speed)-2));
177 | this.updateScore();
178 | } else {
179 | let blinker = new Blinker(this.player);
180 | blinker.interval = 0.2;
181 | blinker.lifetime = 1.0;
182 | blinker.stopped.subscribe(() => { this.reset(); });
183 | this.player.chain(blinker);
184 | this.player.stop();
185 | APP.setMusic();
186 | APP.playSound('plunge');
187 | }
188 | }
189 | }
190 |
191 | onDirChanged(v: Vec2) {
192 | this.player.setMove(v);
193 | }
194 |
195 | render(ctx: CanvasRenderingContext2D) {
196 | ctx.fillStyle = 'rgb(0,0,0)';
197 | fillRect(ctx, this.screen);
198 | this.track.render(ctx);
199 | super.render(ctx);
200 | this.scoreBox.render(ctx);
201 | this.highScoreBox.render(ctx);
202 | }
203 |
204 | updateScore() {
205 | this.scoreBox.clear();
206 | this.scoreBox.putText([this.score.toString()]);
207 | if (this.highScore < this.score) {
208 | this.highScore = this.score;
209 | this.highScoreBox.clear();
210 | this.highScoreBox.putText([this.highScore.toString()], 'right');
211 | }
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/docs/userguide.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 | Euskit User Guide
15 |
16 |
17 |
18 |
19 | Introduction
20 |
21 | Euskit (pronounced you-skit ) is a game engine for HTML5/Web games. It is
22 | specifically made for making retro-looking 2D games with pixel
23 | art. Growing up in 80s in Japan, those arcade games have always a
24 | special place in me. This is my attempt to headstart in several
25 | game jams that I participated. The engine is designed to be
26 | lightweight and self-contained in that it doesn't depend on any
27 | external library except TypeScript. It is licensed under MIT License.
28 |
29 |
Getting Started
30 |
31 | The following instruction applies both Windows and Unix (or Mac).
32 |
33 | Install Node.js and TypeScript .
34 |
35 | > npm install -g typescript
36 |
37 |
38 | Copy the skel directory as your working directory:
39 |
40 | index.html Main HTML file.
41 | tsconfig.json TypeScript compiler settings.
42 | base/*.ts Euskit base library code.
43 | src/game.ts Game source code.
44 | assets/ Game assets.
45 |
46 |
47 | (On Unix, this can be also done by running the following script.)
48 |
49 | $ ./tools/setup.sh /path/to/project
50 |
51 |
52 | Run tsc at the working directory.
53 |
54 | > tsc
55 |
56 |
57 | Open the index.html file.
58 |
59 |
60 | Big Picture
61 |
62 | Here are the important concepts in Euskit:
63 |
64 |
65 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | App
82 | Images, ...
83 | Scene
84 | Scene
85 | ...
86 | Sprite
87 | Entity
88 | World
89 |
90 |
91 |
92 |
93 |
94 | App:
95 | Resource management and event loop.
96 | Scene:
97 | Game state management and event handling.
98 | World:
99 | Container where Entities are placed in.
100 | Entity:
101 | In-game character.
102 | Sprite:
103 | Graphical object to be shown.
104 |
105 |
106 |
107 | Key Classes
108 |
109 | App
110 |
111 | Every game has exactly one App object. It does the
112 | basic pluming and resource management (images and sounds); it has
113 | an event loop and connect all the external parts (i.e. a browser)
114 | to the game. Typically, you don't have to change this part.
115 | Scene
116 |
117 | A Scene can be thought of a mini-app or "mode" within the App.
118 | It's pretty much an event handler that manages the in-game states.
119 | This is primarily what a Euskit user will write. Euskit supports
120 | multiple Scenes, but it's possible to create an entire game with just
121 | one Scene.
122 | Entity
123 |
124 | An Entity is a bit like a GameObject in Unity.
125 | (Unlike Unity, however, Euskit uses a traditional hierarchical model
126 | instead of components.)
127 | Each Entity has its own process, Collider and one or more
128 | Sprites.
129 | Once you place an Entity in the game world, it moves on its own.
130 | A Rect is typically used for Collider in 2D games.
131 | World
132 |
133 | A World is where Entitys belong to.
134 | It is basically a container that manages the state of each Entity
135 | and performs basic collision handling.
136 | Sprite
137 |
138 | A Sprite is something to be displayed.
139 | It has a location, rotation and the reference to its content.
140 | Unlike some other engines, Sprite doesn't know how to move itself.
141 | It is just sitting at a certain location on screen. Scene or
142 | Entity is responsible to change/move its position.
143 | Task
144 |
145 | Each Entity is a subclass of Task.
146 | A Task is an independent object that runs by itself.
147 | It is often convenient to create a short-lived task for a delayed action
148 | (see the examples ).
149 | Signal
150 |
151 | Signal is much like C# events, but it is renamed here
152 | to avoid confusing with HTML5 events. Unlike EventListener class in HTML5,
153 | Each Signals are distinguished not by strings but by variables
154 | (see the examples ).
155 |
156 |
157 | How To Make Games Like...
158 |
159 | 1. Platformer
160 |
161 | TODO
162 |
163 |
2. Shooter
164 |
165 | TODO
166 |
--------------------------------------------------------------------------------
/base/entity.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 |
7 |
8 | // limitMotion: returns a motion vector that satisfies the given constraints.
9 | function limitMotion(
10 | collider: Collider, v0: Vec2,
11 | fences: Rect[]=null,
12 | obstacles: Collider[]=null,
13 | checkxy=true): Vec2
14 | {
15 | let bounds = collider.getAABB();
16 | let d = v0;
17 | if (fences !== null) {
18 | for (let fence of fences) {
19 | d = fence.boundRect(d, bounds);
20 | }
21 | }
22 | if (obstacles !== null) {
23 | for (let obstacle of obstacles) {
24 | d = obstacle.contact(d, collider);
25 | }
26 | }
27 | let v = d;
28 | if (checkxy && d !== v0) {
29 | v0 = v0.sub(d);
30 | bounds = bounds.add(d);
31 | collider = collider.add(d);
32 | if (v0.x != 0) {
33 | d = new Vec2(v0.x, 0);
34 | if (fences !== null) {
35 | for (let fence of fences) {
36 | d = fence.boundRect(d, bounds);
37 | }
38 | }
39 | if (obstacles !== null) {
40 | for (let obstacle of obstacles) {
41 | d = obstacle.contact(d, collider);
42 | }
43 | }
44 | v = v.add(d);
45 | v0 = v0.sub(d);
46 | bounds = bounds.add(d);
47 | collider = collider.add(d);
48 | }
49 | if (v0.y != 0) {
50 | d = new Vec2(0, v0.y);
51 | if (fences !== null) {
52 | for (let fence of fences) {
53 | d = fence.boundRect(d, bounds);
54 | }
55 | }
56 | if (obstacles !== null) {
57 | for (let obstacle of obstacles) {
58 | d = obstacle.contact(d, collider);
59 | }
60 | }
61 | v = v.add(d);
62 | //v0 = v0.sub(d);
63 | //bounds = bounds.add(d);
64 | //collider = collider.add(d);
65 | }
66 | }
67 | return v;
68 | }
69 |
70 |
71 | /** Entity: a thing that can interact with other things.
72 | */
73 | class Entity extends Task {
74 |
75 | world: World = null;
76 |
77 | pos: Vec2;
78 | collider: Collider = null;
79 | sprites: Sprite[] = null;
80 | fences: Rect[] = null;
81 | order: number = 0;
82 |
83 | rotation: number = 0;
84 | scale: Vec2 = null;
85 | alpha: number = 1.0;
86 |
87 | constructor(pos: Vec2) {
88 | super();
89 | this.pos = (pos !== null)? pos.copy() : pos;
90 | }
91 |
92 | toString() {
93 | return '';
94 | }
95 |
96 | isVisible() {
97 | return this.isRunning();
98 | }
99 |
100 | render(ctx: CanvasRenderingContext2D) {
101 | if (this.sprites !== null && this.pos !== null) {
102 | renderSprites(
103 | ctx, this.sprites, this.pos,
104 | this.rotation, this.scale, this.alpha);
105 | }
106 | }
107 |
108 | getCollider(): Collider {
109 | if (this.pos === null || this.collider === null) return null;
110 | return this.collider.add(this.pos);
111 | }
112 |
113 | onCollided(entity: Entity) {
114 | // [OVERRIDE]
115 | }
116 |
117 | getMove(v: Vec2) {
118 | let collider = this.getCollider();
119 | return limitMotion(collider, v, this.fences);
120 | }
121 | }
122 |
123 |
124 | // Particle
125 | //
126 | class Particle extends Entity {
127 |
128 | movement: Vec2 = null;
129 |
130 | onTick() {
131 | super.onTick();
132 | if (this.movement !== null) {
133 | this.pos = this.pos.add(this.movement);
134 | let frame = this.getFrame();
135 | if (frame !== null) {
136 | let collider = this.getCollider();
137 | if (collider !== null && !collider.overlapsRect(frame)) {
138 | this.stop();
139 | }
140 | }
141 | }
142 | }
143 |
144 | getFrame(): Rect {
145 | // [OVERRIDE]
146 | return null;
147 | }
148 | }
149 |
150 |
151 | // TileMapEntity
152 | //
153 | class TileMapEntity extends Entity {
154 |
155 | tilemap: TileMap;
156 | isObstacle: (c:number)=>boolean = ((c:number) => { return false; });
157 |
158 | constructor(tilemap: TileMap, pos: Vec2) {
159 | super(pos);
160 | this.tilemap = tilemap;
161 | }
162 |
163 | getMove(v: Vec2) {
164 | let collider = this.getCollider();
165 | let hitbox = collider.getAABB();
166 | let range = hitbox.union(hitbox.add(v));
167 | let obstacles = this.tilemap.getTileRects(this.isObstacle, range);
168 | return limitMotion(collider, v, this.fences, obstacles);
169 | }
170 | }
171 |
172 |
173 | // PhysicsConfig
174 | //
175 | class PhysicsConfig {
176 |
177 | jumpfunc: (vy:number,t:number)=>number =
178 | ((vy:number, t:number) => {
179 | return (0 <= t && t <= 5)? -4 : vy+1;
180 | });
181 | maxspeed: Vec2 = new Vec2(6,6);
182 |
183 | isObstacle: (c:number)=>boolean = ((c:number) => { return false; });
184 | isGrabbable: (c:number)=>boolean = ((c:number) => { return false; });
185 | isStoppable: (c:number)=>boolean = ((c:number) => { return false; });
186 | }
187 |
188 |
189 | // PlatformerEntity
190 | //
191 | class PlatformerEntity extends TileMapEntity {
192 |
193 | physics: PhysicsConfig;
194 | velocity: Vec2 = new Vec2();
195 |
196 | protected _jumpt: number = Infinity;
197 | protected _jumpend: number = 0;
198 | protected _landed: boolean = false;
199 |
200 | constructor(physics: PhysicsConfig, tilemap: TileMap, pos: Vec2) {
201 | super(tilemap, pos);
202 | this.physics = physics;
203 | }
204 |
205 | onTick() {
206 | super.onTick();
207 | this.fall(this._jumpt);
208 | if (this.isJumping()) {
209 | this._jumpt++;
210 | } else {
211 | this._jumpt = Infinity;
212 | }
213 | }
214 |
215 | getMove(v0: Vec2, context=null as string) {
216 | let collider = this.getCollider();
217 | let hitbox = collider.getAABB();
218 | let range = hitbox.union(hitbox.add(v0));
219 | let obstacles = this.getObstaclesFor(range, v0, context);
220 | return limitMotion(collider, v0, this.fences, obstacles);
221 | }
222 |
223 | getObstaclesFor(range: Rect, v: Vec2, context: string): Rect[] {
224 | let f = ((context == 'fall')?
225 | this.physics.isStoppable :
226 | this.physics.isObstacle);
227 | return this.tilemap.getTileRects(f, range);
228 | }
229 |
230 | setJump(jumpend: number) {
231 | if (0 < jumpend) {
232 | if (this.canJump()) {
233 | this._jumpt = 0;
234 | this.onJumped();
235 | }
236 | }
237 | this._jumpend = jumpend;
238 | }
239 |
240 | fall(t: number) {
241 | if (this.canFall()) {
242 | let vy = this.physics.jumpfunc(this.velocity.y, t);
243 | let v = new Vec2(this.velocity.x, vy);
244 | v = this.getMove(v, 'fall');
245 | this.pos = this.pos.add(v);
246 | this.velocity = v.clamp(this.physics.maxspeed);
247 | let landed = (0 < vy && this.velocity.y == 0);
248 | if (!this._landed && landed) {
249 | this.onLanded();
250 | }
251 | this._landed = landed;
252 | } else {
253 | this.velocity = new Vec2();
254 | if (!this._landed) {
255 | this.onLanded();
256 | }
257 | this._landed = true;
258 | }
259 | }
260 |
261 | canMove(v: Vec2) {
262 | return v === this.getMove(v);
263 | }
264 |
265 | canJump() {
266 | return this.isLanded();
267 | }
268 |
269 | canFall() {
270 | return true;
271 | }
272 |
273 | isJumping() {
274 | return (this._jumpt < this._jumpend);
275 | }
276 |
277 | isLanded() {
278 | return this._landed;
279 | }
280 |
281 | onJumped() {
282 | // [OVERRIDE]
283 | }
284 |
285 | onLanded() {
286 | // [OVERRIDE]
287 | }
288 | }
289 |
--------------------------------------------------------------------------------
/base/task.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 |
4 | enum TaskState {
5 | Scheduled,
6 | Running,
7 | Finished,
8 | }
9 |
10 |
11 | /** Object that represents a continuous process.
12 | * onTick() method is invoked at every frame.
13 | */
14 | class Task {
15 |
16 | /** List to which this task belongs (assigned by TaskList). */
17 | parent: TaskList = null;
18 |
19 | /** True if the task is running. */
20 | state: TaskState = TaskState.Scheduled;
21 | /** Lifetime.
22 | * This task automatically terminates itself after
23 | * the time specified here passes. */
24 | lifetime: number = Infinity;
25 | /** Start time. */
26 | startTime: number = Infinity;
27 |
28 | /** Fired when this task is stopped. */
29 | stopped: Signal;
30 |
31 | constructor() {
32 | this.stopped = new Signal(this);
33 | }
34 |
35 | toString() {
36 | return '';
37 | }
38 |
39 | /** Returns the number of seconds elapsed since
40 | * this task has started. */
41 | getTime() {
42 | return (getTime() - this.startTime);
43 | }
44 |
45 | /** Returns true if the task is scheduled but not yet running. */
46 | isScheduled() {
47 | return (this.state == TaskState.Scheduled);
48 | }
49 |
50 | /** Returns true if the task is running. */
51 | isRunning() {
52 | return (this.state == TaskState.Running);
53 | }
54 |
55 | /** Invoked when the task is started. */
56 | onStart() {
57 | if (this.state == TaskState.Scheduled) {
58 | this.state = TaskState.Running;
59 | this.startTime = getTime();
60 | }
61 | }
62 |
63 | /** Invoked when the task is stopped. */
64 | onStop() {
65 | }
66 |
67 | /** Invoked at every frame while the task is running. */
68 | onTick() {
69 | if (this.lifetime <= this.getTime()) {
70 | this.stop();
71 | }
72 | }
73 |
74 | /** Terminates the task. */
75 | stop() {
76 | if (this.state == TaskState.Running) {
77 | this.state = TaskState.Finished;
78 | this.stopped.fire();
79 | }
80 | }
81 |
82 | /** Schedules another task right after this task.
83 | * @param next Next Task.
84 | */
85 | chain(next: Task, signal: Signal=null): Task {
86 | switch (this.state) {
87 | case TaskState.Scheduled:
88 | case TaskState.Running:
89 | signal = (signal !== null)? signal : this.stopped;
90 | signal.subscribe(() => {
91 | if (this.parent !== null) {
92 | this.parent.add(next);
93 | }
94 | });
95 | break;
96 | case TaskState.Finished:
97 | // Start immediately if this task has already finished.
98 | if (this.parent !== null) {
99 | this.parent.add(next);
100 | }
101 | }
102 | return next;
103 | }
104 | }
105 |
106 |
107 | /** Task that plays a sound.
108 | */
109 | class SoundTask extends Task {
110 |
111 | /** Sound object to play. */
112 | sound: HTMLAudioElement;
113 | /** Start time of the sound. */
114 | soundStart: number;
115 | /** End time of the sound. */
116 | soundEnd: number;
117 |
118 | /** Constructor.
119 | * @param sound Sound object to play.
120 | * @param soundStart Start time of the sound.
121 | */
122 | constructor(sound: HTMLAudioElement, soundStart=MP3_GAP, soundEnd=0) {
123 | super();
124 | this.sound = sound;
125 | this.soundStart = soundStart;
126 | this.soundEnd = soundEnd;
127 | }
128 |
129 | /** Invoked when the task is started. */
130 | onStart() {
131 | super.onStart();
132 | // Start playing.
133 | this.sound.currentTime = this.soundStart;
134 | this.sound.play();
135 | }
136 |
137 | /** Invoked when the task is stopped. */
138 | onStop() {
139 | // Stop playing.
140 | this.sound.pause();
141 | super.onStop();
142 | }
143 |
144 | /** Invoked at every frame while the task is running. */
145 | onTick() {
146 | super.onTick();
147 | // Check if the playing is finished.
148 | if (0 < this.soundEnd && this.soundEnd <= this.sound.currentTime) {
149 | this.stop();
150 | } else if (this.sound.ended) {
151 | this.stop();
152 | }
153 | }
154 | }
155 |
156 | /** Abstract list of Tasks
157 | */
158 | interface TaskList {
159 | /** Add a new Task to the list.
160 | * @param task Task to add.
161 | */
162 | add(task: Task): void;
163 |
164 | /** Remove an existing Task from the list.
165 | * @param task Task to remove.
166 | */
167 | remove(task: Task): void;
168 | }
169 |
170 | /** List of Tasks that run parallely.
171 | */
172 | class ParallelTaskList extends Task implements TaskList {
173 |
174 | /** List of current tasks. */
175 | tasks: Task[] = [];
176 | /** If true, this task is stopped when the list becomes empty. */
177 | stopWhenEmpty: boolean = true;
178 |
179 | toString() {
180 | return ('');
181 | }
182 |
183 | /** Empties the task list. */
184 | onStart() {
185 | super.onStart();
186 | this.tasks = [];
187 | }
188 |
189 | /** Invoked at every frame. Update the current tasks. */
190 | onTick() {
191 | for (let task of this.tasks) {
192 | if (task.isScheduled()) {
193 | task.onStart();
194 | }
195 | if (task.isRunning()) {
196 | task.onTick();
197 | }
198 | }
199 |
200 | // Remove the finished tasks from the list.
201 | let removed = this.tasks.filter((task: Task) => { return !task.isRunning(); });
202 | for (let task of removed) {
203 | this.remove(task);
204 | }
205 |
206 | // Terminates itself then the list is empty.
207 | if (this.stopWhenEmpty && this.tasks.length == 0) {
208 | this.stop();
209 | }
210 | }
211 |
212 | /** Add a new Task to the list.
213 | * @param task Task to add.
214 | */
215 | add(task: Task) {
216 | task.parent = this;
217 | this.tasks.push(task);
218 | }
219 |
220 | /** Remove an existing Task from the list.
221 | * @param task Task to remove.
222 | */
223 | remove(task: Task) {
224 | if (!task.isScheduled()) {
225 | task.onStop();
226 | }
227 | removeElement(this.tasks, task);
228 | }
229 | }
230 |
231 |
232 | /** List of Tasks that run sequentially.
233 | */
234 | class SequentialTaskList extends Task implements TaskList {
235 |
236 | /** List of current tasks. */
237 | tasks: Task[] = null;
238 | /** If true, this task is stopped when the list becomes empty. */
239 | stopWhenEmpty: boolean = true;
240 |
241 | /** Constructor.
242 | * @param tasks List of tasks. (optional)
243 | */
244 | constructor(tasks: Task[]=null) {
245 | super();
246 | this.tasks = tasks;
247 | }
248 |
249 | /** Empties the task list. */
250 | onStart() {
251 | super.onStart();
252 | if (this.tasks === null) {
253 | this.tasks = [];
254 | }
255 | }
256 |
257 | /** Add a new Task to the list.
258 | * @param task Task to add.
259 | */
260 | add(task: Task) {
261 | task.parent = this;
262 | this.tasks.push(task);
263 | }
264 |
265 | /** Remove an existing Task from the list.
266 | * @param task Task to remove.
267 | */
268 | remove(task: Task) {
269 | removeElement(this.tasks, task);
270 | }
271 |
272 | /** Returns the task that is currently running
273 | * (or null if empty) */
274 | getCurrentTask() {
275 | return (0 < this.tasks.length)? this.tasks[0] : null;
276 | }
277 |
278 | /** Invoked at every frame. Update the current tasks. */
279 | onTick() {
280 | let task:Task = null;
281 | while (true) {
282 | task = this.getCurrentTask();
283 | if (task === null) break;
284 | // Starts the next task.
285 | if (task.isScheduled()) {
286 | task.onStart();
287 | }
288 | if (task.isRunning()) {
289 | task.onTick();
290 | break;
291 | }
292 | // Finishes the current task.
293 | this.remove(task);
294 | }
295 |
296 | // Terminates itself then the list is empty.
297 | if (this.stopWhenEmpty && task === null) {
298 | this.stop();
299 | }
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/docs/cheatsheet.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
16 | Euskit Cheat Sheet
17 |
18 |
19 |
20 |
21 | Initialize Game
22 |
23 | let FONT: Font;
24 | let SPRITES:ImageSpriteSheet;
25 | function main() {
26 | APP = new App(320, 240);
27 | FONT = new Font(APP.images['font'], 'white');
28 | SPRITES = new ImageSpriteSheet(
29 | APP.images['sprites'], new Vec2(16,16), new Vec2(8,8));
30 | APP.init(new Game());
31 | }
32 |
33 |
34 | Basic Player Control
35 |
36 | class Player extends Entity {
37 | usermove: Vec2;
38 | constructor(pos: Vec2) {
39 | super(pos);
40 |
41 | let sprite = new RectSprite('green', new Rect(-10,-10,20,20));
42 | this.sprites = [sprite];
43 | this.collider = sprite.getBounds();
44 | }
45 | onStart() {
46 | super.onStart();
47 |
48 | this.usermove = new Vec2();
49 | }
50 | onTick() {
51 | super.onTick();
52 |
53 | this.moveIfPossible(this.usermove);
54 | }
55 | }
56 |
57 | class Game extends GameScene {
58 | player: Player;
59 | onStart() {
60 | super.onStart();
61 |
62 | this.player = new Player(this.world.area.center());
63 | this.world.add(this.player);
64 | }
65 | onDirChanged(v: Vec2) {
66 |
67 | this.player.usermove = v;
68 | }
69 | }
70 |
71 |
72 | Restrict Entity within Bounds
73 |
74 | class Player extends Entity {
75 | getFencesFor(range: Rect, v: Vec2, context: string): Rect[] {
76 | return [this.world.area];
77 | }
78 | }
79 |
80 |
81 | Spawn Another Entity
82 |
83 | class Bullet extends Particle {
84 | constructor(pos: Vec2) {
85 | super(pos);
86 | let bounds = new Rect(-2, -2, 4, 4);
87 | let sprite = new RectSprite('white', bounds)
88 | this.sprites = [sprite];
89 | this.collider = bounds;
90 | this.movement = new Vec2(8, 0);
91 | }
92 | getFrame() {
93 | return this.world.area;
94 | }
95 | }
96 |
97 | class Player extends Entity {
98 | ...
99 | fire() {
100 | let bullet = new Bullet(this.pos);
101 | this.world.add(bullet);
102 | }
103 | }
104 |
105 |
106 | Schedule Delayed Action
107 |
108 | let task = new Task();
109 | task.lifetime = 2;
110 | task.stopped.subscribe(() => { info("foo"); });
111 | this.world.add(task);
112 |
113 |
114 | Signal Subscription/Firing
115 |
116 | class Player extends Entity {
117 | happened: Signal;
118 | constructor(pos: Vec2) {
119 | super(pos);
120 | this.happened = new Signal(this);
121 | }
122 | somethingHappened() {
123 | this.happened.fire('holy!');
124 | }
125 | }
126 |
127 | let player = new Player();
128 | player.happened.subscribe((e:Entity, value:string) => {
129 | info(value);
130 | });
131 |
132 |
133 | Change Scene
134 |
135 | class Game extends GameScene {
136 | ...
137 | gameover() {
138 |
139 | APP.lockKeys();
140 |
141 | this.changeScene(new GameOver());
142 | }
143 | }
144 |
145 |
146 | Show Explosion Effect
147 |
148 | class Explosion extends Entity {
149 | constructor(pos: Vec2) {
150 | super(pos);
151 | this.sprites = [new RectSprite('yellow, new Rect(-10,-10,20,20))];
152 | this.lifetime = 0.5;
153 | }
154 | }
155 |
156 | class Player extends Entity {
157 | ...
158 | die() {
159 | this.chain(new Explosion(this.pos));
160 | this.stop();
161 | }
162 | }
163 |
164 |
165 | Create TileMap
166 |
167 | class Game extends GameScene {
168 | ...
169 | onStart() {
170 | const MAP = [
171 | "0010010000",
172 | "0222022002",
173 | ...
174 | "0000010030",
175 | ];
176 | this.tilemap = new TileMap(16, 10, 10, MAP.map(
177 | (v:string) => { return str2array(v); }
178 | ));
179 | let p = this.tilemap.findTile((c:number) => { return c == 3; });
180 | this.player = new Player(this, this.tilemap.map2coord(p).center());
181 | }
182 | render(ctx: CanvasRenderingContext2D) {
183 | super.render(ctx);
184 | this.tilemap.renderWindowFromBottomLeft(
185 | ctx, this.world.window,
186 | (x,y,c) => { return TILES.get(c); });
187 | }
188 | }
189 |
190 |
191 | Collision with TileMap
192 |
193 | class Player extends Entity {
194 | tilemap: TileMap;
195 | getObstaclesFor(range: Rect, v: Vec2, context=null as string): Rect[] {
196 | let f = ((c:number) => { return (c == 1 || c == 3); });
197 | return this.tilemap.getTileRects(f, range);
198 | }
199 | }
200 |
201 |
202 | Collision with Other Entities
203 |
204 | class Player extends Entity {
205 | getObstaclesFor(range: Rect, v: Vec2, context=null as string): Rect[] {
206 | let f = ((e:Entity) => { return (e instanceof Enemy); });
207 | return this.world.getEntityColliders(f, range);
208 | }
209 | }
210 |
211 |
212 | Draw Rectangle
213 |
214 | ctx.strokeStyle = 'white';
215 | ctx.lineWidth = 2;
216 | strokeRect(ctx, rect);
217 |
218 |
219 | Display Text
220 |
221 | let glyphs = APP.images['font'];
222 | let font = new Font(glyphs, 'white');
223 | let textbox = new TextBox(new Rect(0, 0, 300, 200), font);
224 | textbox.borderWidth = 2;
225 | textbox.borderColor = 'white';
226 | textbox.clear();
227 | textbox.putText(['HELLO', 'WORLD'], 'center', 'center');
228 | textbox.render(ctx);
229 |
230 |
231 | Blinking Banner
232 |
233 | let banner = new BannerBox(
234 | this.screen, font,
235 | ['COLLECT ALL TEH THINGS!']);
236 | banner.lifetime = 2.0;
237 | banner.interval = 0.5;
238 | this.world.add(banner);
239 |
240 |
241 | Text Particle
242 |
243 | let yay = new TextParticle(entity.pos, font, 'YAY!');
244 | yay.movement = new Vec2(0,-4);
245 | yay.lifetime = 1.0;
246 | this.world.add(yay);
247 |
248 |
249 | Typewriter Effect
250 |
251 | let textbox = new TextBox(new Rect(0, 0, 300, 200), font);
252 | let dialog = new DialogBox(textbox);
253 |
254 | dialog.addDisplay('Hello, this is a test.', 12);
255 |
256 |
257 |
258 |
259 | let glyphs = APP.images['font'];
260 | let font = new Font(glyphs, 'white');
261 | let invfont = new InvertedFont(glyphs, 'white');
262 | let textbox = new TextBox(new Rect(0, 0, 300, 200), font);
263 | let dialog = new DialogBox(textbox, invfont);
264 |
265 | dialog.addDisplay('What to do?');
266 | let menu = this.dialogBox.addMenu();
267 | menu.addItem(new Vec2(20,20), 'Foo');
268 | menu.addItem(new Vec2(20,30), 'Bar');
269 | menu.addItem(new Vec2(20,40), 'Bzzz');
270 | menu.selected.subscribe((value) => {
271 | info(value);
272 | });
273 |
274 |
275 | Mouse/Touchscreen Control
276 |
277 | class Button extends Entity {
278 | constructor(pos: Vec2) {
279 | super(pos);
280 | this.sprites = [new RectSprite('white', new Rect(-10,-10,20,20))];
281 | }
282 | }
283 | class Game extends GameScene {
284 | onStart() {
285 | this.world.add(new Button(new Vec2(100,100)));
286 | this.world.mouseDown.subscribe((world:World, entity:Entity) => {
287 | info(entity);
288 | }
289 | }
290 | }
291 |
292 |
293 | Text Button
294 |
295 | class TextButton extends Entity {
296 | constructor(frame: Rect, text: string) {
297 | super(frame.center());
298 | frame = frame.move(-this.pos.x, -this.pos.y);
299 | let textbox = new TextBox(frame, font);
300 | textbox.putText([text], 'center', 'center');
301 | this.sprites = [textbox];
302 | this.collider = frame;
303 | }
304 | isFocused() {
305 | return (this.world !== null &&
306 | this.world.mouseFocus === this);
307 | }
308 | renderExtra(ctx: CanvasRenderingContext2D) {
309 | if (this.isFocused()) {
310 | let rect = this.sprite.getBounds();
311 | ctx.strokeStyle = 'white';
312 | ctx.lineWidth = 2;
313 | strokeRect(ctx, rect.inflate(4,4));
314 | }
315 | }
316 | }
317 |
318 |
--------------------------------------------------------------------------------
/base/scene.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 |
7 | // World
8 | //
9 | class World extends ParallelTaskList {
10 |
11 | mouseFocus: Entity = null;
12 | mouseActive: Entity = null;
13 | mouseDown: Signal;
14 | mouseUp: Signal;
15 |
16 | area: Rect;
17 | window: Rect;
18 | entities: Entity[];
19 |
20 | constructor(area: Rect) {
21 | super();
22 | this.mouseDown = new Signal(this);
23 | this.mouseUp = new Signal(this);
24 | this.area = area.copy();
25 | this.reset();
26 | }
27 |
28 | toString() {
29 | return '';
30 | }
31 |
32 | reset() {
33 | this.window = this.area.copy();
34 | this.entities = [];
35 | }
36 |
37 | onTick() {
38 | super.onTick();
39 | this.checkEntityCollisions();
40 | }
41 |
42 | add(task: Task) {
43 | if (task instanceof Entity) {
44 | task.world = this;
45 | this.entities.push(task);
46 | this.sortEntitiesByOrder();
47 | }
48 | super.add(task);
49 | }
50 |
51 | remove(task: Task) {
52 | if (task instanceof Entity) {
53 | removeElement(this.entities, task);
54 | }
55 | super.remove(task);
56 | }
57 |
58 | render(ctx: CanvasRenderingContext2D) {
59 | ctx.save();
60 | ctx.translate(-this.window.x, -this.window.y);
61 | for (let entity of this.entities) {
62 | if (!entity.isVisible()) continue;
63 | if (entity.pos === null) continue;
64 | entity.render(ctx);
65 | }
66 | ctx.restore();
67 | for (let entity of this.entities) {
68 | if (!entity.isVisible()) continue;
69 | if (entity.pos !== null) continue;
70 | entity.render(ctx);
71 | }
72 | }
73 |
74 | findEntityAt(p: Vec2): Entity {
75 | let found: Entity = null;
76 | for (let entity of this.entities) {
77 | if (!entity.isVisible()) continue;
78 | let collider = entity.getCollider();
79 | if (collider instanceof Rect) {
80 | if (collider.containsPt(p)) {
81 | if (found === null || entity.order < found.order) {
82 | found = entity;
83 | }
84 | }
85 | }
86 | }
87 | return found;
88 | }
89 |
90 | moveCenter(v: Vec2) {
91 | this.window = this.window.add(v);
92 | }
93 |
94 | setCenter(target: Rect, bounds: Rect=null) {
95 | if (this.window.width < target.width) {
96 | this.window.x = (target.width-this.window.width)/2;
97 | } else if (target.x < this.window.x) {
98 | this.window.x = target.x;
99 | } else if (this.window.x+this.window.width < target.x+target.width) {
100 | this.window.x = target.x+target.width - this.window.width;
101 | }
102 | if (this.window.height < target.height) {
103 | this.window.y = (target.height-this.window.height)/2;
104 | } else if (target.y < this.window.y) {
105 | this.window.y = target.y;
106 | } else if (this.window.y+this.window.height < target.y+target.height) {
107 | this.window.y = target.y+target.height - this.window.height;
108 | }
109 | if (bounds !== null) {
110 | this.window = this.window.clamp(bounds);
111 | }
112 | }
113 |
114 | moveAll(v: Vec2) {
115 | for (let entity of this.entities) {
116 | if (!entity.isRunning()) continue;
117 | if (entity.pos === null) continue;
118 | entity.pos = entity.pos.add(v);
119 | }
120 | }
121 |
122 | onMouseDown(p: Vec2, button: number) {
123 | if (button == 0) {
124 | this.mouseFocus = this.findEntityAt(p);
125 | this.mouseActive = this.mouseFocus;
126 | if (this.mouseActive !== null) {
127 | this.mouseDown.fire(this.mouseActive, p);
128 | }
129 | }
130 | }
131 |
132 | onMouseUp(p: Vec2, button: number) {
133 | if (button == 0) {
134 | this.mouseFocus = this.findEntityAt(p);
135 | if (this.mouseActive !== null) {
136 | this.mouseUp.fire(this.mouseActive, p);
137 | }
138 | this.mouseActive = null;
139 | }
140 | }
141 |
142 | onMouseMove(p: Vec2) {
143 | if (this.mouseActive === null) {
144 | this.mouseFocus = this.findEntityAt(p);
145 | }
146 | }
147 |
148 | findEntities(collider0: Collider): Entity[] {
149 | let found = [] as Entity[];
150 | for (let entity1 of this.entities) {
151 | if (!entity1.isRunning()) continue;
152 | let collider1 = entity1.getCollider();
153 | if (collider1 !== null && !collider1.overlaps(collider0)) continue;
154 | found.push(entity1);
155 | }
156 | return found;
157 | }
158 |
159 | applyEntities(f: (e:Entity)=>boolean, collider0: Collider=null): Entity {
160 | for (let entity1 of this.entities) {
161 | if (!entity1.isRunning()) continue;
162 | if (collider0 !== null) {
163 | let collider1 = entity1.getCollider();
164 | if (collider1 !== null && !collider1.overlaps(collider0)) continue;
165 | }
166 | if (f(entity1)) {
167 | return entity1;
168 | }
169 | }
170 | return null;
171 | }
172 |
173 | sortEntitiesByOrder() {
174 | this.entities.sort((a:Entity, b:Entity) => { return a.order-b.order; });
175 | }
176 |
177 | getEntityColliders(f0: (e:Entity)=>boolean, range: Collider=null) {
178 | let a = [] as Collider[];
179 | let f = (entity: Entity) => {
180 | if (f0(entity)) {
181 | let collider = entity.getCollider();
182 | if (collider != null) {
183 | a.push(collider);
184 | }
185 | }
186 | return false;
187 | }
188 | this.applyEntities(f, range);
189 | return a;
190 | }
191 |
192 | checkEntityCollisions() {
193 | this.applyEntityPairs(
194 | (e0:Entity, e1:Entity) => {
195 | e0.onCollided(e1);
196 | e1.onCollided(e0);
197 | });
198 | }
199 |
200 | applyEntityPairs(f: (e0:Entity,e1:Entity)=>void) {
201 | for (let i = 0; i < this.entities.length; i++) {
202 | let entity0 = this.entities[i];
203 | if (!entity0.isRunning()) continue;
204 | let collider0 = entity0.getCollider();
205 | if (collider0 === null) continue;
206 | for (let j = i+1; j < this.entities.length; j++) {
207 | let entity1 = this.entities[j];
208 | if (!entity1.isRunning()) continue;
209 | let collider1 = entity1.getCollider();
210 | if (collider1 === null) continue;
211 | if (collider0.overlaps(collider1)) {
212 | f(entity0, entity1)
213 | }
214 | }
215 | }
216 | }
217 | }
218 |
219 |
220 | // Scene
221 | //
222 | class Scene {
223 |
224 | screen: Rect;
225 |
226 | constructor() {
227 | this.screen = new Rect(0, 0, APP.canvas.width, APP.canvas.height);
228 | }
229 |
230 | changeScene(scene: Scene) {
231 | APP.post(() => { APP.setScene(scene); });
232 | }
233 |
234 | reset() {
235 | this.onStop();
236 | this.onStart();
237 | }
238 |
239 | onStart() {
240 | // [OVERRIDE]
241 | }
242 |
243 | onStop() {
244 | // [OVERRIDE]
245 | }
246 |
247 | onTick() {
248 | // [OVERRIDE]
249 | }
250 |
251 | render(ctx: CanvasRenderingContext2D) {
252 | // [OVERRIDE]
253 | }
254 |
255 | onDirChanged(v: Vec2) {
256 | // [OVERRIDE]
257 | }
258 |
259 | onButtonPressed(keysym: KeySym) {
260 | // [OVERRIDE]
261 | }
262 |
263 | onButtonReleased(keysym: KeySym) {
264 | // [OVERRIDE]
265 | }
266 |
267 | onKeyDown(key: number) {
268 | // [OVERRIDE]
269 | }
270 |
271 | onKeyUp(key: number) {
272 | // [OVERRIDE]
273 | }
274 |
275 | onKeyPress(char: number) {
276 | // [OVERRIDE]
277 | }
278 |
279 | onMouseDown(p: Vec2, button: number) {
280 | // [OVERRIDE]
281 | }
282 |
283 | onMouseUp(p: Vec2, button: number) {
284 | // [OVERRIDE]
285 | }
286 |
287 | onMouseMove(p: Vec2) {
288 | // [OVERRIDE]
289 | }
290 |
291 | onFocus() {
292 | // [OVERRIDE]
293 | }
294 |
295 | onBlur() {
296 | // [OVERRIDE]
297 | }
298 | }
299 |
300 |
301 | // HTMLScene
302 | //
303 | class HTMLScene extends Scene {
304 |
305 | text: string;
306 |
307 | constructor(text: string) {
308 | super();
309 | this.text = text;
310 | }
311 |
312 | onStart() {
313 | super.onStart();
314 | let scene = this;
315 | let bounds = APP.elem.getBoundingClientRect();
316 | let e = APP.addElement(
317 | new Rect(bounds.width/8, bounds.height/4,
318 | 3*bounds.width/4, bounds.height/2));
319 | e.align = 'left';
320 | e.style.padding = '10px';
321 | e.style.color = 'black';
322 | e.style.background = 'white';
323 | e.style.border = 'solid black 2px';
324 | e.innerHTML = this.text;
325 | e.onmousedown = ((e) => { scene.onChanged(); });
326 | }
327 |
328 | render(ctx: CanvasRenderingContext2D) {
329 | ctx.fillStyle = 'rgb(0,0,0)';
330 | fillRect(ctx, this.screen);
331 | }
332 |
333 | onChanged() {
334 | // [OVERRIDE]
335 | }
336 |
337 | onMouseDown(p: Vec2, button: number) {
338 | this.onChanged();
339 | }
340 |
341 | onKeyDown(key: number) {
342 | this.onChanged();
343 | }
344 |
345 | }
346 |
347 |
348 | // GameScene
349 | //
350 | class GameScene extends Scene {
351 |
352 | world: World = null;
353 |
354 | onStart() {
355 | super.onStart();
356 | this.world = new World(this.screen);
357 | this.world.onStart();
358 | }
359 |
360 | onTick() {
361 | super.onTick();
362 | this.world.onTick();
363 | }
364 |
365 | render(ctx: CanvasRenderingContext2D) {
366 | super.render(ctx);
367 | this.world.render(ctx);
368 | }
369 |
370 | add(task: Task) {
371 | this.world.add(task);
372 | }
373 |
374 | remove(task: Task) {
375 | this.world.remove(task);
376 | }
377 |
378 | onMouseDown(p: Vec2, button: number) {
379 | super.onMouseDown(p, button);
380 | this.world.onMouseDown(p, button);
381 | }
382 |
383 | onMouseUp(p: Vec2, button: number) {
384 | super.onMouseUp(p, button);
385 | this.world.onMouseUp(p, button);
386 | }
387 |
388 | onMouseMove(p: Vec2) {
389 | super.onMouseMove(p);
390 | this.world.onMouseMove(p);
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/base/tilemap.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 |
6 | // TileMap
7 | //
8 | class TileMap {
9 |
10 | tilesize: number;
11 | width: number;
12 | height: number;
13 | bounds: Rect;
14 | map: Int32Array[];
15 |
16 | private _rangemap: { [index:string]: RangeMap } = {};
17 |
18 | constructor(tilesize: number, width: number, height: number,
19 | map: Int32Array[]=null) {
20 | this.tilesize = tilesize;
21 | this.width = width;
22 | this.height = height;
23 | if (map === null) {
24 | map = range(height).map(() => { return new Int32Array(width); });
25 | }
26 | this.map = map;
27 | this.bounds = new Rect(0, 0,
28 | this.width*this.tilesize,
29 | this.height*this.tilesize);
30 | }
31 |
32 | toString() {
33 | return '';
34 | }
35 |
36 | get(x: number, y: number): number {
37 | if (0 <= x && 0 <= y && x < this.width && y < this.height) {
38 | return this.map[y][x];
39 | } else {
40 | return -1;
41 | }
42 | }
43 |
44 | set(x: number, y: number, c: number) {
45 | if (0 <= x && 0 <= y && x < this.width && y < this.height) {
46 | this.map[y][x] = c;
47 | this._rangemap = {};
48 | }
49 | }
50 |
51 | fill(c: number, rect: Rect=null) {
52 | if (rect === null) {
53 | rect = new Rect(0, 0, this.width, this.height);
54 | }
55 | for (let dy = 0; dy < rect.height; dy++) {
56 | let y = rect.y+dy;
57 | for (let dx = 0; dx < rect.width; dx++) {
58 | let x = rect.x+dx;
59 | this.map[y][x] = c;
60 | }
61 | }
62 | this._rangemap = {};
63 | }
64 |
65 | copy(): TileMap {
66 | let map:Int32Array[] = [];
67 | for (let a of this.map) {
68 | map.push(a.slice());
69 | }
70 | return new TileMap(this.tilesize, this.width, this.height, map);
71 | }
72 |
73 | coord2map(rect: Vec2|Rect): Rect {
74 | let ts = this.tilesize;
75 | if (rect instanceof Rect) {
76 | let x0 = Math.floor(rect.x/ts);
77 | let y0 = Math.floor(rect.y/ts);
78 | let x1 = Math.ceil((rect.x+rect.width)/ts);
79 | let y1 = Math.ceil((rect.y+rect.height)/ts);
80 | return new Rect(x0, y0, x1-x0, y1-y0);
81 | } else {
82 | let x = Math.floor(rect.x/ts);
83 | let y = Math.floor(rect.y/ts);
84 | return new Rect(x, y, 1, 1);
85 | }
86 | }
87 |
88 | map2coord(rect: Vec2|Rect): Rect {
89 | let ts = this.tilesize;
90 | if (rect instanceof Vec2) {
91 | return new Rect(rect.x*ts, rect.y*ts, ts, ts);
92 | } else if (rect instanceof Rect) {
93 | return new Rect(rect.x*ts, rect.y*ts,
94 | rect.width*ts, rect.height*ts);
95 | } else {
96 | return null;
97 | }
98 | }
99 |
100 | apply(f: (x:number,y:number,c:number)=>boolean, rect: Rect=null): Vec2 {
101 | if (rect === null) {
102 | rect = new Rect(0, 0, this.width, this.height);
103 | }
104 | for (let dy = 0; dy < rect.height; dy++) {
105 | let y = rect.y+dy;
106 | for (let dx = 0; dx < rect.width; dx++) {
107 | let x = rect.x+dx;
108 | let c = this.get(x, y);
109 | if (f(x, y, c)) {
110 | return new Vec2(x,y);
111 | }
112 | }
113 | }
114 | return null;
115 | }
116 |
117 | shift(vx: number, vy: number, rect: Rect=null) {
118 | if (rect === null) {
119 | rect = new Rect(0, 0, this.width, this.height);
120 | }
121 | let src:Int32Array[] = [];
122 | for (let dy = 0; dy < rect.height; dy++) {
123 | let a = new Int32Array(rect.width);
124 | for (let dx = 0; dx < rect.width; dx++) {
125 | a[dx] = this.map[rect.y+dy][rect.x+dx];
126 | }
127 | src.push(a);
128 | }
129 | for (let dy = 0; dy < rect.height; dy++) {
130 | for (let dx = 0; dx < rect.width; dx++) {
131 | let x = (dx+vx + rect.width) % rect.width;
132 | let y = (dy+vy + rect.height) % rect.height;
133 | this.map[rect.y+y][rect.x+x] = src[dy][dx];
134 | }
135 | }
136 | }
137 |
138 | findTile(f0: (c:number)=>boolean, rect: Rect=null): Vec2 {
139 | return this.apply((x,y,c)=>{return f0(c);}, rect);
140 | }
141 |
142 | findTileByCoord(f0: (c:number)=>boolean, range: Rect): Rect {
143 | let p = this.apply((x,y,c)=>{return f0(c);}, this.coord2map(range));
144 | return (p === null)? null : this.map2coord(p);
145 | }
146 |
147 | getTileRects(f0: (c:number)=>boolean, range:Rect): Rect[] {
148 | let ts = this.tilesize;
149 | let rects = [] as Rect[];
150 | let f = (x:number, y:number, c:number) => {
151 | if (f0(c)) {
152 | rects.push(new Rect(x*ts, y*ts, ts, ts));
153 | }
154 | return false;
155 | }
156 | this.apply(f, this.coord2map(range));
157 | return rects;
158 | }
159 |
160 | getRangeMap(key:string, f: (c:number)=>boolean): RangeMap {
161 | let map = this._rangemap[key];
162 | if (map === undefined) {
163 | map = new RangeMap(this, f);
164 | this._rangemap[key] = map;
165 | }
166 | return map;
167 | }
168 |
169 | render(ctx: CanvasRenderingContext2D, sprites: SpriteSheet) {
170 | this.renderFromBottomLeft(
171 | ctx, (x,y,c) => { return sprites.get(c); });
172 | }
173 |
174 | renderFromBottomLeft(
175 | ctx: CanvasRenderingContext2D,
176 | ft: (x:number,y:number,c:number)=>Sprite,
177 | x0=0, y0=0, w=0, h=0) {
178 | // Align the pos to the bottom left corner.
179 | let ts = this.tilesize;
180 | w = (w != 0)? w : this.width;
181 | h = (h != 0)? h : this.height;
182 | // Draw tiles from the bottom-left first.
183 | for (let dy = h-1; 0 <= dy; dy--) {
184 | let y = y0+dy;
185 | for (let dx = 0; dx < w; dx++) {
186 | let x = x0+dx;
187 | let c = this.get(x, y);
188 | let sprite = ft(x, y, c);
189 | if (sprite !== null) {
190 | ctx.save();
191 | ctx.translate(ts*dx, ts*dy);
192 | sprite.render(ctx);
193 | ctx.restore();
194 | }
195 | }
196 | }
197 | }
198 |
199 | renderFromTopRight(
200 | ctx: CanvasRenderingContext2D,
201 | ft: (x:number,y:number,c:number)=>Sprite,
202 | x0=0, y0=0, w=0, h=0) {
203 | // Align the pos to the bottom left corner.
204 | let ts = this.tilesize;
205 | w = (w != 0)? w : this.width;
206 | h = (h != 0)? h : this.height;
207 | // Draw tiles from the top-right first.
208 | for (let dy = 0; dy < h; dy++) {
209 | let y = y0+dy;
210 | for (let dx = w-1; 0 <= dx; dx--) {
211 | let x = x0+dx;
212 | let c = this.get(x, y);
213 | let sprite = ft(x, y, c);
214 | if (sprite !== null) {
215 | ctx.save();
216 | ctx.translate(ts*dx, ts*dy);
217 | sprite.render(ctx);
218 | ctx.restore();
219 | }
220 | }
221 | }
222 | }
223 |
224 |
225 | renderWindow(
226 | ctx: CanvasRenderingContext2D,
227 | window:Rect, sprites: SpriteSheet) {
228 | this.renderWindowFromBottomLeft(
229 | ctx, window, (x,y,c) => { return sprites.get(c); });
230 | }
231 |
232 | renderWindowFromBottomLeft(
233 | ctx: CanvasRenderingContext2D,
234 | window: Rect,
235 | ft: (x:number,y:number,c:number)=>Sprite) {
236 | let ts = this.tilesize;
237 | let x0 = Math.floor(window.x/ts);
238 | let y0 = Math.floor(window.y/ts);
239 | let x1 = Math.ceil((window.x+window.width)/ts);
240 | let y1 = Math.ceil((window.y+window.height)/ts);
241 | ctx.save();
242 | ctx.translate(x0*ts-window.x, y0*ts-window.y);
243 | this.renderFromBottomLeft(
244 | ctx, ft,
245 | x0, y0, x1-x0+1, y1-y0+1);
246 | ctx.restore();
247 | }
248 |
249 | renderWindowFromTopRight(
250 | ctx: CanvasRenderingContext2D,
251 | window: Rect,
252 | ft: (x:number,y:number,c:number)=>Sprite) {
253 | let ts = this.tilesize;
254 | let x0 = Math.floor(window.x/ts);
255 | let y0 = Math.floor(window.y/ts);
256 | let x1 = Math.ceil((window.x+window.width)/ts);
257 | let y1 = Math.ceil((window.y+window.height)/ts);
258 | ctx.save();
259 | ctx.translate(x0*ts-window.x, y0*ts-window.y);
260 | this.renderFromTopRight(
261 | ctx, ft,
262 | x0, y0, x1-x0+1, y1-y0+1);
263 | ctx.restore();
264 | }
265 | }
266 |
267 |
268 | // RangeMap
269 | //
270 | class RangeMap {
271 |
272 | width: number;
273 | height: number;
274 |
275 | private _data: Int32Array[];
276 |
277 | constructor(tilemap: TileMap, f: (c:number)=>boolean) {
278 | let data = new Array(tilemap.height+1);
279 | let row0 = new Int32Array(tilemap.width+1);
280 | for (let x = 0; x < tilemap.width; x++) {
281 | row0[x+1] = 0;
282 | }
283 | data[0] = row0;
284 | for (let y = 0; y < tilemap.height; y++) {
285 | let row1 = new Int32Array(tilemap.width+1);
286 | let n = 0;
287 | for (let x = 0; x < tilemap.width; x++) {
288 | if (f(tilemap.get(x, y))) {
289 | n++;
290 | }
291 | row1[x+1] = row0[x+1] + n;
292 | }
293 | data[y+1] = row1;
294 | row0 = row1;
295 | }
296 | this.width = tilemap.width;
297 | this.height = tilemap.height;
298 | this._data = data;
299 | }
300 |
301 | get(x0: number, y0: number, x1: number, y1: number): number {
302 | let t: number;
303 | if (x1 < x0) {
304 | t = x0; x0 = x1; x1 = t;
305 | // assert(x0 <= x1);
306 | }
307 | if (y1 < y0) {
308 | t = y0; y0 = y1; y1 = t;
309 | // assert(y0 <= y1);
310 | }
311 | x0 = clamp(0, x0, this.width);
312 | y0 = clamp(0, y0, this.height);
313 | x1 = clamp(0, x1, this.width);
314 | y1 = clamp(0, y1, this.height);
315 | return (this._data[y1][x1] - this._data[y1][x0] -
316 | this._data[y0][x1] + this._data[y0][x0]);
317 | }
318 |
319 | exists(rect: Rect): boolean {
320 | return (this.get(rect.x, rect.y,
321 | rect.x+rect.width,
322 | rect.y+rect.height) !== 0);
323 | }
324 |
325 | }
326 |
--------------------------------------------------------------------------------
/base/sprite.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 |
5 | /** Abstract image obejct that is placed at (0, 0).
6 | * render() is responsible to draw the image.
7 | */
8 | interface Sprite {
9 |
10 | /** Returns the bounds of the image at (0, 0). */
11 | getBounds(): Rect;
12 |
13 | /** Renders this image in the given context. */
14 | render(ctx: CanvasRenderingContext2D): void;
15 | }
16 |
17 |
18 | /** Sprite that is a solid filled rectangle.
19 | * Typically used as placeholders.
20 | */
21 | class RectSprite implements Sprite {
22 |
23 | /** Fill color. */
24 | color: string;
25 | /** Destination rectangle. */
26 | dstRect: Rect;
27 |
28 | constructor(color: string, dstRect: Rect) {
29 | this.color = color;
30 | this.dstRect = dstRect;
31 | }
32 |
33 | /** Returns the bounds of the image at (0, 0). */
34 | getBounds(): Rect {
35 | return this.dstRect;
36 | }
37 |
38 | /** Renders this image in the given context. */
39 | render(ctx: CanvasRenderingContext2D) {
40 | if (this.color !== null) {
41 | ctx.fillStyle = this.color;
42 | fillRect(ctx, this.dstRect);
43 | }
44 | }
45 | }
46 |
47 |
48 | /** Sprite that is a solid filled oval.
49 | */
50 | class OvalSprite implements Sprite {
51 |
52 | /** Fill color. */
53 | color: string;
54 | /** Destination rectangle. */
55 | dstRect: Rect;
56 |
57 | constructor(color: string, dstRect: Rect) {
58 | this.color = color;
59 | this.dstRect = dstRect;
60 | }
61 |
62 | /** Returns the bounds of the image at (0, 0). */
63 | getBounds(): Rect {
64 | return this.dstRect;
65 | }
66 |
67 | /** Renders this image in the given context. */
68 | render(ctx: CanvasRenderingContext2D) {
69 | if (this.color !== null) {
70 | ctx.save();
71 | ctx.fillStyle = this.color;
72 | ctx.translate(this.dstRect.cx(), this.dstRect.cy());
73 | ctx.scale(this.dstRect.width/2, this.dstRect.height/2);
74 | ctx.beginPath();
75 | ctx.arc(0, 0, 1, 0, Math.PI*2);
76 | ctx.fill();
77 | ctx.restore();
78 | }
79 | }
80 | }
81 |
82 |
83 | /** Sprite that uses a canvas object.
84 | */
85 | class CanvasSprite implements Sprite {
86 |
87 | /** Source image. */
88 | canvas: HTMLCanvasElement;
89 | /** Destination rectangle. */
90 | dstRect: Rect;
91 | /** Source rectangle. */
92 | srcRect: Rect;
93 |
94 | constructor(canvas: HTMLCanvasElement, srcRect: Rect=null, dstRect: Rect=null) {
95 | this.canvas = canvas;
96 | if (srcRect === null) {
97 | srcRect = new Rect(0, 0, canvas.width, canvas.height);
98 | }
99 | this.srcRect = srcRect;
100 | if (dstRect === null) {
101 | dstRect = new Rect(-canvas.width/2, -canvas.height/2, canvas.width, canvas.height);
102 | }
103 | this.dstRect = dstRect;
104 | }
105 |
106 | /** Returns the bounds of the image at (0, 0). */
107 | getBounds(): Rect {
108 | return this.dstRect;
109 | }
110 |
111 | /** Renders this image in the given context. */
112 | render(ctx: CanvasRenderingContext2D) {
113 | ctx.drawImage(
114 | this.canvas,
115 | this.srcRect.x, this.srcRect.y,
116 | this.srcRect.width, this.srcRect.height,
117 | this.dstRect.x, this.dstRect.y,
118 | this.dstRect.width, this.dstRect.height);
119 | }
120 | }
121 |
122 |
123 | /** Sprite that uses a (part of) HTML element.
124 | */
125 | class ImageSprite implements Sprite {
126 |
127 | /** Source image. */
128 | image: HTMLImageElement;
129 | /** Source rectangle. */
130 | srcRect: Rect;
131 | /** Destination rectangle. */
132 | dstRect: Rect;
133 |
134 | constructor(image: HTMLImageElement, srcRect: Rect=null, dstRect: Rect=null) {
135 | this.image = image;
136 | if (srcRect === null) {
137 | srcRect = new Rect(0, 0, image.width, image.height);
138 | }
139 | if (dstRect === null) {
140 | dstRect = new Rect(-srcRect.width/2, -srcRect.height/2, srcRect.width, srcRect.height);
141 | }
142 | this.srcRect = srcRect;
143 | this.dstRect = dstRect;
144 | }
145 |
146 | /** Returns the bounds of the image at (0, 0). */
147 | getBounds(): Rect {
148 | return this.dstRect;
149 | }
150 |
151 | /** Renders this image in the given context. */
152 | render(ctx: CanvasRenderingContext2D) {
153 | ctx.drawImage(
154 | this.image,
155 | this.srcRect.x, this.srcRect.y,
156 | this.srcRect.width, this.srcRect.height,
157 | this.dstRect.x, this.dstRect.y,
158 | this.dstRect.width, this.dstRect.height);
159 | }
160 | }
161 |
162 |
163 | /** Sprite that consists of tiled images.
164 | * A image is displayed repeatedly to fill up the specified bounds.
165 | */
166 | class TiledSprite implements Sprite {
167 |
168 | /** Image source to be tiled. */
169 | sprite: Sprite;
170 | /** Bounds to fill. */
171 | bounds: Rect;
172 | /** Image offset. */
173 | offset: Vec2;
174 |
175 | constructor(sprite: Sprite, bounds: Rect, offset: Vec2=null) {
176 | this.sprite = sprite;
177 | this.bounds = bounds;
178 | this.offset = (offset !== null)? offset : new Vec2();
179 | }
180 |
181 | /** Returns the bounds of the image at a given pos. */
182 | getBounds(): Rect {
183 | return this.bounds;
184 | }
185 |
186 | /** Renders this image in the given context. */
187 | render(ctx: CanvasRenderingContext2D) {
188 | ctx.save();
189 | ctx.translate(int(this.bounds.x), int(this.bounds.y));
190 | ctx.beginPath();
191 | ctx.rect(0, 0, this.bounds.width, this.bounds.height);
192 | ctx.clip();
193 | let dstRect = this.sprite.getBounds();
194 | let w = dstRect.width;
195 | let h = dstRect.height;
196 | let dx0 = int(Math.floor(this.offset.x/w)*w - this.offset.x);
197 | let dy0 = int(Math.floor(this.offset.y/h)*h - this.offset.y);
198 | for (let dy = dy0; dy < this.bounds.height; dy += h) {
199 | for (let dx = dx0; dx < this.bounds.width; dx += w) {
200 | ctx.save();
201 | ctx.translate(dx, dy);
202 | this.sprite.render(ctx);
203 | ctx.restore();
204 | }
205 | }
206 | ctx.restore();
207 | }
208 | }
209 |
210 |
211 | /** Internal object that represents a star. */
212 | class Star {
213 | sprite: Sprite;
214 | z: number;
215 | s: number;
216 | p: Vec2;
217 | init(maxdepth: number) {
218 | this.z = Math.random()*maxdepth+1;
219 | this.s = (Math.random()*2+1) / this.z;
220 | }
221 | }
222 |
223 |
224 | /** Sprite for "star flowing" effects.
225 | * A image is scattered across the area with a varied depth.
226 | */
227 | class StarSprite implements Sprite {
228 |
229 | /** Bounds to fill. */
230 | bounds: Rect;
231 | /** Maximum depth of stars. */
232 | maxdepth: number;
233 | /** Image source to be used as a single star. */
234 | sprites: Sprite[];
235 |
236 | private _stars: Star[] = [];
237 |
238 | constructor(bounds: Rect, nstars: number,
239 | maxdepth=3, sprites: Sprite[]=null) {
240 | this.bounds = bounds
241 | this.maxdepth = maxdepth;
242 | if (sprites === null) {
243 | sprites = [new RectSprite('white', new Rect(0,0,1,1))];
244 | }
245 | this.sprites = sprites;
246 | for (let i = 0; i < nstars; i++) {
247 | let star = new Star();
248 | star.sprite = choice(sprites);
249 | star.init(this.maxdepth);
250 | star.p = this.bounds.rndPt();
251 | this._stars.push(star);
252 | }
253 | }
254 |
255 | /** Returns the bounds of the image at a given pos. */
256 | getBounds(): Rect {
257 | return this.bounds;
258 | }
259 |
260 | /** Renders this image in the given context. */
261 | render(ctx: CanvasRenderingContext2D) {
262 | for (let star of this._stars) {
263 | ctx.save();
264 | ctx.translate(star.p.x, star.p.y);
265 | ctx.scale(star.s, star.s);
266 | star.sprite.render(ctx);
267 | ctx.restore();
268 | }
269 | }
270 |
271 | /** Moves the stars by the given offset. */
272 | move(offset: Vec2) {
273 | for (let star of this._stars) {
274 | star.p.x += offset.x/star.z;
275 | star.p.y += offset.y/star.z;
276 | let rect = star.p.expand(star.s, star.s);
277 | if (!this.bounds.overlapsRect(rect)) {
278 | star.init(this.maxdepth);
279 | star.p = this.bounds.modPt(star.p);
280 | }
281 | }
282 | }
283 | }
284 |
285 |
286 | /** Object that stores multiple Sprite objects.
287 | * Each cell on the grid represents an individual Sprite.
288 | */
289 | class SpriteSheet {
290 |
291 | constructor() {
292 | }
293 |
294 | /** Returns an Sprite at the given cell. */
295 | get(x:number, y=0, w=1, h=1, origin: Vec2=null): Sprite {
296 | return null as Sprite;
297 | }
298 | }
299 |
300 |
301 | /** Array of Sprites.
302 | */
303 | class ArraySpriteSheet extends SpriteSheet {
304 |
305 | sprites: Sprite[];
306 |
307 | constructor(sprites: Sprite[]) {
308 | super();
309 | this.sprites = sprites;
310 | }
311 |
312 | /** Returns an Sprite at the given cell. */
313 | get(x:number, y=0, w=1, h=1, origin: Vec2=null): Sprite {
314 | if (x < 0 || this.sprites.length <= x || y != 0) return null;
315 | return this.sprites[x];
316 | }
317 |
318 | /** Sets an Sprite at the given cell. */
319 | set(i:number, sprite:Sprite) {
320 | this.sprites[i] = sprite;
321 | }
322 | }
323 |
324 |
325 | /** SpriteSheet that is based on a single HTML image.
326 | */
327 | class ImageSpriteSheet extends SpriteSheet {
328 |
329 | /** Source image. */
330 | image: HTMLImageElement;
331 | /** Size of each cell. */
332 | size: Vec2;
333 | /** Origin of each Sprite. */
334 | origin: Vec2;
335 |
336 | constructor(image: HTMLImageElement, size: Vec2, origin: Vec2=null) {
337 | super();
338 | this.image = image;
339 | this.size = size;
340 | this.origin = origin;
341 | }
342 |
343 | /** Returns an Sprite at the given cell. */
344 | get(x:number, y=0, w=1, h=1, origin: Vec2=null): Sprite {
345 | if (origin === null) {
346 | origin = this.origin;
347 | if (origin === null) {
348 | origin = new Vec2(w*this.size.x/2, h*this.size.y/2);
349 | }
350 | }
351 | let srcRect = new Rect(
352 | x*this.size.x, y*this.size.y,
353 | w*this.size.x, h*this.size.y);
354 | let dstRect = new Rect(
355 | -origin.x, -origin.y,
356 | w*this.size.x, h*this.size.y);
357 | return new ImageSprite(this.image, srcRect, dstRect);
358 | }
359 | }
360 |
361 |
362 | /** Renders sprites with the specific parameters.
363 | * @param ctx CanvasRenderingContext2D.
364 | * @param sprites Array of Sprites.
365 | * @param pos Position Vec2.
366 | * @param rotation Rotation.
367 | * @param scale Scale Vec2.
368 | * @param alpha Alpha.
369 | */
370 | function renderSprites(
371 | ctx: CanvasRenderingContext2D,
372 | sprites: Sprite[],
373 | pos: Vec2=null,
374 | rotation: number=0,
375 | scale: Vec2=null,
376 | alpha: number=1.0) {
377 | ctx.save();
378 | if (pos !== null) {
379 | ctx.translate(pos.x, pos.y);
380 | }
381 | if (rotation != 0) {
382 | ctx.rotate(rotation);
383 | }
384 | if (scale !== null) {
385 | ctx.scale(scale.x, scale.y);
386 | }
387 | ctx.globalAlpha = alpha;
388 | for (let sprite of sprites) {
389 | sprite.render(ctx);
390 | }
391 | ctx.restore();
392 | }
393 |
--------------------------------------------------------------------------------
/samples/platformer/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 | ///
9 | ///
10 | ///
11 |
12 | // Platformer
13 | //
14 | // An example of intermediate level using
15 | // basic physics and path finding.
16 | //
17 |
18 |
19 | // Initialize the resources.
20 | let SPRITES: SpriteSheet;
21 | enum S {
22 | PLAYER = 0,
23 | SHADOW = 1,
24 | THINGY = 2,
25 | YAY = 3,
26 | MONSTER = 4,
27 | };
28 | let TILES: SpriteSheet;
29 | enum T {
30 | BACKGROUND = 0,
31 | BLOCK = 1,
32 | LADDER = 2,
33 | THINGY = 3,
34 | ENEMY = 8,
35 | PLAYER = 9,
36 | }
37 | function main() {
38 | APP = new App(640, 480);
39 | SPRITES = new ImageSpriteSheet(
40 | APP.images['sprites'], new Vec2(32,32), new Vec2(16,16));
41 | TILES = new ImageSpriteSheet(
42 | APP.images['tiles'], new Vec2(48,48), new Vec2(0,16));
43 | APP.init(new Game());
44 | }
45 |
46 |
47 | function findShadowPos(tilemap: TileMap, pos: Vec2) {
48 | let rect = tilemap.coord2map(pos);
49 | let p = new Vec2(rect.x, rect.y);
50 | while (p.y < tilemap.height) {
51 | let c = tilemap.get(p.x, p.y+1);
52 | if (c == T.BLOCK || c == -1) break;
53 | p.y++;
54 | }
55 | let y = tilemap.map2coord(p).center().y;
56 | return new Vec2(0, y-pos.y);
57 | }
58 |
59 |
60 | // ShadowSprite
61 | //
62 | class ShadowSprite implements Sprite {
63 |
64 | shadow: ImageSprite;
65 | shadowPos: Vec2 = null;
66 |
67 | constructor() {
68 | this.shadow = SPRITES.get(S.SHADOW) as ImageSprite;
69 | }
70 |
71 | getBounds(): Rect {
72 | return this.shadow.getBounds();
73 | }
74 |
75 | render(ctx: CanvasRenderingContext2D) {
76 | let shadow = this.shadow;
77 | let pos = this.shadowPos;
78 | if (pos !== null) {
79 | ctx.save();
80 | ctx.translate(pos.x, pos.y);
81 | let srcRect = shadow.srcRect;
82 | let dstRect = shadow.dstRect;
83 | // Shadow gets smaller based on its ground distance.
84 | let d = pos.y/4;
85 | if (d*2 <= dstRect.width && d*2 <= dstRect.height) {
86 | ctx.drawImage(
87 | shadow.image,
88 | srcRect.x, srcRect.y, srcRect.width, srcRect.height,
89 | dstRect.x+d, dstRect.y+d*2,
90 | dstRect.width-d*2, dstRect.height-d*2);
91 | }
92 | ctx.restore();
93 | }
94 | }
95 | }
96 |
97 |
98 | // Player
99 | //
100 | class Player extends PlatformerEntity {
101 |
102 | shadow: ShadowSprite = new ShadowSprite();
103 | usermove: Vec2 = new Vec2();
104 | holding: boolean = true;
105 | picked: Signal;
106 |
107 | constructor(scene: Game, pos: Vec2) {
108 | super(scene.physics, scene.tilemap, pos);
109 | let sprite = SPRITES.get(S.PLAYER);
110 | this.sprites = [this.shadow, sprite];
111 | this.collider = sprite.getBounds();
112 | this.picked = new Signal(this);
113 | }
114 |
115 | onJumped() {
116 | super.onJumped();
117 | // Release a ladder when jumping.
118 | this.holding = false;
119 | }
120 |
121 | onLanded() {
122 | super.onLanded();
123 | // Grab a ladder when landed.
124 | this.holding = true;
125 | }
126 |
127 | hasLadder() {
128 | let range = this.getCollider().getAABB();
129 | return (this.tilemap.findTileByCoord(this.physics.isGrabbable, range) !== null);
130 | }
131 |
132 | canFall() {
133 | return !(this.holding && this.hasLadder());
134 | }
135 |
136 | getObstaclesFor(range: Rect, v: Vec2, context: string): Rect[] {
137 | if (!this.holding) {
138 | return this.tilemap.getTileRects(this.physics.isObstacle, range);
139 | }
140 | return super.getObstaclesFor(range, v, context);
141 | }
142 |
143 | onTick() {
144 | super.onTick();
145 | let v = this.usermove;
146 | if (!this.holding) {
147 | v = new Vec2(v.x, 0);
148 | } else if (!this.hasLadder()) {
149 | v = new Vec2(v.x, lowerbound(0, v.y));
150 | }
151 | v = this.getMove(v);
152 | this.pos = this.pos.add(v);
153 | this.shadow.shadowPos = findShadowPos(this.tilemap, this.pos);
154 | }
155 |
156 | setJump(jumpend: number) {
157 | super.setJump(jumpend);
158 | if (0 < jumpend && this.isJumping()) {
159 | APP.playSound('jump');
160 | }
161 | }
162 |
163 | setMove(v: Vec2) {
164 | this.usermove = v.scale(8);
165 | if (v.y != 0) {
166 | // Grab the ladder in air.
167 | this.holding = true;
168 | }
169 | }
170 |
171 | onCollided(entity: Entity) {
172 | super.onCollided(entity);
173 | if (entity instanceof Thingy) {
174 | APP.playSound('pick');
175 | entity.stop();
176 | let yay = new Particle(this.pos.move(0,-16));
177 | yay.sprites = [SPRITES.get(S.YAY)];
178 | yay.movement = new Vec2(0,-4);
179 | yay.lifetime = 0.5;
180 | this.world.add(yay);
181 | this.picked.fire();
182 | }
183 | }
184 | }
185 |
186 |
187 | // Monster
188 | //
189 | class Monster extends PlanningEntity {
190 |
191 | shadow: ShadowSprite = new ShadowSprite();
192 | target: Entity;
193 |
194 | constructor(scene: Game, pos: Vec2, target: Entity) {
195 | super(scene.physics, scene.tilemap, scene.grid, scene.caps,
196 | SPRITES.get(S.MONSTER).getBounds(), pos, 4);
197 | let sprite = SPRITES.get(S.MONSTER);
198 | this.sprites = [this.shadow, sprite];
199 | this.collider = sprite.getBounds();
200 | this.target = target;
201 | }
202 |
203 | onTick() {
204 | super.onTick();
205 | let goal = this.grid.coord2grid(this.target.pos);
206 | if (this.runner instanceof PlatformerActionRunner) {
207 | if (!this.runner.goal.equals(goal)) {
208 | // abandon an obsolete plan.
209 | this.setRunner(null);
210 | }
211 | }
212 | if (this.runner === null) {
213 | let action = this.buildPlan(goal);
214 | if (action !== null) {
215 | this.setRunner(new PlatformerActionRunner(this, action, goal));
216 | }
217 | }
218 | this.shadow.shadowPos = findShadowPos(this.tilemap, this.pos);
219 | }
220 |
221 | setAction(action: PlanAction) {
222 | super.setAction(action);
223 | if (action !== null && !(action instanceof NullAction)) {
224 | info("setAction: "+action);
225 | }
226 | }
227 | }
228 |
229 |
230 | // Thingy
231 | //
232 | class Thingy extends Entity {
233 |
234 | constructor(pos: Vec2) {
235 | super(pos);
236 | let sprite = SPRITES.get(S.THINGY);
237 | this.sprites = [sprite];
238 | this.collider = sprite.getBounds().inflate(-4, -4);
239 | }
240 | }
241 |
242 |
243 | // Game
244 | //
245 | class Game extends GameScene {
246 |
247 | physics: PhysicsConfig;
248 | tilemap: TileMap;
249 | grid: GridConfig;
250 | caps: PlatformerCaps;
251 | player: Player;
252 | thingies: number;
253 |
254 | debug: boolean = false;
255 | watch: PlanningEntity = null;
256 |
257 | onStart() {
258 | super.onStart();
259 | this.physics = new PhysicsConfig();
260 | this.physics.jumpfunc = ((vy:number, t:number) => {
261 | return (0 <= t && t <= 6)? -8 : vy+2;
262 | });
263 | this.physics.maxspeed = new Vec2(16, 16);
264 | this.physics.isObstacle =
265 | ((c:number) => { return c == T.BLOCK; });
266 | this.physics.isGrabbable =
267 | ((c:number) => { return c == T.LADDER; });
268 | this.physics.isStoppable =
269 | ((c:number) => { return c == T.BLOCK || c == T.LADDER; });
270 |
271 | const MAP = [
272 | "00000000000000300000",
273 | "00002111210001121100",
274 | "00112000200000020000",
275 | "00000000200000111211",
276 | "00300011111000000200",
277 | "00100300002000000200",
278 | "00000000002111121100",
279 | "00000110002000020000",
280 | "00000000002000020830",
281 | "00110002111000111111",
282 | "00000002000000002000",
283 | "11030111112110002003",
284 | "00010000002000112110",
285 | "31020100092000002000",
286 | "11111111111111111111",
287 | ];
288 | this.tilemap = new TileMap(32, 20, 15, MAP.map(
289 | (v:string) => { return str2array(v); }
290 | ));
291 | this.grid = new GridConfig(this.tilemap);
292 | this.caps = new PlatformerCaps(this.grid, this.physics, new Vec2(4, 4));
293 |
294 | // Place the player.
295 | let p = this.tilemap.findTile((c:number) => { return c == T.PLAYER; });
296 | this.player = new Player(this, this.tilemap.map2coord(p).center());
297 | this.player.fences = [this.world.area];
298 | this.player.picked.subscribe((entity:Entity) => {
299 | this.onPicked(entity);
300 | });
301 | this.add(this.player);
302 |
303 | // Place monsters and stuff.
304 | this.thingies = 0;
305 | this.tilemap.apply((x:number, y:number, c:number) => {
306 | let rect = this.tilemap.map2coord(new Vec2(x,y));
307 | switch (c) {
308 | case T.THINGY:
309 | let thingy = new Thingy(rect.center());
310 | this.add(thingy);
311 | this.thingies++;
312 | break;
313 | case T.ENEMY:
314 | let monster = new Monster(this, rect.center(), this.player);
315 | monster.fences = [this.world.area];
316 | this.add(monster);
317 | this.watch = monster;
318 | break;
319 | }
320 | return false;
321 | });
322 | }
323 |
324 | onTick() {
325 | super.onTick();
326 | this.world.setCenter(
327 | this.player.pos.expand(80,80),
328 | this.tilemap.bounds);
329 | }
330 |
331 | onDirChanged(v: Vec2) {
332 | this.player.setMove(v);
333 | }
334 | onButtonPressed(keysym: KeySym) {
335 | this.player.setJump(Infinity);
336 | }
337 | onButtonReleased(keysym: KeySym) {
338 | this.player.setJump(0);
339 | }
340 |
341 | onPicked(entity: Entity) {
342 | this.thingies--;
343 | if (this.thingies == 0) {
344 | let task = new Task();
345 | task.lifetime = 2;
346 | task.stopped.subscribe(() => {
347 | APP.lockKeys();
348 | this.changeScene(new Ending());
349 | });
350 | this.add(task);
351 | }
352 | }
353 |
354 | render(ctx: CanvasRenderingContext2D) {
355 | ctx.fillStyle = 'rgb(0,0,0)';
356 | fillRect(ctx, this.screen);
357 | // Render the background tiles.
358 | this.tilemap.renderWindowFromBottomLeft(
359 | ctx, this.world.window,
360 | (x,y,c) => {
361 | return (c != T.BLOCK)? TILES.get(T.BACKGROUND) : null;
362 | });
363 | // Render the map tiles.
364 | this.tilemap.renderWindowFromBottomLeft(
365 | ctx, this.world.window,
366 | (x,y,c) => {
367 | return (c == T.BLOCK || c == T.LADDER)? TILES.get(c) : null;
368 | });
369 | super.render(ctx);
370 | // Render the planmap.
371 | if (this.debug) {
372 | if (this.watch !== null && this.watch.runner !== null) {
373 | this.watch.planmap.render(ctx, this.grid);
374 | }
375 | }
376 | }
377 | }
378 |
379 |
380 | // Ending
381 | //
382 | class Ending extends HTMLScene {
383 |
384 | constructor() {
385 | let html = 'You Won! Yay!';
386 | super(html);
387 | }
388 |
389 | change() {
390 | this.changeScene(new Game());
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/base/pathfind.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 |
6 |
7 | // GridConfig
8 | //
9 | class GridConfig {
10 |
11 | tilemap: TileMap;
12 | gridsize: number;
13 | offset: number;
14 |
15 | constructor(tilemap:TileMap, resolution=1) {
16 | this.tilemap = tilemap;
17 | this.gridsize = tilemap.tilesize/resolution;
18 | this.offset = fmod(this.gridsize, tilemap.tilesize)/2;
19 | }
20 |
21 | coord2grid(p: Vec2) {
22 | return new Vec2(
23 | int((p.x-this.offset)/this.gridsize),
24 | int((p.y-this.offset)/this.gridsize));
25 | }
26 |
27 | grid2coord(p: Vec2) {
28 | return new Vec2(
29 | int((p.x+.5)*this.gridsize)+this.offset,
30 | int((p.y+.5)*this.gridsize)+this.offset);
31 | }
32 |
33 | clip(rect: Rect) {
34 | return this.tilemap.bounds.intersection(rect);
35 | }
36 | }
37 |
38 |
39 | // PlanActor
40 | //
41 | interface PlanActor {
42 | setAction(action: PlanAction): void;
43 | }
44 |
45 |
46 | // PlanAction
47 | //
48 | function getKey(x: number, y: number, context: string=null)
49 | {
50 | return (context === null)? (x+','+y) : (x+','+y+':'+context);
51 | }
52 |
53 | class PlanAction {
54 |
55 | p: Vec2;
56 | next: PlanAction;
57 | cost: number;
58 | context: string;
59 |
60 | constructor(p: Vec2,
61 | next: PlanAction=null,
62 | cost=0,
63 | context: string=null) {
64 | this.p = p.copy();
65 | this.next = next;
66 | this.cost = cost;
67 | this.context = context;
68 | }
69 |
70 | getKey() {
71 | return getKey(this.p.x, this.p.y, this.context);
72 | }
73 |
74 | getColor() {
75 | return (null as string);
76 | }
77 |
78 | getList() {
79 | let a: PlanAction[] = [];
80 | let action: PlanAction = this;
81 | while (action !== null) {
82 | a.push(action);
83 | action = action.next;
84 | }
85 | return a;
86 |
87 | }
88 |
89 | chain(next: PlanAction) {
90 | let action: PlanAction = this;
91 | while (true) {
92 | if (action.next === null) {
93 | action.next = next;
94 | break;
95 | }
96 | action = action.next;
97 | }
98 | return next;
99 | }
100 |
101 | toString() {
102 | return ('');
103 | }
104 | }
105 |
106 | class NullAction extends PlanAction {
107 | toString() {
108 | return ('');
109 | }
110 | }
111 |
112 |
113 | // PlanMap
114 | //
115 | class PlanActionEntry {
116 | action: PlanAction;
117 | total: number;
118 | constructor(action: PlanAction, total: number) {
119 | this.action = action;
120 | this.total = total;
121 | }
122 | }
123 | class PlanMap {
124 |
125 | private _map: { [index:string]: PlanAction } = {};
126 | private _queue: PlanActionEntry[] = [];
127 | private _goal: Vec2 = null; // for debugging
128 | private _start: Vec2 = null; // for debugging
129 |
130 | toString() {
131 | return ('');
132 | }
133 |
134 | addAction(start: Vec2, action: PlanAction) {
135 | let key = action.getKey();
136 | let prev = this._map[key];
137 | if (prev === undefined || action.cost < prev.cost) {
138 | this._map[key] = action;
139 | let dist = ((start === null)? Infinity :
140 | (Math.abs(start.x-action.p.x)+
141 | Math.abs(start.y-action.p.y)));
142 | this._queue.push(new PlanActionEntry(action, dist+action.cost));
143 | }
144 | }
145 |
146 | getAction(x: number, y: number, context: string=null) {
147 | let k = getKey(x, y, context);
148 | if (this._map.hasOwnProperty(k)) {
149 | return this._map[k];
150 | } else {
151 | return null;
152 | }
153 | }
154 |
155 | render(ctx: CanvasRenderingContext2D, grid: GridConfig) {
156 | let gs = grid.gridsize;
157 | let rs = gs/2;
158 | ctx.lineWidth = 1;
159 | for (let k in this._map) {
160 | let action = this._map[k];
161 | let color = action.getColor();
162 | if (color !== null) {
163 | let p0 = grid.grid2coord(action.p);
164 | ctx.strokeStyle = color;
165 | ctx.strokeRect(p0.x-rs/2+.5,
166 | p0.y-rs/2+.5,
167 | rs, rs);
168 | if (action.next !== null) {
169 | let p1 = grid.grid2coord(action.next.p);
170 | ctx.beginPath();
171 | ctx.moveTo(p0.x+.5, p0.y+.5);
172 | ctx.lineTo(p1.x+.5, p1.y+.5);
173 | ctx.stroke();
174 | }
175 | }
176 | }
177 | if (this._goal !== null) {
178 | let p = grid.grid2coord(this._goal);
179 | ctx.strokeStyle = '#00ff00';
180 | ctx.strokeRect(p.x-gs/2+.5,
181 | p.y-gs/2+.5,
182 | gs, gs);
183 | }
184 | if (this._start !== null) {
185 | let p = grid.grid2coord(this._start);
186 | ctx.strokeStyle = '#ff0000';
187 | ctx.strokeRect(p.x-gs/2+.5,
188 | p.y-gs/2+.5,
189 | gs, gs);
190 | }
191 | }
192 |
193 | build(actor: PlanActor, goal: Vec2, range: Rect,
194 | start: Vec2=null, maxcost=Infinity): PlanAction {
195 | //info("build: goal="+goal+", start="+start+", range="+range+", maxcost="+maxcost);
196 | this._map = {};
197 | this._queue = [];
198 | this._goal = goal;
199 | this._start = start;
200 | this.addAction(null, new NullAction(goal));
201 | while (0 < this._queue.length) {
202 | let entry = this._queue.shift();
203 | let action = entry.action;
204 | if (start !== null && start.equals(action.p)) return action;
205 | if (maxcost <= action.cost) continue;
206 | this.expand(actor, range, action, start);
207 | // A* search.
208 | if (start !== null) {
209 | this._queue.sort((a:PlanActionEntry,b:PlanActionEntry) => {
210 | return a.total-b.total;
211 | });
212 | }
213 | }
214 | return null;
215 | }
216 |
217 | expand(actor: PlanActor, range: Rect, prev: PlanAction,
218 | start: Vec2=null) {
219 | // [OVERRIDE]
220 | }
221 | }
222 |
223 |
224 | // ActionRunner
225 | //
226 | class ActionRunner extends Task {
227 |
228 | actor: PlanActor;
229 | action: PlanAction;
230 | timeout: number;
231 |
232 | constructor(actor: PlanActor, action: PlanAction, timeout=Infinity) {
233 | super();
234 | this.actor = actor;
235 | this.timeout = timeout;
236 |
237 | this.actor.setAction(action);
238 | this.action = action;
239 | this.lifetime = timeout;
240 | }
241 |
242 | toString() {
243 | return ('');
244 | }
245 |
246 | onTick() {
247 | super.onTick();
248 | let action = this.action;
249 | if (action !== null) {
250 | action = this.execute(action);
251 | if (action === null) {
252 | this.actor.setAction(action);
253 | this.stop();
254 | } else if (action !== this.action) {
255 | this.actor.setAction(action);
256 | this.lifetime = this.timeout;
257 | }
258 | this.action = action;
259 | }
260 | }
261 |
262 | execute(action: PlanAction): PlanAction {
263 | if (action instanceof NullAction) {
264 | return action.next;
265 | }
266 | return action;
267 | }
268 | }
269 |
270 |
271 | // WalkerActor
272 | //
273 | interface WalkerActor extends PlanActor {
274 | canMoveTo(p: Vec2): boolean;
275 | moveToward(p: Vec2): void;
276 | isCloseTo(p: Vec2): boolean;
277 | }
278 |
279 | // WalkerAction
280 | //
281 | class WalkerAction extends PlanAction {
282 | toString() {
283 | return ('');
284 | }
285 | getColor(): string { return null; }
286 | }
287 | class WalkerWalkAction extends WalkerAction {
288 | toString() {
289 | return ('');
290 | }
291 | getColor(): string { return 'white'; }
292 | }
293 |
294 | // WalkerPlanMap
295 | //
296 | class WalkerPlanMap extends PlanMap {
297 |
298 | grid: GridConfig;
299 | obstacle: RangeMap;
300 |
301 | constructor(grid: GridConfig, obstacle: RangeMap) {
302 | super();
303 | this.grid = grid;
304 | this.obstacle = obstacle;
305 | }
306 |
307 | expand(actor: WalkerActor, range: Rect,
308 | prev: WalkerAction, start: Vec2=null) {
309 | let p0 = prev.p;
310 | let cost0 = prev.cost;
311 | // assert(range.containsPt(p0));
312 |
313 | // try walking.
314 | for (let i = 0; i < 4; i++) {
315 | let d = new Vec2(1,0).rot90(i);
316 | let p1 = p0.add(d);
317 | if (range.containsPt(p1) &&
318 | actor.canMoveTo(p1)) {
319 | this.addAction(start, new WalkerWalkAction(
320 | p1, prev, cost0+1, null));
321 | }
322 | }
323 | }
324 | }
325 |
326 | // WalkerActionRunner
327 | //
328 | class WalkerActionRunner extends ActionRunner {
329 |
330 | goal: Vec2;
331 |
332 | constructor(actor: WalkerActor, action: PlanAction,
333 | goal: Vec2, timeout=Infinity) {
334 | super(actor, action, timeout);
335 | this.goal = goal;
336 | }
337 |
338 | execute(action: PlanAction): PlanAction {
339 | let actor = this.actor as WalkerActor;;
340 |
341 | if (action instanceof WalkerWalkAction) {
342 | let dst = action.next.p;
343 | actor.moveToward(dst);
344 | if (actor.isCloseTo(dst)) {
345 | return action.next;
346 | }
347 | }
348 | return super.execute(action);
349 | }
350 | }
351 |
352 |
353 | // WalkerEntity
354 | //
355 | class WalkerEntity extends TileMapEntity implements WalkerActor {
356 |
357 | grid: GridConfig;
358 | gridbox: Rect;
359 | planmap: WalkerPlanMap;
360 | allowance: number;
361 |
362 | runner: ActionRunner = null;
363 |
364 | constructor(grid: GridConfig, objmap: RangeMap,
365 | hitbox: Rect, pos: Vec2, allowance=0) {
366 | super(grid.tilemap, pos);
367 | this.grid = grid;
368 | let gs = grid.gridsize;
369 | this.gridbox = new Rect(
370 | 0, 0,
371 | Math.ceil(hitbox.width/gs)*gs,
372 | Math.ceil(hitbox.height/gs)*gs);
373 | this.planmap = new WalkerPlanMap(this.grid, objmap);
374 | this.allowance = (allowance !== 0)? allowance : grid.gridsize/2;
375 | }
376 |
377 | buildPlan(goal: Vec2, start: Vec2=null, size=0, maxcost=20) {
378 | start = (start !== null)? start : this.getGridPos();
379 | let range = (size == 0)? this.grid.tilemap.bounds : goal.inflate(size, size);
380 | range = this.grid.clip(range);
381 | return this.planmap.build(this, goal, range, start, maxcost) as WalkerAction;
382 | }
383 |
384 | setRunner(runner: ActionRunner) {
385 | if (this.runner !== null) {
386 | this.runner.stop();
387 | }
388 | this.runner = runner;
389 | if (this.runner !== null) {
390 | this.runner.stopped.subscribe(() => { this.runner = null; });
391 | this.parent.add(this.runner);
392 | }
393 | }
394 |
395 | setAction(action: PlanAction) {
396 | // [OVERRIDE]
397 | }
398 |
399 | // WalkerActor methods
400 |
401 | canMoveTo(p: Vec2) {
402 | let hitbox = this.getGridBoxAt(p);
403 | return !this.planmap.obstacle.exists(this.grid.tilemap.coord2map(hitbox));
404 | }
405 |
406 | moveToward(p: Vec2) {
407 | let p0 = this.pos;
408 | let p1 = this.getGridBoxAt(p).center();
409 | let v = p1.sub(p0);
410 | v = this.getMove(v);
411 | this.pos = this.pos.add(v);
412 | }
413 |
414 | isCloseTo(p: Vec2) {
415 | return this.grid.grid2coord(p).distance(this.pos) < this.allowance;
416 | }
417 |
418 | getGridPos() {
419 | return this.grid.coord2grid(this.pos);
420 | }
421 | getGridBoxAt(p: Vec2) {
422 | return this.grid.grid2coord(p).expand(this.gridbox.width, this.gridbox.height);
423 | }
424 | }
425 |
--------------------------------------------------------------------------------
/samples/scramble/src/game.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 | ///
5 | ///
6 | ///
7 | ///
8 |
9 | // Scramble
10 | //
11 | // Shooter + scrolling terrain. Mouse support.
12 | //
13 |
14 |
15 | // Initialize the resources.
16 | let FONT: Font;
17 | let SPRITES:ImageSpriteSheet;
18 | let TILES:SpriteSheet = new ArraySpriteSheet(
19 | [null, new RectSprite('red', new Rect(0,0,16,16))]);
20 | function main() {
21 | APP = new App(320, 240);
22 | FONT = new Font(APP.images['font'], 'white');
23 | SPRITES = new ImageSpriteSheet(
24 | APP.images['sprites'], new Vec2(16,16), new Vec2(8,8));
25 | APP.init(new Scramble());
26 | }
27 |
28 | function isTerrain(c:number) {
29 | return (c != 0);
30 | }
31 |
32 |
33 | // Explosion
34 | //
35 | class Explosion extends Entity {
36 | constructor(pos: Vec2) {
37 | super(pos);
38 | this.sprites = [SPRITES.get(1)];
39 | this.lifetime = 0.2;
40 | }
41 | }
42 |
43 |
44 | // Enemy
45 | //
46 | class Enemy extends Particle {
47 |
48 | killed: Signal;
49 |
50 | constructor(pos: Vec2) {
51 | super(pos);
52 | this.killed = new Signal(this);
53 | }
54 |
55 | getFrame() {
56 | return this.world.area;
57 | }
58 |
59 | onCollided(entity: Entity) {
60 | if (entity instanceof Bullet ||
61 | entity instanceof Bomb) {
62 | APP.playSound('explosion');
63 | this.chain(new Explosion(this.pos));
64 | this.stop();
65 | this.killed.fire();
66 | }
67 | }
68 | }
69 |
70 | // Enemy1
71 | class Enemy1 extends Enemy {
72 |
73 | constructor(pos: Vec2) {
74 | super(pos);
75 | let sprite = SPRITES.get(2);
76 | this.sprites = [sprite];
77 | this.collider = sprite.getBounds().inflate(-2,-2);
78 | this.movement = new Vec2(-rnd(1,8), rnd(3)-1);
79 | }
80 | }
81 |
82 | // Enemy2
83 | class Enemy2 extends Enemy {
84 |
85 | constructor(pos: Vec2) {
86 | super(pos);
87 | let sprite = SPRITES.get(3);
88 | this.sprites = [sprite];
89 | this.collider = sprite.getBounds().inflate(-2,-2);
90 | this.movement = new Vec2(-rnd(1,4), 0);
91 | }
92 |
93 | onTick() {
94 | super.onTick();
95 | // Move wiggly vertically.
96 | if (rnd(4) == 0) {
97 | this.movement.y = rnd(5)-2;
98 | }
99 | }
100 | }
101 |
102 | // Fuel
103 | class Fuel extends Enemy {
104 |
105 | constructor(pos: Vec2) {
106 | super(pos);
107 | let sprite = SPRITES.get(4);
108 | this.sprites = [sprite];
109 | this.collider = sprite.getBounds();
110 | }
111 | }
112 |
113 | // Missile
114 | class Missile extends Enemy {
115 |
116 | threshold: number;
117 |
118 | constructor(pos: Vec2, threshold: number) {
119 | super(pos);
120 | let sprite = SPRITES.get(5);
121 | this.sprites = [sprite];
122 | this.collider = sprite.getBounds();
123 | this.threshold = threshold;
124 | }
125 |
126 | onTick() {
127 | super.onTick();
128 | if (this.pos.x < this.threshold) {
129 | this.movement = new Vec2(0,-4);
130 | }
131 | }
132 | }
133 |
134 |
135 | // FlyingEntity
136 | //
137 | class FlyingEntity extends Entity {
138 |
139 | tilemap: TileMap;
140 |
141 | constructor(tilemap: TileMap, pos: Vec2) {
142 | super(pos);
143 | this.tilemap = tilemap;
144 | }
145 |
146 | onTick() {
147 | super.onTick();
148 | let range = this.getCollider().getAABB();
149 | if (this.tilemap.findTileByCoord(isTerrain, range) !== null) {
150 | this.onTerrainCollided();
151 | }
152 | }
153 |
154 | onTerrainCollided() {
155 | // [OVERRIDE]
156 | this.stop();
157 | }
158 | }
159 |
160 |
161 | // Bullet
162 | //
163 | class Bullet extends FlyingEntity {
164 |
165 | bounds = new Rect(-4, -1, 8, 2);
166 | movement = new Vec2(8, 0);
167 |
168 | constructor(tilemap: TileMap, pos: Vec2) {
169 | super(tilemap, pos);
170 | this.sprites = [new RectSprite('white', this.bounds)];
171 | this.collider = this.bounds;
172 | }
173 |
174 | onTick() {
175 | super.onTick();
176 | this.pos = this.pos.add(this.movement);
177 | let collider = this.getCollider();
178 | if (!collider.overlaps(this.world.area)) {
179 | this.stop();
180 | }
181 | }
182 | }
183 |
184 |
185 | // Bomb
186 | //
187 | class Bomb extends FlyingEntity {
188 |
189 | bounds = new Rect(-3, -2, 6, 4);
190 | movement: Vec2;
191 |
192 | constructor(tilemap: TileMap, pos: Vec2) {
193 | super(tilemap, pos);
194 | this.sprites = [new RectSprite('cyan', this.bounds)];
195 | this.collider = this.bounds;
196 | this.movement = new Vec2(2, 0);
197 | }
198 |
199 | onTick() {
200 | super.onTick();
201 | this.pos = this.pos.add(this.movement);
202 | this.movement.y = upperbound(6, this.movement.y+1);
203 | let collider = this.getCollider();
204 | if (!collider.overlaps(this.world.area)) {
205 | this.stop();
206 | }
207 | }
208 | }
209 |
210 |
211 | // Player
212 | //
213 | class Player extends FlyingEntity {
214 |
215 | usermove: Vec2 = new Vec2();
216 | goalpos: Vec2 = null;
217 | firing: boolean = false;
218 | droping: boolean = false;
219 | nextfire: number = 0; // Firing counter
220 | nextdrop: number = 0; // Droping counter
221 |
222 | constructor(tilemap: TileMap, pos: Vec2) {
223 | super(tilemap, pos);
224 | let sprite = SPRITES.get(0);
225 | this.sprites = [sprite];
226 | this.collider = sprite.getBounds().inflate(-2,-2);
227 | }
228 |
229 | onTick() {
230 | super.onTick();
231 | let v = this.usermove;
232 | if (this.goalpos !== null) {
233 | v = this.goalpos.sub(this.pos);
234 | if (Math.abs(v.x) < 8) { v.x = 0; }
235 | if (Math.abs(v.y) < 8) { v.y = 0; }
236 | }
237 | // Disallows diagonal move.
238 | v = v.sign().scale(4);
239 | // Restrict its position within the screen.
240 | v = this.getMove(v);
241 | this.pos = this.pos.add(v);
242 | if (this.firing) {
243 | if (this.nextfire == 0) {
244 | // Fire a bullet at a certain interval.
245 | let bullet = new Bullet(this.tilemap, this.pos);
246 | this.world.add(bullet);
247 | APP.playSound('pew');
248 | this.nextfire = 5;
249 | }
250 | this.nextfire--;
251 | }
252 | if (this.droping) {
253 | if (this.nextdrop == 0) {
254 | // Drop a bomb at a certain interval.
255 | let bomb = new Bomb(this.tilemap, this.pos);
256 | this.world.add(bomb);
257 | APP.playSound('bomb');
258 | this.nextdrop = 10;
259 | }
260 | this.nextdrop--;
261 | }
262 | }
263 |
264 | setFire(firing: boolean) {
265 | this.firing = firing;
266 | if (!this.firing) {
267 | // Reset the counter when start shooting.
268 | this.nextfire = 0;
269 | }
270 | }
271 |
272 | setDrop(droping: boolean) {
273 | this.droping = droping;
274 | if (!this.droping) {
275 | // Reset the counter when start droping.
276 | this.nextdrop = 0;
277 | }
278 | }
279 |
280 | setMove(v: Vec2) {
281 | this.usermove = v;
282 | this.goalpos = null;
283 | }
284 | setGoal(p: Vec2) {
285 | this.goalpos = p.copy();
286 | }
287 |
288 | onCollided(entity: Entity) {
289 | if (entity instanceof Enemy) {
290 | APP.playSound('explosion');
291 | this.chain(new Explosion(this.pos));
292 | this.stop();
293 | }
294 | }
295 |
296 | onTerrainCollided() {
297 | APP.playSound('explosion');
298 | this.chain(new Explosion(this.pos));
299 | this.stop();
300 | }
301 | }
302 |
303 |
304 | // Scramble
305 | //
306 | class Scramble extends GameScene {
307 |
308 | tilesize: number = 16;
309 | scoreBox: TextBox;
310 | player: Player;
311 | terrain: TileMap;
312 |
313 | tx: number;
314 | theight: number;
315 | speed: number;
316 | spawning: number;
317 | score: number;
318 |
319 | constructor() {
320 | super();
321 | this.scoreBox = new TextBox(this.screen.inflate(-8,-8), FONT);
322 | }
323 |
324 | onStart() {
325 | super.onStart();
326 | this.world.area = new Rect(
327 | 0, 0, this.screen.width+this.tilesize, this.screen.height);
328 | this.terrain = new TileMap(
329 | this.tilesize,
330 | 1+int(this.world.area.width/this.tilesize),
331 | int(this.world.area.height/this.tilesize));
332 | this.player = new Player(this.terrain, this.world.area.center());
333 | this.player.fences = [this.world.area];
334 | let task = new Task();
335 | task.lifetime = 2;
336 | task.stopped.subscribe(() => { this.reset(); });
337 | this.player.chain(task);
338 | this.add(this.player);
339 | this.tx = 0;
340 | this.theight = 1;
341 | this.speed = 1;
342 | this.spawning = 0;
343 | this.score = 0;
344 | this.updateScore();
345 | }
346 |
347 | onTick() {
348 | super.onTick();
349 | this.player.pos = this.player.pos.add(new Vec2(this.speed, 0));
350 | this.world.moveAll(new Vec2(-this.speed, 0));
351 | this.tx += this.speed;
352 | let d = int(this.tx/this.tilesize);
353 | if (0 < d) {
354 | let dx = -d*this.tilesize;
355 | this.tx += dx;
356 | this.terrain.shift(-d, 0);
357 | for (let x = this.terrain.width-d; x < this.terrain.width; x++) {
358 | this.addTerrain(x);
359 | }
360 | }
361 | if (this.spawning == 0) {
362 | this.spawnEnemy();
363 | }
364 | this.spawning--;
365 | }
366 |
367 | onButtonPressed(keysym: KeySym) {
368 | switch (keysym) {
369 | case KeySym.Action1:
370 | this.player.setFire(true);
371 | break;
372 | case KeySym.Action2:
373 | this.player.setDrop(true);
374 | break;
375 | }
376 | }
377 | onButtonReleased(keysym: KeySym) {
378 | switch (keysym) {
379 | case KeySym.Action1:
380 | this.player.setFire(false);
381 | break;
382 | case KeySym.Action2:
383 | this.player.setDrop(false);
384 | break;
385 | }
386 | }
387 | onDirChanged(v: Vec2) {
388 | this.player.setMove(v);
389 | }
390 |
391 | onMouseDown(p: Vec2, button: number) {
392 | super.onMouseDown(p, button);
393 | this.player.setFire(true);
394 | this.player.setDrop(true);
395 | }
396 | onMouseUp(p: Vec2, button: number) {
397 | super.onMouseUp(p, button);
398 | this.player.setFire(false);
399 | this.player.setDrop(false);
400 | }
401 | onMouseMove(p: Vec2) {
402 | this.player.setGoal(p);
403 | }
404 |
405 | updateScore() {
406 | this.scoreBox.clear();
407 | this.scoreBox.putText(['SCORE:'+format(this.score)]);
408 | }
409 |
410 | addTerrain(x: number) {
411 | let y = this.terrain.height-this.theight;
412 | this.terrain.fill(1, new Rect(x, y, 1, this.theight));
413 | this.theight = clamp(1, this.theight+rnd(3)-1, 10);
414 | let rect = this.terrain.map2coord(new Vec2(x, y-1));
415 | let enemy: Enemy = null;
416 | switch (rnd(4)) {
417 | case 1:
418 | enemy = new Fuel(rect.center());
419 | break;
420 | case 2:
421 | enemy = new Missile(rect.center(), rnd(32, this.world.area.width-32));
422 | break;
423 | }
424 | if (enemy !== null) {
425 | enemy.killed.subscribe(() => { this.score++; this.updateScore(); });
426 | this.add(enemy);
427 | }
428 | }
429 |
430 | spawnEnemy() {
431 | let area = this.world.area;
432 | let pos = new Vec2(area.width, rnd(area.height));
433 | let enemy: Enemy;
434 | switch (rnd(2)) {
435 | case 1:
436 | enemy = new Enemy1(pos);
437 | break;
438 | default:
439 | enemy = new Enemy2(pos);
440 | break;
441 | }
442 | enemy.killed.subscribe(() => { this.score++; this.updateScore(); });
443 | this.add(enemy);
444 | this.spawning = 10+rnd(20);
445 | }
446 |
447 | render(ctx: CanvasRenderingContext2D) {
448 | ctx.fillStyle = 'rgb(0,0,32)';
449 | fillRect(ctx, this.screen);
450 | let dx = this.tx % this.tilesize;
451 | ctx.save();
452 | ctx.translate(-dx, 0);
453 | this.terrain.render(ctx, TILES);
454 | ctx.restore();
455 | super.render(ctx);
456 | this.scoreBox.render(ctx);
457 | }
458 | }
459 |
--------------------------------------------------------------------------------