├── .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 | ![logo](logo.png) 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 | --------------------------------------------------------------------------------