├── 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 | Euskit Help 3 | 4 | 10 | -------------------------------------------------------------------------------- /skel/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=beep.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/adventure/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS= 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/pong/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=beep.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/maze/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=beep.mp3 goal.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/racing/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=music.mp3 plunge.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/shooter/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=pew.mp3 explosion.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/pseudo3d/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=jump.mp3 pick.mp3 bomb.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/platformer/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=jump.mp3 pick.mp3 ending.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /samples/scramble/assets/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for assets 2 | 3 | PYTHON=python 4 | SOX=sox 5 | LAME=lame -t --cbr 6 | 7 | AUDIOS=pew.mp3 bomb.mp3 explosion.mp3 8 | 9 | all: $(AUDIOS) 10 | 11 | clean: 12 | -$(RM) $(AUDIOS) 13 | 14 | .SUFFIXES: .png .wav .mp3 15 | 16 | .wav.mp3: 17 | $(SOX) $< -t wav - pad 0 0.5 | $(LAME) - $@ 18 | -------------------------------------------------------------------------------- /skel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/maze/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/pong/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/adventure/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/pseudo3d/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/racing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/scramble/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/shooter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/platformer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "alwaysStrict": true, 5 | "noEmitOnError": true, 6 | "noFallthroughCasesInSwitch": true, 7 | "noImplicitAny": true, 8 | "noImplicitReturns": true, 9 | "noImplicitThis": true, 10 | "outFile": "js/game.js" 11 | }, 12 | 13 | "files": [ 14 | "src/game.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /samples/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | all: 4 | cd pong; $(MAKE) $@ 5 | cd shooter; $(MAKE) $@ 6 | cd racing; $(MAKE) $@ 7 | cd maze; $(MAKE) $@ 8 | cd platformer; $(MAKE) $@ 9 | cd pseudo3d; $(MAKE) $@ 10 | cd adventure; $(MAKE) $@ 11 | cd scramble; $(MAKE) $@ 12 | 13 | clean: 14 | -cd pong; $(MAKE) $@ 15 | -cd shooter; $(MAKE) $@ 16 | -cd racing; $(MAKE) $@ 17 | -cd maze; $(MAKE) $@ 18 | -cd platformer; $(MAKE) $@ 19 | -cd pseudo3d; $(MAKE) $@ 20 | -cd adventure; $(MAKE) $@ 21 | -cd scramble; $(MAKE) $@ 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TYPEDOC=typedoc 4 | 5 | BASEDIR=../base 6 | BASES= \ 7 | $(BASEDIR)/animation.ts \ 8 | $(BASEDIR)/app.ts \ 9 | $(BASEDIR)/entity.ts \ 10 | $(BASEDIR)/geom.ts \ 11 | $(BASEDIR)/sprite.ts \ 12 | $(BASEDIR)/pathfind.ts \ 13 | $(BASEDIR)/planplat.ts \ 14 | $(BASEDIR)/scene.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | OUTDIR=api 19 | 20 | all: $(OUTDIR) 21 | 22 | clean: 23 | -$(RM) -r $(OUTDIR) 24 | 25 | $(OUTDIR): $(BASES) 26 | $(TYPEDOC) --out $(OUTDIR) $(BASES) 27 | -------------------------------------------------------------------------------- /samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Samples 4 | 5 |

Samples

6 | 16 | 17 | -------------------------------------------------------------------------------- /docs/samples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Samples 4 | 5 |

Samples

6 | 16 | 17 | -------------------------------------------------------------------------------- /samples/maze/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/pong/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/racing/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/shooter/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/adventure/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/platformer/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/pseudo3d/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/scramble/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | BASEDIR=../../base 5 | BASES= \ 6 | $(BASEDIR)/animation.ts \ 7 | $(BASEDIR)/app.ts \ 8 | $(BASEDIR)/entity.ts \ 9 | $(BASEDIR)/geom.ts \ 10 | $(BASEDIR)/sprite.ts \ 11 | $(BASEDIR)/pathfind.ts \ 12 | $(BASEDIR)/planplat.ts \ 13 | $(BASEDIR)/scene.ts \ 14 | $(BASEDIR)/task.ts \ 15 | $(BASEDIR)/text.ts \ 16 | $(BASEDIR)/tilemap.ts \ 17 | $(BASEDIR)/utils.ts 18 | 19 | all: js/game.js 20 | cd assets; $(MAKE) $@ 21 | 22 | clean: 23 | # -cd assets; $(MAKE) $@ 24 | -$(RM) -r js 25 | 26 | js/game.js: $(BASES) src/game.ts 27 | $(TSC) 28 | -------------------------------------------------------------------------------- /samples/pong/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pong 6 | 7 | 8 |

Pong

9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /docs/samples/pong/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pong 6 | 7 | 8 |

Pong

9 |
10 |
11 |
12 |
13 | 14 |
15 | 16 | -------------------------------------------------------------------------------- /skel/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | RM=rm -f 3 | TSC=tsc 4 | PYTHON=python 5 | RSYNC=rsync -auvz 6 | WATCHER=$(PYTHON) tools/watcher.py 7 | 8 | BASEDIR=../base 9 | BASES= \ 10 | $(BASEDIR)/animation.ts \ 11 | $(BASEDIR)/app.ts \ 12 | $(BASEDIR)/entity.ts \ 13 | $(BASEDIR)/geom.ts \ 14 | $(BASEDIR)/sprite.ts \ 15 | $(BASEDIR)/pathfind.ts \ 16 | $(BASEDIR)/planplat.ts \ 17 | $(BASEDIR)/scene.ts \ 18 | $(BASEDIR)/task.ts \ 19 | $(BASEDIR)/text.ts \ 20 | $(BASEDIR)/tilemap.ts \ 21 | $(BASEDIR)/utils.ts 22 | 23 | REMOTE_DIR=yourhost.example.com:public_html/game 24 | 25 | all: js/game.js 26 | cd assets; $(MAKE) $@ 27 | 28 | clean: 29 | # -cd assets; $(MAKE) $@ 30 | -$(RM) -r js 31 | 32 | watch: 33 | $(WATCHER) $(BASES) src/game.ts 34 | 35 | upload: all 36 | $(RSYNC) --exclude '.*' --exclude '*.wav' --exclude Makefile index.html js assets $(REMOTE_DIR) 37 | 38 | js/game.js: $(BASES) src/game.ts 39 | $(TSC) 40 | -------------------------------------------------------------------------------- /samples/adventure/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adventure 7 | 14 | 15 | 16 |

Adventure

17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /docs/samples/adventure/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Adventure 7 | 14 | 15 | 16 |

Adventure

17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /skel/embed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | @@NAME@@ 8 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 |
24 | 25 | -------------------------------------------------------------------------------- /samples/maze/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Maze 7 | 15 | 16 | 17 |

Maze

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /tools/doit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # image manipulation 4 | 5 | import sys 6 | import pygame 7 | 8 | def main(argv): 9 | import getopt 10 | def usage(): 11 | print ('usage: %s [-d] [-o output] [file ...]' % argv[0]) 12 | return 100 13 | try: 14 | (opts, args) = getopt.getopt(argv[1:], 'do:') 15 | except getopt.GetoptError: 16 | return usage() 17 | debug = 0 18 | output = 'out.png' 19 | for (k, v) in opts: 20 | if k == '-d': debug += 1 21 | elif k == '-o': output = v 22 | # 23 | path = args.pop(0) 24 | img = pygame.image.load(path) 25 | (width,height) = img.get_size() 26 | for y in xrange(height): 27 | for x in xrange(width): 28 | c = img.get_at((x,y)) 29 | if c == (0,0,0): 30 | c = (255,255,255) 31 | img.set_at((x,y), c) 32 | pygame.image.save(img, output) 33 | return 0 34 | 35 | if __name__ == '__main__': sys.exit(main(sys.argv)) 36 | -------------------------------------------------------------------------------- /docs/samples/maze/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Maze 7 | 15 | 16 | 17 |

Maze

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /samples/racing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Racing 7 | 15 | 16 | 17 |

Racing

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /samples/shooter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shooter 7 | 15 | 16 | 17 |

Shooter

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /docs/samples/racing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Racing 7 | 15 | 16 | 17 |

Racing

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /docs/samples/shooter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Shooter 7 | 15 | 16 | 17 |

Shooter

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /skel/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @@NAME@@ 7 | 15 | 16 | 17 |

@@NAME@@

18 |
19 |

@@JAM@@, @@DATE@@, @@THEME@@ 20 |

21 |
22 |
Made with Euskit.
23 |
24 | 25 | 26 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /samples/scramble/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scramble 7 | 14 | 15 | 16 |

Scramble

17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /docs/samples/scramble/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Scramble 7 | 14 | 15 | 16 |

Scramble

17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-2021 Yusuke Shinyama 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or 10 | sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 18 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 19 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 20 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 21 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 23 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /samples/pseudo3d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pseudo3d 7 | 15 | 16 | 17 |

Pseudo3d

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /docs/samples/pseudo3d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Pseudo3d 7 | 15 | 16 | 17 |

Pseudo3d

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /samples/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Platformer 7 | 15 | 16 | 17 |

Platformer

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /docs/samples/platformer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Platformer 7 | 15 | 16 | 17 |

Platformer

18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | -------------------------------------------------------------------------------- /tools/mktiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # usage: mktiles.py [-o output] gridsize w h 4 | 5 | import sys 6 | import pygame 7 | 8 | def main(argv): 9 | import getopt 10 | def usage(): 11 | print ('usage: %s [-o output] gridsize width [height]' % argv[0]) 12 | return 100 13 | try: 14 | (opts, args) = getopt.getopt(argv[1:], 'o:') 15 | except getopt.GetoptError: 16 | return usage() 17 | output = 'out.png' 18 | for (k, v) in opts: 19 | if k == '-o': output = v 20 | # 21 | if len(args) < 2: return usage() 22 | gridsize = int(args.pop(0)) 23 | width = int(args.pop(0)) 24 | height = 1 25 | if args: 26 | height = int(args.pop(0)) 27 | color1 = (255,255,255) 28 | color2 = (200,200,200) 29 | img = pygame.Surface((width*gridsize, height*gridsize), 24) 30 | for y in xrange(height): 31 | for x in xrange(width): 32 | if (x+y)%2 == 0: 33 | c = color1 34 | else: 35 | c = color2 36 | img.fill(c, (x*gridsize, y*gridsize, gridsize, gridsize)) 37 | pygame.image.save(img, output) 38 | return 0 39 | 40 | if __name__ == '__main__': sys.exit(main(sys.argv)) 41 | -------------------------------------------------------------------------------- /tools/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # setup.sh - project initialization script. 3 | 4 | # "strict mode" 5 | set -euo pipefail 6 | IFS=$'\n\t' 7 | 8 | # show the usage. 9 | [ 1 = $# ] || ( echo "usage: $0 destdir" && exit 111 ) 10 | 11 | # destdir. 12 | dst="$1" 13 | # dirname of the script. 14 | dirname=${0%/*} 15 | basedir="$dirname/.." 16 | mkdir "$dst" || : # ignore errors 17 | [ -d "$dst" ] || ( echo "directory not exist: $dst" && exit 1 ) 18 | 19 | echo "basedir: $basedir" 20 | echo "setting up for: $dst" 21 | 22 | # create .rsyn 23 | mkdir "$dst"/src || : # ignore errors 24 | mkdir "$dst"/base || : # ignore errors 25 | mkdir "$dst"/assets || : # ignore errors 26 | mkdir "$dst"/tools || : # ignore errors 27 | cp "$basedir"/base/*.ts "$dst"/base 28 | cp "$basedir"/tools/*.py "$dst"/tools 29 | cp "$basedir"/skel/.gitignore "$dst" 30 | sed 's+^BASEDIR=../base+BASEDIR=./base+' \ 31 | "$basedir"/skel/Makefile > "$dst"/Makefile 32 | cp "$basedir"/skel/index.html "$dst" 33 | cp "$basedir"/skel/tsconfig.json "$dst" 34 | cp "$basedir"/skel/assets/.gitignore "$dst"/assets 35 | cp "$basedir"/skel/assets/* "$dst"/assets 36 | sed 's+^/// "$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 Pong (Code) 41 | * Shoter Shooter (Code) 42 | * Racing Racing (Code) 43 | * Maze Maze (Code) 44 | * Platformer Platformer (Code) 45 | * Pseudo3d Pseudo3d (Code) 46 | * Adventure Adventure (Code) 47 | * Scramble 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 |

Euskit User Guide

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 |
  1. Install Node.js and TypeScript. 34 |
     35 | > npm install -g typescript
     36 | 
    37 | 38 |
  2. 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 |
  3. Run tsc at the working directory. 53 |
     54 | > tsc
     55 | 
    56 | 57 |
  4. 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 |

Euskit Cheat Sheet

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;  // Current direction.
 38 |     constructor(pos: Vec2) {
 39 | 	super(pos);
 40 |         // Configure the Player's sprite and collider.
 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 |         // Initialize the direction.
 48 | 	this.usermove = new Vec2();
 49 |     }
 50 |     onTick() {
 51 | 	super.onTick();
 52 |         // Change the position.
 53 | 	this.moveIfPossible(this.usermove);
 54 |     }
 55 | }
 56 | 
 57 | class Game extends GameScene {
 58 |     player: Player;
 59 |     onStart() {
 60 | 	super.onStart();
 61 |         // Place a player at the center of the world.
 62 | 	this.player = new Player(this.world.area.center());
 63 | 	this.world.add(this.player);
 64 |     }
 65 |     onDirChanged(v: Vec2) {
 66 |         // When the input changed, change the player's direction.
 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 |         // Block input for a second.
139 |         APP.lockKeys();
140 |         // Transition to another scene object.
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 | // this.world.add(dialog);
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 | // this.world.add(dialog);
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 | --------------------------------------------------------------------------------