├── .gitignore
├── logo.png
├── misc
├── cal.ase
├── cal2.ase
├── fork.jpg
├── timer.ase
├── engine.ase
└── goodnews.jpg
├── src
├── sounds
│ ├── cool.mp3
│ ├── scream.mp3
│ └── index.d.ts
├── sprites
│ ├── ken.ase
│ ├── ken.png
│ ├── heart.png
│ ├── logo.ase
│ ├── logo.png
│ ├── timer.png
│ └── index.d.ts
├── fonts
│ ├── apl386.woff2
│ └── index.d.ts
├── confetti.ts
└── kaboomware.ts
├── games
└── tga
│ ├── eat
│ ├── assets
│ │ ├── sounds
│ │ │ └── walk.mp3
│ │ └── sprites
│ │ │ ├── bao.ase
│ │ │ ├── bao.png
│ │ │ ├── cactus.ase
│ │ │ ├── cactus.png
│ │ │ ├── fire.png
│ │ │ ├── fish.ase
│ │ │ ├── fish.png
│ │ │ ├── grass.ase
│ │ │ ├── grass.png
│ │ │ ├── fire.json
│ │ │ ├── cactus.json
│ │ │ ├── bao.json
│ │ │ └── fish.json
│ └── game.ts
│ ├── dodge
│ ├── assets
│ │ └── sprites
│ │ │ ├── bang.ase
│ │ │ ├── bang.png
│ │ │ ├── dodge.ase
│ │ │ ├── meteor.ase
│ │ │ ├── meteor.png
│ │ │ ├── stickman.ase
│ │ │ ├── stickman.png
│ │ │ ├── bang.json
│ │ │ ├── stickman.json
│ │ │ └── meteor.json
│ └── game.ts
│ ├── snipe
│ ├── assets
│ │ ├── sounds
│ │ │ └── shoot.mp3
│ │ └── sprites
│ │ │ ├── barney.ase
│ │ │ ├── barney.png
│ │ │ ├── cactus.ase
│ │ │ ├── cactus.png
│ │ │ ├── desert.ase
│ │ │ └── desert.png
│ └── game.ts
│ └── squeeze
│ ├── assets
│ ├── sounds
│ │ ├── fly.mp3
│ │ └── squeeze.mp3
│ └── sprites
│ │ ├── fly.ase
│ │ ├── fly.png
│ │ ├── hand.ase
│ │ ├── hand.png
│ │ ├── wall.ase
│ │ ├── wall.png
│ │ ├── blood.ase
│ │ ├── blood.png
│ │ ├── fly.json
│ │ ├── hand.json
│ │ └── blood.json
│ └── game.ts
├── tsconfig.json
├── scripts
├── build.js
├── create.js
├── dev.js
└── play.js
├── package.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | dist/
4 | .tmp/
5 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/logo.png
--------------------------------------------------------------------------------
/misc/cal.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/cal.ase
--------------------------------------------------------------------------------
/misc/cal2.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/cal2.ase
--------------------------------------------------------------------------------
/misc/fork.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/fork.jpg
--------------------------------------------------------------------------------
/misc/timer.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/timer.ase
--------------------------------------------------------------------------------
/misc/engine.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/engine.ase
--------------------------------------------------------------------------------
/misc/goodnews.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/misc/goodnews.jpg
--------------------------------------------------------------------------------
/src/sounds/cool.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sounds/cool.mp3
--------------------------------------------------------------------------------
/src/sprites/ken.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/ken.ase
--------------------------------------------------------------------------------
/src/sprites/ken.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/ken.png
--------------------------------------------------------------------------------
/src/sounds/scream.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sounds/scream.mp3
--------------------------------------------------------------------------------
/src/sprites/heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/heart.png
--------------------------------------------------------------------------------
/src/sprites/logo.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/logo.ase
--------------------------------------------------------------------------------
/src/sprites/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/logo.png
--------------------------------------------------------------------------------
/src/sprites/timer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/sprites/timer.png
--------------------------------------------------------------------------------
/src/fonts/apl386.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/src/fonts/apl386.woff2
--------------------------------------------------------------------------------
/src/sprites/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.png" {
2 | const value: string
3 | export default value
4 | }
5 |
--------------------------------------------------------------------------------
/src/sounds/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.mp3" {
2 | const value: Uint8Array
3 | export default value
4 | }
5 |
--------------------------------------------------------------------------------
/src/fonts/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.woff2" {
2 | const value: Uint8Array
3 | export default value
4 | }
5 |
6 |
--------------------------------------------------------------------------------
/games/tga/eat/assets/sounds/walk.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sounds/walk.mp3
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/bao.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/bao.ase
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/bao.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/bao.png
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/bang.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/bang.ase
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/bang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/bang.png
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/cactus.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/cactus.ase
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/cactus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/cactus.png
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/fire.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/fire.png
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/fish.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/fish.ase
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/fish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/fish.png
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/grass.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/grass.ase
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/grass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/eat/assets/sprites/grass.png
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sounds/shoot.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sounds/shoot.mp3
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sounds/fly.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sounds/fly.mp3
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/dodge.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/dodge.ase
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/meteor.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/meteor.ase
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/meteor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/meteor.png
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/barney.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/barney.ase
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/barney.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/barney.png
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/cactus.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/cactus.ase
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/cactus.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/cactus.png
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/desert.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/desert.ase
--------------------------------------------------------------------------------
/games/tga/snipe/assets/sprites/desert.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/snipe/assets/sprites/desert.png
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/fly.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/fly.ase
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/fly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/fly.png
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/hand.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/hand.ase
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/hand.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/hand.png
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/wall.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/wall.ase
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/wall.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/wall.png
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/stickman.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/stickman.ase
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/stickman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/dodge/assets/sprites/stickman.png
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sounds/squeeze.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sounds/squeeze.mp3
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/blood.ase:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/blood.ase
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/blood.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/replit/kaboomware/master/games/tga/squeeze/assets/sprites/blood.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "module": "esnext",
5 | "target": "esnext",
6 | "moduleResolution": "node",
7 | "paths": {
8 | "kaboomware": ["./src/kaboomware.ts"]
9 | }
10 | },
11 | "include": [
12 | "src/**/*",
13 | "example/**/*"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | import * as esbuild from "esbuild"
2 |
3 | await esbuild.build({
4 | entryPoints: [ "src/kaboomware.ts" ],
5 | outfile: "dist/kaboomware.js",
6 | bundle: true,
7 | sourcemap: true,
8 | keepNames: true,
9 | loader: {
10 | ".png": "dataurl",
11 | ".mp3": "binary",
12 | ".woff2": "binary",
13 | },
14 | minify: true,
15 | format: "esm",
16 | })
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kaboomware",
3 | "type": "module",
4 | "version": "0.1.6",
5 | "main": "./dist/kaboomware.js",
6 | "types": "./dist/kaboomware.d.ts",
7 | "files": [
8 | "dist/",
9 | "src/"
10 | ],
11 | "scripts": {
12 | "build": "rm -rf dist && node scripts/build.js && tsc --declaration --emitDeclarationOnly --outDir dist src/kaboomware.ts",
13 | "dev": "node scripts/dev.js",
14 | "play": "node scripts/play.js",
15 | "create": "node scripts/create.js",
16 | "check": "tsc",
17 | "prepare": "npm run build",
18 | "clean": "rm -rf .tmp"
19 | },
20 | "dependencies": {
21 | "kaboom": "file:../kaboom"
22 | },
23 | "devDependencies": {
24 | "esbuild": "^0.19.2",
25 | "typescript": "^5.2.2"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/fly.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "fly 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 26, "h": 17 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 26, "h": 17 },
8 | "sourceSize": { "w": 26, "h": 17 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "fly 1.ase",
13 | "frame": { "x": 26, "y": 0, "w": 26, "h": 17 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 26, "h": 17 },
17 | "sourceSize": { "w": 26, "h": 17 },
18 | "duration": 100
19 | }
20 | ],
21 | "meta": {
22 | "app": "https://www.aseprite.org/",
23 | "version": "1.2.39-arm64",
24 | "image": "fly.png",
25 | "format": "RGBA8888",
26 | "size": { "w": 52, "h": 17 },
27 | "scale": "1",
28 | "frameTags": [
29 | { "name": "fly", "from": 0, "to": 1, "direction": "forward" }
30 | ],
31 | "layers": [
32 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
33 | ],
34 | "slices": [
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/bang.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "bang 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 72, "h": 56 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 72, "h": 56 },
8 | "sourceSize": { "w": 72, "h": 56 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "bang 1.ase",
13 | "frame": { "x": 72, "y": 0, "w": 72, "h": 56 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 72, "h": 56 },
17 | "sourceSize": { "w": 72, "h": 56 },
18 | "duration": 100
19 | }
20 | ],
21 | "meta": {
22 | "app": "https://www.aseprite.org/",
23 | "version": "1.2.39-arm64",
24 | "image": "bang.png",
25 | "format": "RGBA8888",
26 | "size": { "w": 144, "h": 56 },
27 | "scale": "1",
28 | "frameTags": [
29 | { "name": "explode", "from": 0, "to": 1, "direction": "forward" }
30 | ],
31 | "layers": [
32 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
33 | ],
34 | "slices": [
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/fire.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "fire 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 100, "h": 107 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 100, "h": 107 },
8 | "sourceSize": { "w": 100, "h": 107 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "fire 1.ase",
13 | "frame": { "x": 100, "y": 0, "w": 100, "h": 107 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 100, "h": 107 },
17 | "sourceSize": { "w": 100, "h": 107 },
18 | "duration": 100
19 | }
20 | ],
21 | "meta": {
22 | "app": "https://www.aseprite.org/",
23 | "version": "1.2.39-arm64",
24 | "image": "fire.png",
25 | "format": "RGBA8888",
26 | "size": { "w": 200, "h": 107 },
27 | "scale": "1",
28 | "frameTags": [
29 | { "name": "burn", "from": 0, "to": 1, "direction": "forward" }
30 | ],
31 | "layers": [
32 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
33 | ],
34 | "slices": [
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/scripts/create.js:
--------------------------------------------------------------------------------
1 | import * as fs from "fs/promises"
2 |
3 | const [author, game] = (process.argv[2] ?? "").split(":")
4 |
5 | if (!author || !game) {
6 | console.error("Must specify author and game name")
7 | console.error("$ npm run create {author}:{game}")
8 | process.exit(1)
9 | }
10 |
11 | const dir = `games/${author}/${game}`
12 |
13 | const template = `
14 | import type { Game, Button } from "kaboomware"
15 |
16 | const game: Game = {
17 |
18 | prompt: "Squeeze!",
19 | author: "wario",
20 | hue: 0.6,
21 |
22 | onLoad: (k) => {
23 | // Load your assets here
24 | },
25 |
26 | onStart: (k) => {
27 | const scene = k.make()
28 | return scene
29 | },
30 |
31 | }
32 |
33 | export default game
34 | `.trim()
35 |
36 | const isDir = (path) =>
37 | fs
38 | .stat(path)
39 | .then((stat) => stat.isDirectory())
40 | .catch(() => false)
41 |
42 | if (await isDir(dir)) {
43 | console.error(`Game already exists at ${dir}!`)
44 | process.exit(1)
45 | }
46 |
47 | await fs.mkdir(dir, { recursive: true })
48 | await fs.mkdir(`${dir}/assets`, { recursive: true })
49 | await fs.writeFile(`${dir}/game.ts`, template)
50 |
51 | console.log(`Game created at ${dir}!`)
52 |
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/hand.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "hand 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 603, "h": 629 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 603, "h": 629 },
8 | "sourceSize": { "w": 603, "h": 629 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "hand 1.ase",
13 | "frame": { "x": 603, "y": 0, "w": 603, "h": 629 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 603, "h": 629 },
17 | "sourceSize": { "w": 603, "h": 629 },
18 | "duration": 100
19 | }
20 | ],
21 | "meta": {
22 | "app": "https://www.aseprite.org/",
23 | "version": "1.2.39-arm64",
24 | "image": "hand.png",
25 | "format": "RGBA8888",
26 | "size": { "w": 1206, "h": 629 },
27 | "scale": "1",
28 | "frameTags": [
29 | { "name": "idle", "from": 0, "to": 0, "direction": "forward" },
30 | { "name": "squeeze", "from": 1, "to": 1, "direction": "forward" }
31 | ],
32 | "layers": [
33 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" }
34 | ],
35 | "slices": [
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/cactus.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "cactus 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 88, "h": 154 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 88, "h": 154 },
8 | "sourceSize": { "w": 88, "h": 154 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "cactus 1.ase",
13 | "frame": { "x": 88, "y": 0, "w": 88, "h": 154 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 88, "h": 154 },
17 | "sourceSize": { "w": 88, "h": 154 },
18 | "duration": 100
19 | }
20 | ],
21 | "meta": {
22 | "app": "https://www.aseprite.org/",
23 | "version": "1.2.39-arm64",
24 | "image": "cactus.png",
25 | "format": "RGBA8888",
26 | "size": { "w": 176, "h": 154 },
27 | "scale": "1",
28 | "frameTags": [
29 | { "name": "woohoo", "from": 0, "to": 1, "direction": "forward" }
30 | ],
31 | "layers": [
32 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" },
33 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" },
34 | { "name": "Layer 3", "opacity": 255, "blendMode": "normal" }
35 | ],
36 | "slices": [
37 | ]
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/stickman.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "stickman 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 34, "h": 54 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 34, "h": 54 },
8 | "sourceSize": { "w": 34, "h": 54 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "stickman 1.ase",
13 | "frame": { "x": 34, "y": 0, "w": 34, "h": 54 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 34, "h": 54 },
17 | "sourceSize": { "w": 34, "h": 54 },
18 | "duration": 100
19 | },
20 | {
21 | "filename": "stickman 2.ase",
22 | "frame": { "x": 68, "y": 0, "w": 34, "h": 54 },
23 | "rotated": false,
24 | "trimmed": false,
25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 34, "h": 54 },
26 | "sourceSize": { "w": 34, "h": 54 },
27 | "duration": 100
28 | }
29 | ],
30 | "meta": {
31 | "app": "https://www.aseprite.org/",
32 | "version": "1.2.39-arm64",
33 | "image": "stickman.png",
34 | "format": "RGBA8888",
35 | "size": { "w": 102, "h": 54 },
36 | "scale": "1",
37 | "frameTags": [
38 | ],
39 | "layers": [
40 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
41 | ],
42 | "slices": [
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/scripts/dev.js:
--------------------------------------------------------------------------------
1 | import * as esbuild from "esbuild"
2 | import * as fs from "fs/promises"
3 | import * as path from "path"
4 |
5 | const [author, game] = (process.argv[2] ?? "").split(":")
6 |
7 | if (!author || !game) {
8 | console.error("Must specify author and game name")
9 | console.error("$ npm run dev {author}:{game}")
10 | process.exit(1)
11 | }
12 |
13 | const dir = `games/${author}/${game}`
14 |
15 | const code = `
16 | import kaboomware from "kaboomware"
17 | import game from "./../${dir}/game"
18 |
19 | kaboomware([
20 | game,
21 | ], {
22 | dev: true,
23 | letterbox: true,
24 | background: [0, 0, 0],
25 | })
26 | `.trim()
27 |
28 | const html = `
29 |
30 |
31 |
32 | kaboomware
33 |
34 |
35 |
36 |
37 |
38 | `.trim()
39 |
40 | try {
41 | await fs.rm(".tmp", { recursive: true })
42 | } catch {}
43 | await fs.mkdir(".tmp", { recursive: true })
44 | await fs.writeFile(".tmp/main.ts", code)
45 | await fs.writeFile(".tmp/index.html", html)
46 | await fs.symlink(path.relative(".tmp", `${dir}/assets`), ".tmp/assets")
47 |
48 | const ctx = await esbuild.context({
49 | entryPoints: [ ".tmp/main.ts" ],
50 | outfile: ".tmp/bundle.js",
51 | bundle: true,
52 | sourcemap: true,
53 | keepNames: true,
54 | format: "esm",
55 | loader: {
56 | ".png": "dataurl",
57 | ".mp3": "binary",
58 | ".woff2": "binary",
59 | },
60 | })
61 |
62 | await ctx.watch()
63 |
64 | const { port } = await ctx.serve({
65 | servedir: ".tmp",
66 | })
67 |
68 | console.log(`http://localhost:${port}`)
69 |
--------------------------------------------------------------------------------
/games/tga/dodge/assets/sprites/meteor.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "meteor 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 56, "h": 139 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 56, "h": 139 },
8 | "sourceSize": { "w": 56, "h": 139 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "meteor 1.ase",
13 | "frame": { "x": 56, "y": 0, "w": 56, "h": 139 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 56, "h": 139 },
17 | "sourceSize": { "w": 56, "h": 139 },
18 | "duration": 100
19 | },
20 | {
21 | "filename": "meteor 2.ase",
22 | "frame": { "x": 112, "y": 0, "w": 56, "h": 139 },
23 | "rotated": false,
24 | "trimmed": false,
25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 56, "h": 139 },
26 | "sourceSize": { "w": 56, "h": 139 },
27 | "duration": 100
28 | },
29 | {
30 | "filename": "meteor 3.ase",
31 | "frame": { "x": 168, "y": 0, "w": 56, "h": 139 },
32 | "rotated": false,
33 | "trimmed": false,
34 | "spriteSourceSize": { "x": 0, "y": 0, "w": 56, "h": 139 },
35 | "sourceSize": { "w": 56, "h": 139 },
36 | "duration": 100
37 | }
38 | ],
39 | "meta": {
40 | "app": "https://www.aseprite.org/",
41 | "version": "1.2.39-arm64",
42 | "image": "meteor.png",
43 | "format": "RGBA8888",
44 | "size": { "w": 224, "h": 139 },
45 | "scale": "1",
46 | "frameTags": [
47 | { "name": "fall", "from": 0, "to": 3, "direction": "forward" }
48 | ],
49 | "layers": [
50 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
51 | ],
52 | "slices": [
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/games/tga/squeeze/assets/sprites/blood.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "blood 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 123, "h": 125 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 123, "h": 125 },
8 | "sourceSize": { "w": 123, "h": 125 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "blood 1.ase",
13 | "frame": { "x": 123, "y": 0, "w": 123, "h": 125 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 123, "h": 125 },
17 | "sourceSize": { "w": 123, "h": 125 },
18 | "duration": 100
19 | },
20 | {
21 | "filename": "blood 2.ase",
22 | "frame": { "x": 246, "y": 0, "w": 123, "h": 125 },
23 | "rotated": false,
24 | "trimmed": false,
25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 123, "h": 125 },
26 | "sourceSize": { "w": 123, "h": 125 },
27 | "duration": 100
28 | },
29 | {
30 | "filename": "blood 3.ase",
31 | "frame": { "x": 369, "y": 0, "w": 123, "h": 125 },
32 | "rotated": false,
33 | "trimmed": false,
34 | "spriteSourceSize": { "x": 0, "y": 0, "w": 123, "h": 125 },
35 | "sourceSize": { "w": 123, "h": 125 },
36 | "duration": 100
37 | }
38 | ],
39 | "meta": {
40 | "app": "https://www.aseprite.org/",
41 | "version": "1.2.39-arm64",
42 | "image": "blood.png",
43 | "format": "RGBA8888",
44 | "size": { "w": 492, "h": 125 },
45 | "scale": "1",
46 | "frameTags": [
47 | { "name": "splatter", "from": 0, "to": 3, "direction": "forward" }
48 | ],
49 | "layers": [
50 | { "name": "1", "opacity": 255, "blendMode": "normal" }
51 | ],
52 | "slices": [
53 | ]
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/scripts/play.js:
--------------------------------------------------------------------------------
1 | import * as esbuild from "esbuild"
2 | import * as fs from "fs/promises"
3 | import * as path from "path"
4 |
5 | const games = process.argv.slice(2).map((g) => {
6 | const [author, game] = g.split(":")
7 | if (!author || !game) {
8 | console.error("Incorrect format")
9 | console.error("$ npm run play {author}:{game} {author}:{game} ...")
10 | process.exit(1)
11 | }
12 | return { author, game }
13 | })
14 |
15 | if (games.length === 0) {
16 | process.exit(0)
17 | }
18 |
19 | const code = `
20 | import kaboomware from "kaboomware"
21 |
22 | ${games.map(({ author, game }, i) => {
23 | return `import game${i} from "./../games/${author}/${game}/game"`
24 | }).join("\n")}
25 |
26 | kaboomware([
27 | ${games.map(({ author, game }, i) => {
28 | return `\t{ ...game${i}, urlPrefix: "games/${author}/${game}/" },`
29 | }).join("\n")}
30 | ], {
31 | letterbox: true,
32 | background: [0, 0, 0],
33 | })
34 | `.trim()
35 |
36 | const html = `
37 |
38 |
39 |
40 | kaboomware
41 |
42 |
43 |
44 |
45 |
46 | `.trim()
47 |
48 | try {
49 | await fs.rm(".tmp", { recursive: true })
50 | } catch {}
51 | await fs.mkdir(".tmp", { recursive: true })
52 | await fs.writeFile(".tmp/main.ts", code)
53 | await fs.writeFile(".tmp/index.html", html)
54 | await fs.symlink(path.relative(".tmp", "games"), ".tmp/games")
55 |
56 | // TODO: assets
57 |
58 | const ctx = await esbuild.context({
59 | entryPoints: [ ".tmp/main.ts" ],
60 | outfile: ".tmp/bundle.js",
61 | bundle: true,
62 | sourcemap: true,
63 | keepNames: true,
64 | format: "esm",
65 | loader: {
66 | ".png": "dataurl",
67 | ".mp3": "binary",
68 | ".woff2": "binary",
69 | },
70 | })
71 |
72 | await ctx.watch()
73 |
74 | const { port } = await ctx.serve({
75 | servedir: ".tmp",
76 | })
77 |
78 | console.log(`http://localhost:${port}`)
79 |
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/bao.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "bao 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 158, "h": 131 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
8 | "sourceSize": { "w": 158, "h": 131 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "bao 1.ase",
13 | "frame": { "x": 158, "y": 0, "w": 158, "h": 131 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
17 | "sourceSize": { "w": 158, "h": 131 },
18 | "duration": 100
19 | },
20 | {
21 | "filename": "bao 2.ase",
22 | "frame": { "x": 316, "y": 0, "w": 158, "h": 131 },
23 | "rotated": false,
24 | "trimmed": false,
25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
26 | "sourceSize": { "w": 158, "h": 131 },
27 | "duration": 100
28 | },
29 | {
30 | "filename": "bao 3.ase",
31 | "frame": { "x": 474, "y": 0, "w": 158, "h": 131 },
32 | "rotated": false,
33 | "trimmed": false,
34 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
35 | "sourceSize": { "w": 158, "h": 131 },
36 | "duration": 100
37 | },
38 | {
39 | "filename": "bao 4.ase",
40 | "frame": { "x": 632, "y": 0, "w": 158, "h": 131 },
41 | "rotated": false,
42 | "trimmed": false,
43 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
44 | "sourceSize": { "w": 158, "h": 131 },
45 | "duration": 100
46 | },
47 | {
48 | "filename": "bao 5.ase",
49 | "frame": { "x": 790, "y": 0, "w": 158, "h": 131 },
50 | "rotated": false,
51 | "trimmed": false,
52 | "spriteSourceSize": { "x": 0, "y": 0, "w": 158, "h": 131 },
53 | "sourceSize": { "w": 158, "h": 131 },
54 | "duration": 100
55 | }
56 | ],
57 | "meta": {
58 | "app": "https://www.aseprite.org/",
59 | "version": "1.2.39-arm64",
60 | "image": "bao.png",
61 | "format": "RGBA8888",
62 | "size": { "w": 948, "h": 131 },
63 | "scale": "1",
64 | "frameTags": [
65 | { "name": "run", "from": 0, "to": 1, "direction": "forward" },
66 | { "name": "cry", "from": 2, "to": 3, "direction": "forward" },
67 | { "name": "woohoo", "from": 4, "to": 5, "direction": "forward" }
68 | ],
69 | "layers": [
70 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" },
71 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" }
72 | ],
73 | "slices": [
74 | ]
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/confetti.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | KaboomCtx,
3 | Color,
4 | Vec2,
5 | } from "kaboom"
6 |
7 | export type Sampler = T | (() => T)
8 |
9 | export type ConfettiOpt = {
10 | gravity?: number,
11 | airDrag?: number,
12 | spread?: number,
13 | fade?: number,
14 | count?: number,
15 | heading?: Sampler,
16 | color?: Sampler,
17 | pos?: Sampler,
18 | velocity?: Sampler,
19 | angularVelocity?: Sampler,
20 | obj?: () => { draw: () => void },
21 | }
22 |
23 | const DEF_COUNT = 80
24 | const DEF_GRAVITY = 800
25 | const DEF_AIR_DRAG = 0.9
26 | const DEF_VELOCITY = [1000, 4000]
27 | const DEF_ANGULAR_VELOCITY = [-200, 200]
28 | const DEF_FADE = 0.3
29 | const DEF_SPREAD = 60
30 | const DEF_SPIN = [2, 8]
31 | const DEF_SATURATION = 0.7
32 | const DEF_LIGHTNESS = 0.6
33 |
34 | export default function(k: KaboomCtx) {
35 |
36 | return (opt: ConfettiOpt = {}) => {
37 | const confetti = k.make()
38 | // @ts-ignore
39 | const sample = (s: Sampler): T => typeof s === "function" ? s() : s
40 | for (let i = 0; i < (opt.count ?? DEF_COUNT); i++) {
41 | const p = confetti.add([
42 | k.pos(sample(opt.pos ?? k.vec2(0, 0))),
43 | opt.obj ? opt.obj() : k.choose([
44 | k.rect(k.rand(4, 20), k.rand(4, 20)),
45 | k.circle(k.rand(3, 10)),
46 | ]),
47 | k.color(sample(opt.color ?? k.hsl2rgb(k.rand(0, 1), DEF_SATURATION, DEF_LIGHTNESS))),
48 | k.opacity(1),
49 | k.lifespan(4),
50 | k.scale(1),
51 | k.anchor("center"),
52 | k.rotate(k.rand(0, 360)),
53 | ])
54 | const spin = k.rand(DEF_SPIN[0], DEF_SPIN[1])
55 | const gravity = opt.gravity ?? DEF_GRAVITY
56 | const airDrag = opt.airDrag ?? DEF_AIR_DRAG
57 | const heading = sample(opt.heading ?? 0) - 90
58 | const spread = opt.spread ?? DEF_SPREAD
59 | const head = heading + k.rand(-spread / 2, spread / 2)
60 | const fade = opt.fade ?? DEF_FADE
61 | const vel = sample(opt.velocity ?? k.rand(DEF_VELOCITY[0], DEF_VELOCITY[1]))
62 | let velX = Math.cos(k.deg2rad(head)) * vel
63 | let velY = Math.sin(k.deg2rad(head)) * vel
64 | const velA = sample(opt.angularVelocity ?? k.rand(DEF_ANGULAR_VELOCITY[0], DEF_ANGULAR_VELOCITY[1]))
65 | p.onUpdate(() => {
66 | const dt = k.dt()
67 | velY += gravity * dt
68 | p.pos.x += velX * dt
69 | p.pos.y += velY * dt
70 | p.angle += velA * dt
71 | p.opacity -= fade * dt
72 | velX *= airDrag
73 | velY *= airDrag
74 | p.scale.x = k.wave(-1, 1, k.time() * spin)
75 | })
76 | }
77 | return confetti
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/games/tga/dodge/game.ts:
--------------------------------------------------------------------------------
1 | import type { Game, Button } from "kaboomware"
2 |
3 | const SPEED = 240
4 | const METEOR_SPEED = 480
5 | const SPAWN_SPEED = 0.2
6 |
7 | const game: Game = {
8 |
9 | prompt: "Dodge!",
10 | author: "tga",
11 | hue: 0.08,
12 |
13 | onLoad: (k) => {
14 | k.loadRoot("assets/")
15 | k.loadAseprite("stickman", "sprites/stickman.png", "sprites/stickman.json")
16 | k.loadAseprite("meteor", "sprites/meteor.png", "sprites/meteor.json")
17 | k.loadAseprite("bang", "sprites/bang.png", "sprites/bang.json")
18 | },
19 |
20 | onStart: (k) => {
21 |
22 | const scene = k.make([
23 | k.timer(),
24 | ])
25 |
26 | let hurt = false
27 |
28 | scene.add([
29 | k.rect(k.width(), k.height()),
30 | k.color(k.rgb(255, 255, 255)),
31 | ])
32 |
33 | const man = scene.add([
34 | k.sprite("stickman"),
35 | k.pos(400, 200),
36 | k.area(),
37 | k.z(50),
38 | k.anchor("center"),
39 | ])
40 |
41 | const dirs = {
42 | "left": k.LEFT,
43 | "right": k.RIGHT,
44 | "up": k.UP,
45 | "down": k.DOWN,
46 | }
47 |
48 | for (const dir in dirs) {
49 | k.onButtonDown(dir as Button, () => {
50 | if (hurt) return
51 | man.move(dirs[dir].scale(SPEED))
52 | man.frame = (man.frame + 1) % man.numFrames()
53 | })
54 | }
55 |
56 | scene.loop(SPAWN_SPEED, () => {
57 | const destY = k.rand(100, k.height() - 50)
58 | const m = scene.add([
59 | k.sprite("meteor", { anim: "fall" }),
60 | k.anchor("bot"),
61 | k.z(100),
62 | k.pos(man.pos.x + k.rand(-100, 100), -100),
63 | ])
64 | const shadow = scene.add([])
65 | shadow.onDraw(() => {
66 | const r = 1 - (destY - m.pos.y) / 400
67 | k.drawEllipse({
68 | pos: k.vec2(m.pos.x, destY - 10),
69 | radiusX: 24 * r,
70 | radiusY: 16 * r,
71 | color: k.rgb(200, 200, 200),
72 | })
73 | })
74 | m.onUpdate(() => {
75 | m.pos.y += k.dt() * METEOR_SPEED
76 | if (m.pos.y >= destY) {
77 | m.destroy()
78 | shadow.destroy()
79 | scene.add([
80 | k.sprite("bang", { anim: "explode", animSpeed: 2, }),
81 | k.anchor("center"),
82 | k.pos(m.pos.x, m.pos.y - 10),
83 | k.lifespan(0.5),
84 | k.area(),
85 | k.z(200),
86 | ])
87 | if (man.pos.dist(m.pos) <= 50) {
88 | hurt = true
89 | k.lose()
90 | }
91 | }
92 | })
93 | })
94 |
95 | k.onTimeout(() => k.win())
96 |
97 | return scene
98 |
99 | },
100 |
101 | }
102 |
103 | export default game
104 |
--------------------------------------------------------------------------------
/games/tga/eat/game.ts:
--------------------------------------------------------------------------------
1 | import type { Key } from "kaboom"
2 | import type { Game, Button } from "kaboomware"
3 |
4 | const SPEED = 240
5 |
6 | const eatGame: Game = {
7 |
8 | prompt: "Eat!",
9 | author: "tga",
10 | hue: 0.75,
11 |
12 | onLoad: (k) => {
13 | k.loadRoot("assets/")
14 | k.loadSound("walk", "sounds/walk.mp3")
15 | k.loadSprite("grass", "sprites/grass.png")
16 | k.loadAseprite("fish", "sprites/fish.png", "sprites/fish.json")
17 | k.loadAseprite("bao", "sprites/bao.png", "sprites/bao.json")
18 | k.loadAseprite("cactus", "sprites/cactus.png", "sprites/cactus.json")
19 | k.loadAseprite("fire", "sprites/fire.png", "sprites/fire.json")
20 | },
21 |
22 | onStart: (k) => {
23 |
24 | let gotFish = false
25 | let hurt = false
26 | const scene = k.make()
27 |
28 | scene.add([
29 | k.sprite("grass", { width: k.width(), height: k.height() }),
30 | ])
31 |
32 | scene.add([
33 | k.pos(320, 240),
34 | k.sprite("fire", { anim: "burn" }),
35 | k.area({ scale: 0.6 }),
36 | k.anchor("center"),
37 | "danger",
38 | ])
39 |
40 | scene.add([
41 | k.pos(150, 170),
42 | k.sprite("cactus", { anim: "woohoo" }),
43 | k.area({ scale: 0.5 }),
44 | k.anchor("center"),
45 | "danger",
46 | ])
47 |
48 | const fish = scene.add([
49 | k.pos(480, 120),
50 | k.sprite("fish"),
51 | k.area({ scale: 0.6 }),
52 | k.anchor("center"),
53 | "fish",
54 | ])
55 |
56 | const bao = scene.add([
57 | k.pos(120, 380),
58 | k.sprite("bao", { anim: "run" }),
59 | k.area({ scale: 0.6 }),
60 | k.anchor("center"),
61 | ])
62 |
63 | const dirs = {
64 | "left": k.LEFT,
65 | "right": k.RIGHT,
66 | "up": k.UP,
67 | "down": k.DOWN,
68 | }
69 |
70 | for (const dir in dirs) {
71 | k.onButtonDown(dir as Button, () => {
72 | if (gotFish || hurt) return
73 | bao.move(dirs[dir].scale(SPEED))
74 | })
75 | }
76 |
77 | bao.onCollide("danger", () => {
78 | k.lose()
79 | hurt = true
80 | bao.play("cry")
81 | })
82 |
83 | bao.onCollide("fish", () => {
84 | k.win()
85 | gotFish = true
86 | bao.play("woohoo")
87 | fish.play("eaten", { loop: false })
88 | })
89 |
90 | k.onTimeout(() => {
91 | bao.play("cry")
92 | })
93 |
94 | const music = k.play("walk", {
95 | loop: true,
96 | volume: 0.2,
97 | })
98 |
99 | k.onEnd(() => {
100 | music.stop()
101 | k.camPos(k.center())
102 | k.camScale(1, 1)
103 | })
104 |
105 | return scene
106 |
107 | },
108 |
109 | }
110 |
111 | export default eatGame
112 |
--------------------------------------------------------------------------------
/games/tga/eat/assets/sprites/fish.json:
--------------------------------------------------------------------------------
1 | { "frames": [
2 | {
3 | "filename": "fish 0.ase",
4 | "frame": { "x": 0, "y": 0, "w": 115, "h": 72 },
5 | "rotated": false,
6 | "trimmed": false,
7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
8 | "sourceSize": { "w": 115, "h": 72 },
9 | "duration": 100
10 | },
11 | {
12 | "filename": "fish 1.ase",
13 | "frame": { "x": 115, "y": 0, "w": 115, "h": 72 },
14 | "rotated": false,
15 | "trimmed": false,
16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
17 | "sourceSize": { "w": 115, "h": 72 },
18 | "duration": 100
19 | },
20 | {
21 | "filename": "fish 2.ase",
22 | "frame": { "x": 230, "y": 0, "w": 115, "h": 72 },
23 | "rotated": false,
24 | "trimmed": false,
25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
26 | "sourceSize": { "w": 115, "h": 72 },
27 | "duration": 100
28 | },
29 | {
30 | "filename": "fish 3.ase",
31 | "frame": { "x": 345, "y": 0, "w": 115, "h": 72 },
32 | "rotated": false,
33 | "trimmed": false,
34 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
35 | "sourceSize": { "w": 115, "h": 72 },
36 | "duration": 100
37 | },
38 | {
39 | "filename": "fish 4.ase",
40 | "frame": { "x": 460, "y": 0, "w": 115, "h": 72 },
41 | "rotated": false,
42 | "trimmed": false,
43 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
44 | "sourceSize": { "w": 115, "h": 72 },
45 | "duration": 100
46 | },
47 | {
48 | "filename": "fish 5.ase",
49 | "frame": { "x": 575, "y": 0, "w": 115, "h": 72 },
50 | "rotated": false,
51 | "trimmed": false,
52 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
53 | "sourceSize": { "w": 115, "h": 72 },
54 | "duration": 100
55 | },
56 | {
57 | "filename": "fish 6.ase",
58 | "frame": { "x": 690, "y": 0, "w": 115, "h": 72 },
59 | "rotated": false,
60 | "trimmed": false,
61 | "spriteSourceSize": { "x": 0, "y": 0, "w": 115, "h": 72 },
62 | "sourceSize": { "w": 115, "h": 72 },
63 | "duration": 100
64 | }
65 | ],
66 | "meta": {
67 | "app": "https://www.aseprite.org/",
68 | "version": "1.2.39-arm64",
69 | "image": "fish.png",
70 | "format": "RGBA8888",
71 | "size": { "w": 805, "h": 72 },
72 | "scale": "1",
73 | "frameTags": [
74 | { "name": "idle", "from": 0, "to": 0, "direction": "forward" },
75 | { "name": "eaten", "from": 1, "to": 6, "direction": "forward" }
76 | ],
77 | "layers": [
78 | { "name": "Layer 2", "opacity": 255, "blendMode": "normal" },
79 | { "name": "Layer 3", "opacity": 255, "blendMode": "normal" },
80 | { "name": "Layer 1", "opacity": 255, "blendMode": "normal" }
81 | ],
82 | "slices": [
83 | ]
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/games/tga/squeeze/game.ts:
--------------------------------------------------------------------------------
1 | import type { Game, Button } from "kaboomware"
2 |
3 | const NUM_FLIES = 1
4 | const FLY_SPEED = 400
5 | const FLY_MARGIN = 160
6 | const SPEED = 240
7 |
8 | const squeezeGame: Game = {
9 |
10 | prompt: "Squeeze!",
11 | author: "tga",
12 | hue: 0.46,
13 |
14 | onLoad: (k) => {
15 | k.loadRoot("assets/")
16 | k.loadSound("squeeze", "sounds/squeeze.mp3")
17 | k.loadSound("fly", "sounds/fly.mp3")
18 | k.loadSprite("wall", "sprites/wall.png")
19 | k.loadAseprite("hand", "sprites/hand.png", "sprites/hand.json")
20 | k.loadAseprite("fly", "sprites/fly.png", "sprites/fly.json")
21 | k.loadAseprite("blood", "sprites/blood.png", "sprites/blood.json")
22 | },
23 |
24 | onStart: (k) => {
25 |
26 | const buzzSound = k.play("fly", {
27 | loop: true,
28 | volume: 0.2,
29 | })
30 |
31 | const scene = k.make()
32 |
33 | scene.add([
34 | k.sprite("wall", { width: k.width(), height: k.height() }),
35 | ])
36 |
37 | const makeFly = () => {
38 | const fly = k.make([
39 | k.pos(
40 | k.rand(FLY_MARGIN, k.width() - FLY_MARGIN),
41 | k.rand(FLY_MARGIN, k.height() - FLY_MARGIN),
42 | ),
43 | k.sprite("fly", { anim: "fly" }),
44 | k.anchor("center"),
45 | "fly",
46 | ])
47 | fly.onUpdate(() => {
48 | fly.pos.x += k.rand(-FLY_SPEED, FLY_SPEED) * k.dt()
49 | fly.pos.y += k.rand(-FLY_SPEED, FLY_SPEED) * k.dt()
50 | })
51 | return fly
52 | }
53 |
54 | for (let i = 0; i < NUM_FLIES; i++) {
55 | scene.add(makeFly())
56 | }
57 |
58 | const handOffset = k.vec2(-30, -140)
59 |
60 | const hand = scene.add([
61 | k.pos(420, 240),
62 | k.sprite("hand"),
63 | k.z(10),
64 | ])
65 |
66 | hand.onMouseMove(() => {
67 | hand.pos = k.mousePos().add(handOffset)
68 | })
69 |
70 | const dirs = {
71 | "left": k.LEFT,
72 | "right": k.RIGHT,
73 | "up": k.UP,
74 | "down": k.DOWN,
75 | }
76 |
77 | for (const dir in dirs) {
78 | k.onButtonDown(dir as Button, () => {
79 | hand.move(dirs[dir].scale(SPEED))
80 | })
81 | }
82 |
83 | k.onButtonPress("action", () => {
84 | k.play("squeeze")
85 | hand.play("squeeze")
86 | for (const fly of scene.get("fly")) {
87 | const pos = hand.pos.sub(handOffset)
88 | if (pos.dist(fly.pos) <= 20) {
89 | fly.destroy()
90 | const blood = scene.add([
91 | k.pos(fly.pos),
92 | k.anchor("center"),
93 | k.sprite("blood"),
94 | ])
95 | // TODO: have loop option in sprite()
96 | blood.play("splatter", { loop: false, speed: 20 })
97 | if (scene.get("fly").length === 0) {
98 | buzzSound.stop()
99 | k.win()
100 | }
101 | break
102 | }
103 | }
104 | })
105 |
106 | k.onButtonRelease("action", () => {
107 | hand.play("idle")
108 | })
109 |
110 | k.onEnd(() => {
111 | buzzSound.stop()
112 | })
113 |
114 | return scene
115 |
116 | },
117 |
118 | }
119 |
120 | export default squeezeGame
121 |
--------------------------------------------------------------------------------
/games/tga/snipe/game.ts:
--------------------------------------------------------------------------------
1 | import type { Game, Button } from "kaboomware"
2 |
3 | const SPEED = 240
4 |
5 | const shootGame: Game = {
6 |
7 | prompt: "Snipe!",
8 | author: "tga",
9 | hue: 0.5,
10 |
11 | onLoad: (k) => {
12 | k.loadRoot("assets/")
13 | k.loadSprite("desert", "sprites/desert.png")
14 | k.loadSprite("cactus", "sprites/cactus.png")
15 | k.loadSprite("barney", "sprites/barney.png")
16 | k.loadSound("shoot", "sounds/shoot.mp3")
17 | },
18 |
19 | onStart: (k) => {
20 |
21 | function shake(speed = 8) {
22 | let s = 0
23 | return {
24 | update() {
25 | if (s <= 0) return
26 | this.pos = k.Vec2.fromAngle(k.rand(0, 360)).scale(s)
27 | s = k.lerp(s, 0, speed * k.dt())
28 | },
29 | shake(to: number) {
30 | s += to
31 | },
32 | }
33 | }
34 |
35 | const scene = k.make([
36 | k.pos(),
37 | shake(),
38 | ])
39 |
40 | scene.add([
41 | k.sprite("desert", { width: k.width(), height: k.height() }),
42 | ])
43 |
44 | const cactusPos = [
45 | k.vec2(80, 40),
46 | k.vec2(480, 120),
47 | k.vec2(200, 300),
48 | ]
49 |
50 | const barneyPos = k.choose(cactusPos).add(20, 10)
51 |
52 | const barney = scene.add([
53 | k.sprite("barney"),
54 | k.pos(barneyPos),
55 | k.area({ shape: new k.Rect(k.vec2(30, 0), 60, 60) }),
56 | ])
57 |
58 | for (const p of cactusPos) {
59 | scene.add([
60 | k.sprite("cactus"),
61 | k.pos(p),
62 | ])
63 | }
64 |
65 | let pos = k.vec2(400, 300)
66 |
67 | const dirs = {
68 | "left": k.LEFT,
69 | "right": k.RIGHT,
70 | "up": k.UP,
71 | "down": k.DOWN,
72 | }
73 |
74 | for (const dir in dirs) {
75 | k.onButtonDown(dir as Button, () => {
76 | pos = pos.add(dirs[dir].scale(k.dt() * SPEED))
77 | })
78 | }
79 |
80 | scene.onMouseMove(() => {
81 | pos = k.mousePos()
82 | })
83 |
84 | const ui = scene.add()
85 |
86 | ui.onDraw(() => {
87 | // TODO: this is invalidating the outside stencil
88 | k.drawSubtracted(() => {
89 | k.drawRect({
90 | pos: k.vec2(0, 0),
91 | width: k.width(),
92 | height: k.height(),
93 | color: k.rgb(0, 0, 0),
94 | })
95 | }, () => {
96 | k.drawCircle({
97 | pos: pos,
98 | radius: 120,
99 | })
100 | })
101 | k.drawLine({
102 | p1: k.vec2(0, pos.y),
103 | p2: k.vec2(k.width(), pos.y),
104 | width: 3,
105 | color: k.rgb(0, 0, 0),
106 | })
107 | k.drawLine({
108 | p1: k.vec2(pos.x, 0),
109 | p2: k.vec2(pos.x, k.height()),
110 | width: 3,
111 | color: k.rgb(0, 0, 0),
112 | })
113 | k.drawCircle({
114 | pos: pos,
115 | radius: 32,
116 | fill: false,
117 | outline: {
118 | width: 3,
119 | color: k.rgb(0, 0, 0),
120 | },
121 | })
122 | k.drawCircle({
123 | pos: pos,
124 | radius: 4,
125 | color: k.rgb(255, 0, 0),
126 | })
127 | k.drawCircle({
128 | pos: pos,
129 | radius: 120,
130 | fill: false,
131 | outline: {
132 | width: 8,
133 | color: k.rgb(100, 100, 100),
134 | },
135 | })
136 | })
137 |
138 | k.onButtonPress("action", () => {
139 | k.play("shoot")
140 | scene.shake(16)
141 | // TODO: bugged
142 | if (barney.hasPoint(pos)) {
143 | k.win()
144 | console.log("yes")
145 | } else {
146 | console.log("no")
147 | }
148 | })
149 |
150 | return scene
151 |
152 | },
153 |
154 | }
155 |
156 | export default shootGame
157 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | KaboomWare is a tool for making warioware-like mini games in Kaboom.
4 |
5 | ## Developing & Publishing a Mini Game
6 |
7 | 1. Create a fork of the KaboomWare
8 |
9 | 2. Clone your forked repo
10 |
11 | ```sh
12 | $ git clone https://github.com/{your_github_id}/kaboomware
13 | ```
14 |
15 | 3. Install dependencies
16 |
17 | ```sh
18 | $ npm install
19 | ```
20 |
21 | 4. Create a game with
22 |
23 | ```sh
24 | $ npm run create {yourname}:{gamename}
25 | # for example
26 | $ npm run create wario:squeeze
27 | ```
28 |
29 | > Note: Game name has to be ASCII characters with no space
30 |
31 | This will create a folder at `games/{yourname}/{gamename}`, with
32 |
33 | - `main.ts` - Game script
34 | - `assets/` - All the assets that'll be used for the game
35 |
36 | 5. Run your game with
37 |
38 | ```sh
39 | $ npm run dev {yourname}:{gamename}
40 | ```
41 |
42 | 6. Edit `games/{yourname/{gamename}/main.ts` and start developing the game!
43 |
44 | A KaboomWare game is just a plain JavaScript object:
45 |
46 | ```ts
47 | const squeezeGame = {
48 |
49 | // The prompt for the game that tells player what to do. Normally it'll be just a simple verb.
50 | prompt: "Squeeze!",
51 |
52 | // Name of the author.
53 | author: "tga",
54 |
55 | // Background color hue (0.0 - 1.0).
56 | hue: 0.46,
57 |
58 | // Load assets for the game. The argument k is a limited version of the Kaboom context, only k.loadXXX() functions are enabled here.
59 | onLoad: (k) => {
60 | k.loadRoot("assets/")
61 | k.loadSound("fly", "sounds/fly.mp3")
62 | k.loadSprite("hand", "sprites/hand.png")
63 | },
64 |
65 | // Main entry point of the game. This function should return a GameObject that contains the game. The argument k is a limited version of the Kaboom context, plus a set of KaboomWare-specific APIs (see below)
66 | onStart: (k) => {
67 |
68 | // k.add() is disabled, use k.make() to make a game object and return
69 | const scene = k.make()
70 |
71 | // All game objects are added as children of the scene game object
72 | const hand = scene.add([
73 | k.pos(420, 240),
74 | k.sprite("hand"),
75 | ])
76 |
77 | // KaboomWare only supports 1 action button and 4 directional buttons. Use the KaboomWare-specific API k.onButtonXXX()
78 | k.onButtonPress("action", () => {
79 | hand.squeeze()
80 | if (gotIt) {
81 | // Tell KaboomWare player has succeeded and progress to the next game
82 | k.win()
83 | }
84 | })
85 |
86 | // Return the scene game object here and it'll get mounted to KaboomWare when this game starts.
87 | return scene
88 |
89 | },
90 |
91 | }
92 | ```
93 |
94 | The added API in `onStart()` is
95 |
96 | ```ts
97 | type GameAPI = {
98 | // Register an event that runs once when a button is pressed.
99 | onButtonPress: (btn: Button, action: () => void) => EventController,
100 | // Register an event that runs once when a button is released.
101 | onButtonRelease: (btn: Button, action: () => void) => EventController,
102 | // Register an event that runs every frame when a button is held down.
103 | onButtonDown: (btn: Button, action: () => void) => EventController,
104 | // Register an event that runs once when timer runs out.
105 | onTimeout: (action: () => void) => EventController,
106 | // Register an event that runs once when game ends, either succeeded, failed or timed out.
107 | onEnd: (action: () => void) => EventController,
108 | // Run this when player succeeded in completing the game.
109 | win: () => void,
110 | // Run this when player failed.
111 | lose: () => void,
112 | // Current difficulty.
113 | difficulty: 0 | 1 | 2,
114 | }
115 |
116 | type Button =
117 | | "action"
118 | | "left"
119 | | "right"
120 | | "up"
121 | | "down"
122 | ```
123 |
124 | 7. Once you finished a game, submit a PR to the [kaboomware github repo](https://github.com/slmjkdbtl/kaboomware), using the naming format: `[Game] {yourname} - {gamename}`
125 |
126 | One PR should only contain 1 game! Normally a game PR will always go through, unless it's oibviously unplayable.
127 |
--------------------------------------------------------------------------------
/src/kaboomware.ts:
--------------------------------------------------------------------------------
1 | import kaboom from "kaboom"
2 |
3 | import type {
4 | KaboomOpt,
5 | EventController,
6 | GameObj,
7 | KaboomCtx,
8 | Key,
9 | Color,
10 | Vec2,
11 | } from "kaboom"
12 |
13 | import useConfetti from "./confetti"
14 |
15 | // @ts-ignore
16 | import apl386FontBytes from "./fonts/apl386.woff2"
17 | // @ts-ignore
18 | import coolSoundBytes from "./sounds/cool.mp3"
19 | // @ts-ignore
20 | import screamSoundBytes from "./sounds/scream.mp3"
21 | // @ts-ignore
22 | import timerSpriteUrl from "./sprites/timer.png"
23 | // @ts-ignore
24 | import heartSpriteUrl from "./sprites/heart.png"
25 |
26 | const GAME_TIME = 4
27 | export const BG_S = 0.27
28 | export const BG_L = 0.52
29 |
30 | const loadAPIs = [
31 | "loadRoot",
32 | "loadSprite",
33 | "loadSpriteAtlas",
34 | "loadAseprite",
35 | "loadPedit",
36 | "loadBean",
37 | "loadJSON",
38 | "loadSound",
39 | "loadFont",
40 | "loadBitmapFont",
41 | "loadShader",
42 | "loadShaderURL",
43 | "load",
44 | "loadProgress",
45 | ] as const
46 |
47 | const gameAPIs = [
48 | "make",
49 | "pos",
50 | "scale",
51 | "rotate",
52 | "color",
53 | "opacity",
54 | "sprite",
55 | "text",
56 | "rect",
57 | "circle",
58 | "uvquad",
59 | "area",
60 | "anchor",
61 | "z",
62 | "outline",
63 | "body",
64 | "doubleJump",
65 | "move",
66 | "offscreen",
67 | "follow",
68 | "shader",
69 | "timer",
70 | "fixed",
71 | "stay",
72 | "health",
73 | "lifespan",
74 | "state",
75 | "fadeIn",
76 | "play",
77 | "rand",
78 | "randi",
79 | "dt",
80 | "time",
81 | "vec2",
82 | "rgb",
83 | "hsl2rgb",
84 | "choose",
85 | "chance",
86 | "easings",
87 | "map",
88 | "mapc",
89 | "wave",
90 | "lerp",
91 | "deg2rad",
92 | "rad2deg",
93 | "clamp",
94 | "width",
95 | "height",
96 | "mousePos",
97 | "mouseDeltaPos",
98 | "camPos",
99 | "camScale",
100 | "camRot",
101 | "center",
102 | "isFocused",
103 | "isTouchscreen",
104 | "drawSprite",
105 | "drawText",
106 | "formatText",
107 | "drawRect",
108 | "drawLine",
109 | "drawLines",
110 | "drawTriangle",
111 | "drawCircle",
112 | "drawEllipse",
113 | "drawUVQuad",
114 | "drawPolygon",
115 | "drawFormattedText",
116 | "drawMasked",
117 | "drawSubtracted",
118 | "pushTransform",
119 | "popTransform",
120 | "pushTranslate",
121 | "pushScale",
122 | "pushRotate",
123 | "pushMatrix",
124 | "LEFT",
125 | "RIGHT",
126 | "UP",
127 | "DOWN",
128 | "addKaboom",
129 | "debug",
130 | "Line",
131 | "Rect",
132 | "Circle",
133 | "Polygon",
134 | "Vec2",
135 | "Color",
136 | "Mat4",
137 | "Quad",
138 | "RNG",
139 | ] as const
140 |
141 | export type Button =
142 | | "action"
143 | | "left"
144 | | "right"
145 | | "up"
146 | | "down"
147 |
148 | export type LoadCtx = Pick
149 |
150 | export type GameAPI = {
151 | /**
152 | * Register an event that runs once when a button is pressed.
153 | */
154 | onButtonPress: (btn: Button, action: () => void) => EventController,
155 | /**
156 | * Register an event that runs once when a button is released.
157 | */
158 | onButtonRelease: (btn: Button, action: () => void) => EventController,
159 | /**
160 | * Register an event that runs every frame when a button is held down.
161 | */
162 | onButtonDown: (btn: Button, action: () => void) => EventController,
163 | /**
164 | * Register an event that runs once when timer runs out.
165 | */
166 | onTimeout: (action: () => void) => EventController,
167 | /**
168 | * Register an event that runs once when game ends, either succeeded, failed or timed out.
169 | */
170 | onEnd: (action: () => void) => EventController,
171 | /**
172 | * Run this when player succeeded in completing the game.
173 | */
174 | win: () => void,
175 | /**
176 | * Run this when player failed.
177 | */
178 | lose: () => void,
179 | /**
180 | * Current difficulty.
181 | */
182 | difficulty: 0 | 1 | 2,
183 | }
184 |
185 | export type GameCtx = Pick & GameAPI
186 |
187 | export type Game = {
188 | /**
189 | * Prompt of the mini game!
190 | */
191 | prompt: string,
192 | /**
193 | * Name of the author of the game.
194 | */
195 | author: string,
196 | /**
197 | * Hue of the background color (saturation: 27, lightness: 52)
198 | */
199 | hue?: number,
200 | /**
201 | * Assets URL prefix.
202 | */
203 | urlPrefix?: string,
204 | /**
205 | * Load assets.
206 | */
207 | onLoad?: (k: LoadCtx) => void,
208 | /**
209 | * Main entry of the game code. Should return a game object made by `k.make()` that contains the whole game.
210 | *
211 | * @example
212 | * ```js
213 | * ```
214 | */
215 | onStart: (ctx: GameCtx) => GameObj,
216 | }
217 |
218 | export type Opts = {
219 | /**
220 | * Development mode (no timer).
221 | */
222 | dev?: boolean,
223 | scale?: KaboomOpt["scale"],
224 | letterbox?: KaboomOpt["letterbox"],
225 | background?: KaboomOpt["background"],
226 | canvas?: KaboomOpt["canvas"],
227 | root?: KaboomOpt["root"],
228 | stretch?: KaboomOpt["stretch"],
229 | pixelDensity?: KaboomOpt["pixelDensity"],
230 | crisp?: KaboomOpt["crisp"],
231 | gamepads?: KaboomOpt["gamepads"],
232 | maxFPS?: KaboomOpt["maxFPS"],
233 | focus?: KaboomOpt["focus"],
234 | }
235 |
236 | export type KaboomWareCtx = {
237 | onChange: (action: (g: Game) => void) => EventController,
238 | curGame: () => Game,
239 | }
240 |
241 | export default function kaboomware(games: Game[], opt: Opts = {}): KaboomWareCtx {
242 |
243 | const k = kaboom({
244 | ...opt,
245 | font: "apl386o",
246 | width: 800,
247 | height: 600,
248 | })
249 |
250 | const origK = { ...k }
251 |
252 | const makeConfetti = useConfetti(k)
253 | const onChangeEvent = new k.Event<[Game]>()
254 |
255 | let curHue = 0.46
256 | let curPat = "heart"
257 |
258 | k.loadFont("apl386", apl386FontBytes, { filter: "linear" })
259 | k.loadFont("apl386o", apl386FontBytes, { outline: 8, filter: "linear" })
260 | k.loadSound("@cool", coolSoundBytes.buffer.slice(0))
261 | k.loadSound("@scream", screamSoundBytes.buffer.slice(0))
262 | k.loadSprite("@timer", timerSpriteUrl)
263 | k.loadSprite("@heart", heartSpriteUrl)
264 |
265 | const loadCtx = {}
266 |
267 | // TODO: report error msg when calling forbidden functions
268 | for (const api of loadAPIs) {
269 | loadCtx[api] = k[api]
270 | }
271 |
272 | const getGameID = (g: Game) => `${g.author}:${g.prompt}`
273 |
274 | // TODO: scope assets name
275 | for (const g of games) {
276 |
277 | if (g.onLoad) {
278 |
279 | // patch loadXXX() functions to scoped asset names
280 | const loaders = [
281 | "loadSprite",
282 | "loadSpriteAtlas",
283 | "loadAseprite",
284 | "loadPedit",
285 | "loadJSON",
286 | "loadSound",
287 | "loadFont",
288 | "loadBitmapFont",
289 | "loadShader",
290 | "loadShaderURL",
291 | ]
292 |
293 | for (const loader of loaders) {
294 | loadCtx[loader] = (name, ...args) => {
295 | if (typeof name === "string") {
296 | name = getGameID(g) + name
297 | }
298 | return k[loader](name, ...args)
299 | }
300 | }
301 |
302 | // patch loadRoot() to consider g.urlPrefix
303 | if (g.urlPrefix) {
304 | loadCtx["loadRoot"] = (p) => {
305 | if (p) k.loadRoot(g.urlPrefix + p)
306 | return k.loadRoot().slice(g.urlPrefix.length)
307 | }
308 | k.loadRoot(g.urlPrefix)
309 | } else {
310 | k.loadRoot("")
311 | }
312 |
313 | g.onLoad(loadCtx as LoadCtx)
314 | loadCtx["loadRoot"] = k.loadRoot
315 |
316 | }
317 |
318 | }
319 |
320 | const game = k.add([
321 | k.fixed(),
322 | k.pos(),
323 | shake(),
324 | ])
325 |
326 | game.onDraw(() => {
327 |
328 | const bg = k.hsl2rgb(curHue, BG_S, BG_L)
329 | const color = k.hsl2rgb(curHue, BG_S, BG_L - 0.04)
330 | const spr = k.getSprite("@" + curPat)
331 |
332 | if (!spr || !spr.data) return
333 |
334 | const w = spr.data.width
335 | const h = spr.data.height
336 | const gap = 32
337 | const pad = 100
338 | const speed = 40
339 | const ox = (k.time() * speed) % (w + gap)
340 | const oy = (k.time() * speed) % (h + gap)
341 | let offset = false
342 |
343 | k.drawRect({
344 | width: k.width(),
345 | height: k.height(),
346 | color: bg,
347 | })
348 |
349 | for (let x = -pad; x < k.width() + pad; x += w + gap) {
350 | for (let y = -pad; y < k.height() + pad; y += h + gap) {
351 | k.drawSprite({
352 | sprite: spr.data,
353 | color: color,
354 | pos: k.vec2(x + ox, y + oy + (offset ? (h + gap) / 2 : 0)),
355 | fixed: true,
356 | anchor: "center",
357 | })
358 | }
359 | // TODO: not working
360 | // offset = !offset
361 | }
362 |
363 | })
364 |
365 | const bloodEye = k.add([
366 | k.rect(k.width(), k.height()),
367 | k.color(255, 0, 0),
368 | k.z(1000),
369 | k.opacity(0),
370 | ])
371 |
372 | bloodEye.onUpdate(() => {
373 | bloodEye.opacity = k.lerp(bloodEye.opacity, 0, k.dt())
374 | })
375 |
376 | let score = 0
377 | let curGame = 0
378 |
379 | k.onLoad(() => {
380 |
381 | function nextGame() {
382 | curGame = (curGame + 1) % games.length
383 | runGame(games[curGame])
384 | }
385 |
386 | function runGame(g: Game) {
387 |
388 | onChangeEvent.trigger(g)
389 |
390 | if (g.prompt.length > 12) {
391 | throw new Error("Prompt cannot exceed 12 characters!")
392 | }
393 |
394 | game.removeAll()
395 | curHue = g.hue ?? k.rand(0, 1)
396 |
397 | const margin = 20
398 |
399 | const title = game.add([
400 | k.pos(margin, margin),
401 | k.scale(1),
402 | bounce(),
403 | k.z(100),
404 | k.text(g.prompt, {
405 | size: 40,
406 | // width: k.width() - margin * 2,
407 | lineSpacing: 16,
408 | transform: (idx, ch) => ({
409 | pos: k.vec2(0, k.wave(-1, 1, k.time() * 4 + idx * 0.5)),
410 | scale: k.wave(1, 1.05, k.time() * 4 + idx),
411 | angle: k.wave(-2, 2, k.time() * 4 + idx),
412 | }),
413 | }),
414 | ])
415 |
416 | // title.bounce(2, 8)
417 |
418 | const author = k.make([
419 | k.pos(12, 8),
420 | k.text(`by ${g.author}`, { size: 28, font: "apl386" }),
421 | ])
422 |
423 | const authorBox = game.add([
424 | k.pos(margin + title.width + 16, margin + 4),
425 | k.rect(
426 | author.width + author.pos.x * 2,
427 | author.height + author.pos.y * 2,
428 | { radius: 16 },
429 | ),
430 | k.color(k.hsl2rgb(curHue, BG_S, BG_L - 0.15)),
431 | ])
432 |
433 | authorBox.add(author)
434 |
435 | const marginTop = title.height + margin * 2 + 4
436 | const marginBottom = margin
437 | const marginLeft = margin
438 | const marginRight = 80
439 | const gw = k.width() - marginLeft - marginRight
440 | const gh = k.height() - marginTop - marginBottom
441 |
442 | game.add([
443 | k.sprite("@timer"),
444 | k.pos(k.width() - marginRight / 2, k.height() - marginBottom * 3),
445 | k.anchor("center"),
446 | k.scale(),
447 | {
448 | update() {
449 | this.scaleTo(k.wave(1, 1.05, k.time() * 8))
450 | },
451 | }
452 | ])
453 |
454 | const TIMER_BAR_HEIGHT = 400
455 |
456 | const timerBar = game.add([
457 | k.rect(16, TIMER_BAR_HEIGHT, { radius: 8 }),
458 | k.outline(4),
459 | k.pos(k.width() - marginRight / 2, k.height() - marginBottom - 88),
460 | k.color(k.hsl2rgb(curHue, BG_S, BG_L - 0.15)),
461 | k.opacity(1),
462 | k.anchor("bot"),
463 | ])
464 |
465 | const gameBox = game.add([
466 | k.pos(marginLeft, marginTop),
467 | k.rect(gw, gh, { radius: 16 }),
468 | k.mask(),
469 | ])
470 |
471 | game.add([
472 | {
473 | draw() {
474 | k.drawRect({
475 | pos: k.vec2(marginLeft, marginTop),
476 | width: gw,
477 | height: gh,
478 | radius: 16,
479 | fill: false,
480 | outline: {
481 | width: 4,
482 | color: k.rgb(0, 0, 0),
483 | },
484 | })
485 | },
486 | }
487 | ])
488 |
489 | const scene = gameBox.add([
490 | k.timer(),
491 | ])
492 |
493 | const onEndEvent = new k.Event()
494 | const onTimeoutEvent = new k.Event()
495 | let done = false
496 |
497 | const win = () => {
498 | if (done) return
499 | done = true
500 | gameTimer.cancel()
501 | onTimeoutEvent.clear()
502 | k.play("@cool")
503 | score += 1
504 | const conf = {
505 | count: 50,
506 | color: () => k.hsl2rgb(k.rand(), 0.64, 0.6),
507 | velocity: () => k.rand(1000, 4800),
508 | }
509 | k.add(makeConfetti({
510 | pos: k.vec2(0, k.height()),
511 | spread: 60,
512 | heading: 40,
513 | ...conf,
514 | }))
515 | k.add(makeConfetti({
516 | pos: k.vec2(k.width(), k.height()),
517 | spread: 60,
518 | heading: -40,
519 | ...conf,
520 | }))
521 | scene.wait(2, () => {
522 | nextGame()
523 | onEndEvent.trigger()
524 | })
525 | }
526 |
527 | const lose = () => {
528 | if (done) return
529 | bloodEye.opacity = 0.5
530 | game.shake(24)
531 | done = true
532 | gameTimer.cancel()
533 | onTimeoutEvent.clear()
534 | k.play("@scream")
535 | scene.wait(2, () => {
536 | nextGame()
537 | onEndEvent.trigger()
538 | })
539 | }
540 |
541 | let time = 0
542 |
543 | const gameTimer = scene.onUpdate(() => {
544 | time += k.dt()
545 | const r = time / GAME_TIME
546 | timerBar.height = TIMER_BAR_HEIGHT * (1 - r)
547 | if (r >= 0.6) {
548 | timerBar.opacity = k.wave(0.5, 1, time * 16)
549 | }
550 | if (time >= GAME_TIME) {
551 | onTimeoutEvent.trigger()
552 | // TODO
553 | lose()
554 | }
555 | })
556 |
557 | if (opt.dev) {
558 | gameTimer.cancel()
559 | }
560 |
561 | const ctx = {}
562 |
563 | for (const api of gameAPIs) {
564 | ctx[api] = k[api]
565 | }
566 |
567 | // TODO: custom cam
568 | const api: GameAPI = {
569 | onButtonPress: (btn, action) => {
570 | if (btn === "action") {
571 | return k.EventController.join([
572 | scene.onKeyPress("space", action),
573 | scene.onMousePress("left", action),
574 | ])
575 | }
576 | return scene.onKeyPress(btn, action)
577 | },
578 | onButtonRelease: (btn, action) => {
579 | if (btn === "action") {
580 | return k.EventController.join([
581 | scene.onKeyRelease("space", action),
582 | scene.onMouseRelease("left", action),
583 | ])
584 | }
585 | return scene.onKeyRelease(btn, action)
586 | },
587 | onButtonDown: (btn, action) => {
588 | if (btn === "action") {
589 | return k.EventController.join([
590 | scene.onKeyDown("space", action),
591 | scene.onMouseDown("left", action),
592 | ])
593 | }
594 | return scene.onKeyDown(btn, action)
595 | },
596 | onTimeout: (action) => onTimeoutEvent.add(action),
597 | onEnd: (action) => onEndEvent.add(action),
598 | win: win,
599 | lose: lose,
600 | difficulty: 0,
601 | }
602 |
603 | // patch getXXX() functions to scoped asset names
604 | const getters = [
605 | "getSprite",
606 | "getSound",
607 | "getFont",
608 | "getBitmapFont",
609 | "getShader",
610 | "getAsset",
611 | ]
612 |
613 | for (const getter of getters) {
614 | k[getter] = (name: string) => {
615 | name = name.startsWith("@") ? name : getGameID(g) + name
616 | return origK[getter](name)
617 | }
618 | }
619 |
620 | const gameScene = g.onStart({
621 | ...ctx,
622 | width: () => gameBox.width,
623 | height: () => gameBox.height,
624 | mousePos: () => k.mousePos().sub(gameBox.pos),
625 | ...api,
626 | } as unknown as GameCtx)
627 |
628 | scene.add(gameScene)
629 |
630 | // const speech = new SpeechSynthesisUtterance(g.prompt)
631 | // speechSynthesis.speak(speech)
632 |
633 | }
634 |
635 | if (games[0]) {
636 | runGame(games[0])
637 | }
638 |
639 | })
640 |
641 | function shake() {
642 | let shake = 0
643 | return {
644 | shake(s) {
645 | shake = s
646 | },
647 | update() {
648 | shake = k.lerp(shake, 0, k.dt() * 4)
649 | this.pos = k.Vec2.fromAngle(k.rand(0, 360)).scale(shake)
650 | },
651 | }
652 | }
653 |
654 | function bounce() {
655 | let time = 0
656 | let bouncing = null
657 | return {
658 | require: [ "scale" ],
659 | bounce(scale: number = 1.2, speed: number = 1) {
660 | time = 0
661 | bouncing = {
662 | scale: scale,
663 | speed: speed,
664 | }
665 | },
666 | update() {
667 | if (!bouncing) return
668 | time += k.dt()
669 | let s = k.wave(1, bouncing.scale, time * bouncing.speed)
670 | const cycle = Math.PI * 2 / bouncing.speed
671 | if (time >= cycle) {
672 | bouncing = null
673 | time = 0
674 | s = 1
675 | }
676 | this.scaleTo(s)
677 | },
678 | }
679 | }
680 |
681 | return {
682 | onChange: (action) => onChangeEvent.add(action),
683 | curGame: () => games[curGame],
684 | }
685 |
686 | }
687 |
--------------------------------------------------------------------------------