diff --git a/package.json b/package.json index 16f9a8d..b365cca 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "kaplayground", "type": "module", "version": "2.0.0", + "bin": "scripts/cli.js", "scripts": { "install": "git submodule update --init --recursive && cd kaplay && pnpm i", "dev": "vite dev", @@ -10,12 +11,12 @@ "preview": "vite preview", "generate:examples": "node --experimental-strip-types scripts/examples.ts", "fmt": "dprint fmt", - "check": "tsc --noEmit --p tsconfig.app.json" + "check": "tsc --noEmit --p tsconfig.app.json", + "dev:bin": "node scripts/cli.js --examples=fakeExamples" }, "dependencies": { "@formkit/drag-and-drop": "^0.0.38", "@kaplayjs/crew": "^1.6.1", - "@kaplayjs/dprint-config": "^1.1.0", "@monaco-editor/react": "^4.6.0", "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-tabs": "^1.1.0", @@ -37,6 +38,7 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@kaplayjs/dprint-config": "^1.1.0", "@types/node": "^22.7.0", "@types/pako": "^2.0.3", "@types/react": "^18.3.1", @@ -48,6 +50,7 @@ "monaco-editor": "^0.48.0", "postcss": "^8.4.41", "vite": "^5.4.2", + "vite-plugin-custom-env": "^1.0.3", "vite-plugin-static-copy": "^1.0.6" }, "packageManager": "pnpm@9.6.0+sha512.38dc6fba8dba35b39340b9700112c2fe1e12f10b17134715a4aa98ccf7bb035e76fd981cf0bb384dfa98f8d6af5481c2bef2f4266a24bfa20c34eb7147ce0b5e" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a29c766..14f8a72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: '@kaplayjs/crew': specifier: ^1.6.1 version: 1.6.1(@types/node@22.7.0)(typescript@5.4.5) - '@kaplayjs/dprint-config': - specifier: ^1.1.0 - version: 1.1.0 '@monaco-editor/react': specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.48.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -75,6 +72,9 @@ importers: specifier: ^4.5.2 version: 4.5.2(@types/react@18.3.1)(react@18.3.1) devDependencies: + '@kaplayjs/dprint-config': + specifier: ^1.1.0 + version: 1.1.0 '@types/node': specifier: ^22.7.0 version: 22.7.0 @@ -108,6 +108,9 @@ importers: vite: specifier: ^5.4.2 version: 5.4.2(@types/node@22.7.0) + vite-plugin-custom-env: + specifier: ^1.0.3 + version: 1.0.3 vite-plugin-static-copy: specifier: ^1.0.6 version: 1.0.6(vite@5.4.2(@types/node@22.7.0)) @@ -1660,6 +1663,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vite-plugin-custom-env@1.0.3: + resolution: {integrity: sha512-ik325Yd4iX1fwVhUsRGTmyH/r0iIQmfeJZiot2uJa/a5z5yaOlOk6TldQmrz8lFaogzn6d3Hc+hdHMq9AqzU5w==} + vite-plugin-static-copy@1.0.6: resolution: {integrity: sha512-3uSvsMwDVFZRitqoWHj0t4137Kz7UynnJeq1EZlRW7e25h2068fyIZX4ORCCOAkfp1FklGxJNVJBkBOD+PZIew==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3182,6 +3188,8 @@ snapshots: util-deprecate@1.0.2: {} + vite-plugin-custom-env@1.0.3: {} + vite-plugin-static-copy@1.0.6(vite@5.4.2(@types/node@22.7.0)): dependencies: chokidar: 3.6.0 diff --git a/scripts/cli.js b/scripts/cli.js new file mode 100755 index 0000000..d2dfa45 --- /dev/null +++ b/scripts/cli.js @@ -0,0 +1,25 @@ +import { createServer } from "vite"; +import { VitePluginEnv } from "vite-plugin-custom-env"; + +// Get process and options +const args = process.argv.slice(2) ?? []; + +const options = args.reduce((acc, arg) => { + const [key, value] = arg.split("="); + acc[key.replace(/^-{1,2}/, "")] = value; + return acc; +}, {}); + +process.env.EXAMPLES_PATH = options.examples || "examples"; + +const server = await createServer({ + plugins: [ + VitePluginEnv({ + "VITE_USE_FILE": "true", + }), + ], +}); +await server.listen(); + +server.printUrls(); +server.bindCLIShortcuts({ print: true }); diff --git a/scripts/examples.ts b/scripts/examples.ts index 441f3b4..26da260 100644 --- a/scripts/examples.ts +++ b/scripts/examples.ts @@ -4,23 +4,32 @@ import fs from "fs"; import path from "path"; -const examplesPath = path.join(import.meta.dirname, "..", "kaplay", "examples"); +const defaultExamplesPath = path.join( + import.meta.dirname, + "..", + "kaplay", + "examples", +); const distPath = path.join(import.meta.dirname, "..", "src", "data"); -let exampleCount = 0; +export const generateExamples = async (examplesPath = defaultExamplesPath) => { + let exampleCount = 0; -const examples = fs.readdirSync(examplesPath).map((file) => { - if (!file.endsWith(".js")) return null; + const examples = fs.readdirSync(examplesPath).map((file) => { + if (!file.endsWith(".js")) return null; - const filePath = path.join(examplesPath, file); - const code = fs.readFileSync(filePath, "utf-8"); - const name = file.replace(".js", ""); + const filePath = path.join(examplesPath, file); + const code = fs.readFileSync(filePath, "utf-8"); + const name = file.replace(".js", ""); - return { name, code, index: (exampleCount++).toString() }; -}); + return { name, code, index: (exampleCount++).toString() }; + }); -// Write a JSON file with the examples -fs.writeFileSync( - path.join(distPath, "examples.json"), - JSON.stringify(examples.filter(Boolean), null, 4), -); + // Write a JSON file with the examples + fs.writeFileSync( + path.join(distPath, "exampleList.json"), + JSON.stringify(examples.filter(Boolean), null, 4), + ); + + console.log("Generated exampleList.json"); +}; diff --git a/src/App.tsx b/src/App.tsx index 2332319..fcc0d6d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,11 @@ import "react-toastify/dist/ReactToastify.css"; import "allotment/dist/style.css"; import "./styles/index.css"; import "./styles/toast.css"; +import "./util/hotkeys.js"; + +export const dynamicConfig = { + useFile: Boolean(import.meta.env.VITE_USE_FILE), +}; export const App = () => { return ; diff --git a/src/components/Editor/monacoConfig.ts b/src/components/Editor/monacoConfig.ts index 61057de..d2470f7 100644 --- a/src/components/Editor/monacoConfig.ts +++ b/src/components/Editor/monacoConfig.ts @@ -14,7 +14,7 @@ export const configMonaco = (monaco: Monaco) => { // Add the KAPLAY module monaco.languages.typescript.javascriptDefaults.addExtraLib( kaplayModule, - "kaplay.d.ts", + "types.d.ts", ); monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ diff --git a/src/data/exampleList.json b/src/data/exampleList.json index 0d10f34..51f2aee 100644 --- a/src/data/exampleList.json +++ b/src/data/exampleList.json @@ -1,397 +1,387 @@ [ - { - "name": "add", - "code": "// @ts-check\n\n// Adding game objects to screen\n\n// Start a KAPLAY game\nkaplay();\n\n// Load a sprite asset from \"sprites/bean.png\", with the name \"bean\"\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// A \"Game Object\" is the basic unit of entity in KAPLAY\n// Game objects are composed from components\n// Each component gives a game object certain capabilities\n\n// add() assembles a game object from a list of components and add to game, returns the reference of the game object\nconst player = add([\n sprite(\"bean\"), // sprite() component makes it render as a sprite\n pos(120, 80), // pos() component gives it position, also enables movement\n rotate(0), // rotate() component gives it rotation\n anchor(\"center\"), // anchor() component defines the pivot point (defaults to \"topleft\")\n]);\n\n// .onUpdate() is a method on all game objects, it registers an event that runs every frame\nplayer.onUpdate(() => {\n // .angle is a property provided by rotate() component, here we're incrementing the angle by 120 degrees per second, dt() is the time elapsed since last frame in seconds\n player.angle += 120 * dt();\n});\n\n// Add multiple game objects\nfor (let i = 0; i < 3; i++) {\n // generate a random point on screen\n // width() and height() gives the game dimension\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n sprite(\"ghosty\"),\n pos(x, y),\n ]);\n}\n", - "index": "0" - }, - { - "name": "ai", - "code": "// @ts-check\n\n// Use state() component to handle basic AI\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst SPEED = 320;\nconst ENEMY_SPEED = 160;\nconst BULLET_SPEED = 800;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 80),\n area(),\n anchor(\"center\"),\n]);\n\nconst enemy = add([\n sprite(\"ghosty\"),\n pos(width() - 80, height() - 80),\n anchor(\"center\"),\n // This enemy cycle between 3 states, and start from \"idle\" state\n state(\"move\", [\"idle\", \"attack\", \"move\"]),\n]);\n\n// Run the callback once every time we enter \"idle\" state.\n// Here we stay \"idle\" for 0.5 second, then enter \"attack\" state.\nenemy.onStateEnter(\"idle\", async () => {\n await wait(0.5);\n enemy.enterState(\"attack\");\n});\n\n// When we enter \"attack\" state, we fire a bullet, and enter \"move\" state after 1 sec\nenemy.onStateEnter(\"attack\", async () => {\n // Don't do anything if player doesn't exist anymore\n if (player.exists()) {\n const dir = player.pos.sub(enemy.pos).unit();\n\n add([\n pos(enemy.pos),\n move(dir, BULLET_SPEED),\n rect(12, 12),\n area(),\n offscreen({ destroy: true }),\n anchor(\"center\"),\n color(BLUE),\n \"bullet\",\n ]);\n }\n\n await wait(1);\n enemy.enterState(\"move\");\n});\n\nenemy.onStateEnter(\"move\", async () => {\n await wait(2);\n enemy.enterState(\"idle\");\n});\n\n// Like .onUpdate() which runs every frame, but only runs when the current state is \"move\"\n// Here we move towards the player every frame if the current state is \"move\"\nenemy.onStateUpdate(\"move\", () => {\n if (!player.exists()) return;\n const dir = player.pos.sub(enemy.pos).unit();\n enemy.move(dir.scale(ENEMY_SPEED));\n});\n\n// Taking a bullet makes us disappear\nplayer.onCollide(\"bullet\", (bullet) => {\n destroy(bullet);\n destroy(player);\n addKaboom(bullet.pos);\n});\n\n// Register input handlers & movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n", - "index": "1" - }, - { - "name": "animation", - "code": "// @ts-check\n\n// Start kaboom\nkaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\n// Rotating\nconst rotatingBean = add([\n sprite(\"bean\"),\n pos(50, 50),\n anchor(\"center\"),\n rotate(0),\n animate(),\n]);\n\n// Trying sprite change\nrotatingBean.sprite = \"bag\";\n\nrotatingBean.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"forward\",\n});\n\n// Moving right to left using ping-pong\nconst movingBean = add([\n sprite(\"bean\"),\n pos(50, 150),\n anchor(\"center\"),\n animate(),\n]);\n\nmovingBean.animate(\"pos\", [vec2(50, 150), vec2(150, 150)], {\n duration: 2,\n direction: \"ping-pong\",\n});\n\n// Same animation as before, but relative to the spawn position\nconst secondMovingBean = add([\n sprite(\"bean\"),\n pos(150, 0),\n anchor(\"center\"),\n animate({ relative: true }),\n]);\n\nsecondMovingBean.animate(\"pos\", [vec2(50, 150), vec2(150, 150)], {\n duration: 2,\n direction: \"ping-pong\",\n});\n\n// Changing color using a color list\nconst coloringBean = add([\n sprite(\"bean\"),\n pos(50, 300),\n anchor(\"center\"),\n color(WHITE),\n animate(),\n]);\n\ncoloringBean.animate(\"color\", [WHITE, RED, GREEN, BLUE, WHITE], {\n duration: 8,\n});\n\n// Changing opacity using an opacity list\nconst opacitingBean = add([\n sprite(\"bean\"),\n pos(150, 300),\n anchor(\"center\"),\n opacity(1),\n animate(),\n]);\n\nopacitingBean.animate(\"opacity\", [1, 0, 1], {\n duration: 8,\n easing: easings.easeInOutCubic,\n});\n\n// Moving in a square like motion\nconst squaringBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate(),\n]);\n\nsquaringBean.animate(\n \"pos\",\n [\n vec2(50, 400),\n vec2(150, 400),\n vec2(150, 500),\n vec2(50, 500),\n vec2(50, 400),\n ],\n { duration: 8 },\n);\n\n// Moving in a square like motion, but with custom spaced keyframes\nconst timedSquaringBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate(),\n]);\n\ntimedSquaringBean.animate(\n \"pos\",\n [\n vec2(50, 400),\n vec2(150, 400),\n vec2(150, 500),\n vec2(50, 500),\n vec2(50, 400),\n ],\n {\n duration: 8,\n timing: [\n 0,\n 0.1,\n 0.3,\n 0.7,\n 1.0,\n ],\n },\n);\n\n// Using spline interpolation to move according to a smoothened path\nconst curvingBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate({ followMotion: true }),\n rotate(0),\n]);\n\ncurvingBean.animate(\n \"pos\",\n [\n vec2(200, 400),\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500),\n vec2(400, 400),\n ],\n { duration: 8, direction: \"ping-pong\", interpolation: \"spline\" },\n);\n\nconst littleBeanPivot = curvingBean.add([\n animate(),\n rotate(0),\n named(\"littlebeanpivot\"),\n]);\n\nlittleBeanPivot.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"reverse\",\n});\n\nconst littleBean = littleBeanPivot.add([\n sprite(\"bean\"),\n pos(50, 50),\n anchor(\"center\"),\n scale(0.25),\n animate(),\n rotate(0),\n named(\"littlebean\"),\n]);\n\nlittleBean.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"forward\",\n});\n\nconsole.log(JSON.stringify(serializeAnimation(curvingBean, \"root\"), \"\", 2));\n\n/*onDraw(() => {\n drawCurve(t => evaluateCatmullRom(\n vec2(200, 400),\\\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500), t), { color: RED })\n drawCurve(catmullRom(\n vec2(200, 400),\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500)), { color: GREEN })\n})*/\n", - "index": "2" - }, - { - "name": "audio", - "code": "// @ts-check\n\n// audio playback & control\n\nkaplay({\n // Don't pause audio when tab is not active\n backgroundAudio: true,\n background: [0, 0, 0],\n});\n\nloadSound(\"bell\", \"/examples/sounds/bell.mp3\");\nloadSound(\"OtherworldlyFoe\", \"/examples/sounds/OtherworldlyFoe.mp3\");\n\n// play() to play audio\n// (This might not play until user input due to browser policy)\nconst music = play(\"OtherworldlyFoe\", {\n loop: true,\n paused: true,\n});\n\n// Adjust global volume\nvolume(0.5);\n\nconst label = add([\n text(),\n]);\n\nfunction updateText() {\n label.text = `\n${music.paused ? \"Paused\" : \"Playing\"}\nTime: ${music.time().toFixed(2)}\nVolume: ${music.volume.toFixed(2)}\nSpeed: ${music.speed.toFixed(2)}\n\n[space] play/pause\n[up/down] volume\n[left/right] speed\n\t`.trim();\n}\n\nupdateText();\n\n// Update text every frame\nonUpdate(updateText);\n\n// Adjust music properties through input\nonKeyPress(\"space\", () => music.paused = !music.paused);\nonKeyPressRepeat(\"up\", () => music.volume += 0.1);\nonKeyPressRepeat(\"down\", () => music.volume -= 0.1);\nonKeyPressRepeat(\"left\", () => music.speed -= 0.1);\nonKeyPressRepeat(\"right\", () => music.speed += 0.1);\nonKeyPress(\"m\", () => music.seek(4.24));\n\nconst keyboard = \"awsedftgyhujk\";\n\n// Simple piano with \"bell\" sound and the second row of a QWERTY keyboard\nfor (let i = 0; i < keyboard.length; i++) {\n onKeyPress(keyboard[i], () => {\n play(\"bell\", {\n // The original \"bell\" sound is F, -500 will make it C for the first key\n detune: i * 100 - 500,\n });\n });\n}\n\n// Draw music progress bar\nonDraw(() => {\n if (!music.duration()) return;\n const h = 16;\n drawRect({\n pos: vec2(0, height() - h),\n width: music.time() / music.duration() * width(),\n height: h,\n });\n});\n", - "index": "3" - }, - { - "name": "bench", - "code": "// @ts-config\n\n// bench marking sprite rendering performance\n\nkaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\nfor (let i = 0; i < 5000; i++) {\n add([\n sprite(i % 2 === 0 ? \"bean\" : \"bag\"),\n pos(rand(0, width()), rand(0, height())),\n anchor(\"center\"),\n ]);\n}\n\nonDraw(() => {\n drawText({\n text: debug.fps(),\n pos: vec2(width() / 2, height() / 2),\n anchor: \"center\",\n color: rgb(255, 127, 255),\n });\n});\n", - "index": "4" - }, - { - "name": "binding", - "code": "// @ts-check\n\nkaplay({\n buttons: {\n \"jump\": {\n gamepad: [\"south\"],\n keyboard: [\"up\", \"w\"],\n mouse: \"left\",\n },\n \"inspect\": {\n gamepad: \"east\",\n keyboard: \"f\",\n mouse: \"right\",\n },\n },\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Set the gravity acceleration (pixels per second)\nsetGravity(1600);\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n area(),\n // body() component gives the ability to respond to gravity\n body(),\n]);\n\n// Add a platform to hold the player\nadd([\n rect(width(), 48),\n outline(4),\n area(),\n pos(0, height() - 48),\n // Give objects a body() component if you don't want other solid objects pass through\n body({ isStatic: true }),\n]);\n\nadd([\n text(\"Press jump button\", { width: width() / 2 }),\n pos(12, 12),\n]);\n\nonButtonPress(\"jump\", () => {\n debug.log(getLastInputDeviceType());\n\n if (player.isGrounded()) {\n // .jump() is provided by body()\n player.jump();\n }\n});\n\nonButtonDown(\"inspect\", () => {\n debug.log(\"inspecting\");\n});\n", - "index": "5" - }, - { - "name": "burp", - "code": "// @ts-check\n\n// Start the game in burp mode\nkaplay({\n burp: true,\n});\n\n// \"b\" triggers a burp in burp mode\nadd([\n text(\"press b\"),\n]);\n\n// burp() on click / tap for our friends on mobile\nonClick(burp);\n", - "index": "6" - }, - { - "name": "button", - "code": "// @ts-check\n\n// Simple Button UI\n\nkaplay({\n background: [135, 62, 132],\n});\n\n// reset cursor to default on frame start for easier cursor management\nonUpdate(() => setCursor(\"default\"));\n\nfunction addButton(txt, p, f) {\n // add a parent background object\n const btn = add([\n rect(240, 80, { radius: 8 }),\n pos(p),\n area(),\n scale(1),\n anchor(\"center\"),\n outline(4),\n color(0, 0, 0),\n ]);\n\n // add a child object that displays the text\n btn.add([\n text(txt),\n anchor(\"center\"),\n color(0, 0, 0),\n ]);\n\n // onHoverUpdate() comes from area() component\n // it runs every frame when the object is being hovered\n btn.onHoverUpdate(() => {\n const t = time() * 10;\n btn.color = hsl2rgb((t / 10) % 1, 0.6, 0.7);\n btn.scale = vec2(1.2);\n setCursor(\"pointer\");\n });\n\n // onHoverEnd() comes from area() component\n // it runs once when the object stopped being hovered\n btn.onHoverEnd(() => {\n btn.scale = vec2(1);\n btn.color = rgb();\n });\n\n // onClick() comes from area() component\n // it runs once when the object is clicked\n btn.onClick(f);\n\n return btn;\n}\n\naddButton(\"Start\", vec2(200, 100), () => debug.log(\"oh hi\"));\naddButton(\"Quit\", vec2(200, 200), () => debug.log(\"bye\"));\n", - "index": "7" - }, - { - "name": "camera", - "code": "// @ts-check\n\n// Adjust camera / viewport\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst SPEED = 480;\n\nsetGravity(2400);\n\n// Setup a basic level\nconst level = addLevel([\n \"@ = $\",\n \"=======\",\n], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(100, 200),\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\nplayer.onUpdate(() => {\n // Set the viewport center to player.pos\n camPos(player.worldPos());\n});\n\nplayer.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.worldPos());\n});\n\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n score++;\n // Zoooom in!\n camScale(2);\n});\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => player.move(-SPEED, 0));\nonKeyDown(\"right\", () => player.move(SPEED, 0));\n\nlet score = 0;\n\n// Add a ui layer with fixed() component to make the object\n// not affected by camera\nconst ui = add([\n fixed(),\n]);\n\n// Add a score counter\nui.add([\n text(\"0\"),\n pos(12),\n {\n update() {\n this.text = score;\n },\n },\n]);\n\nonClick(() => {\n // Use toWorld() to transform a screen-space coordinate (like mousePos()) to\n // the world-space coordinate, which has the camera transform applied\n addKaboom(toWorld(mousePos()));\n});\n", - "index": "8" - }, - { - "name": "children", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst nucleus = add([\n sprite(\"ghosty\"),\n pos(center()),\n anchor(\"center\"),\n]);\n\n// Add children\nfor (let i = 12; i < 24; i++) {\n nucleus.add([\n sprite(\"bean\"),\n rotate(0),\n anchor(vec2(i).scale(0.25)),\n {\n speed: i * 8,\n },\n ]);\n}\n\nnucleus.onUpdate(() => {\n nucleus.pos = mousePos();\n\n // update children\n nucleus.children.forEach((child) => {\n child.angle += child.speed * dt();\n });\n});\n", - "index": "9" - }, - { - "name": "collision", - "code": "// @ts-check\n\n// Collision handling\n\n// Start kaboom\nkaplay({\n scale: 2,\n});\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\n\n// Define player movement speed\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 40),\n color(),\n rotate(0),\n // area() component gives the object a collider, which enables collision checking\n area(),\n // area({ shape: new Polygon([vec2(0), vec2(100), vec2(-100, 100)]) }),\n // area({ shape: new Rect(vec2(0), 12, 120) }),\n // area({ scale: 0.5 }),\n // body() component makes an object respond to physics\n body(),\n]);\n\n// Register input handlers & movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\nonKeyDown(\"q\", () => {\n player.angle -= SPEED * dt();\n});\n\nonKeyDown(\"e\", () => {\n player.angle += SPEED * dt();\n});\n\n// Add enemies\nfor (let i = 0; i < 3; i++) {\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n sprite(\"ghosty\"),\n pos(x, y),\n // Both objects must have area() component to enable collision detection between\n area(),\n \"enemy\",\n ]);\n}\n\nadd([\n sprite(\"grass\"),\n pos(center()),\n area(),\n // This game object also has isStatic, so our player won't be able to move pass this\n body({ isStatic: true }),\n \"grass\",\n]);\n\nadd([\n sprite(\"steel\"),\n pos(100, 200),\n area(),\n // This will not be static, but have a big mass that's hard to push over\n body({ mass: 10 }),\n]);\n\n// .onCollide() is provided by area() component, it registers an event that runs when an objects collides with another object with certain tag\n// In this case we destroy (remove from game) the enemy when player hits one\nplayer.onCollide(\"enemy\", (enemy) => {\n destroy(enemy);\n});\n\n// .onCollideUpdate() runs every frame when an object collides with another object\nplayer.onCollideUpdate(\"enemy\", () => {\n});\n\n// .onCollideEnd() runs once when an object stopped colliding with another object\nplayer.onCollideEnd(\"grass\", (a) => {\n debug.log(\"leave grass\");\n});\n\n// .clicks() is provided by area() component, it registers an event that runs when the object is clicked\nplayer.onClick(() => {\n debug.log(\"what up\");\n});\n\nplayer.onUpdate(() => {\n // .isHovering() is provided by area() component, which returns a boolean of if the object is currently being hovered on\n if (player.isHovering()) {\n player.color = rgb(0, 0, 255);\n }\n else {\n player.color = rgb();\n }\n});\n\n// Enter inspect mode, which shows the collider outline of each object with area() component, handy for debugging\n// Can also be toggled by pressing F1\ndebug.inspect = true;\n\n// Check out https://kaboomjs.com#AreaComp for everything area() provides\n", - "index": "10" - }, - { - "name": "collisionshapes", - "code": "// @ts-check\n\nkaplay();\n\nsetGravity(300);\n\nadd([\n pos(0, 400),\n rect(width(), 40),\n area(),\n body({ isStatic: true }),\n]);\n\n// Continuous shapes\nloop(1, () => {\n add([\n pos(width() / 2 + rand(-50, 50), 100),\n choose([\n rect(20, 20),\n circle(10),\n ellipse(20, 10),\n polygon([vec2(-15, 10), vec2(0, -10), vec2(15, 10)]),\n ]),\n color(RED),\n area(),\n body(),\n offscreen({ destroy: true, distance: 10 }),\n ]);\n if (getTreeRoot().children.length > 20) {\n destroy(getTreeRoot().children[1]);\n }\n});\n", - "index": "11" - }, - { - "name": "component", - "code": "// @ts-check\n// Custom component\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Components are just functions that returns an object that follows a certain format\nfunction funky() {\n // Can use local closed variables to store component state\n let isFunky = false;\n\n return {\n // ------------------\n // Special properties that controls the behavior of the component (all optional)\n\n // The name of the component\n id: \"funky\",\n // If this component depend on any other components\n require: [\"scale\", \"color\"],\n\n // Runs when the host object is added to the game\n add() {\n // E.g. Register some events from other components, do some bookkeeping, etc.\n },\n\n // Runs every frame as long as the host object exists\n update() {\n if (!isFunky) return;\n\n // \"this\" in all component methods refers to the host game object\n // Here we're updating some properties provided by other components\n this.color = rgb(rand(0, 255), rand(0, 255), rand(0, 255));\n this.scale = vec2(rand(1, 2));\n },\n\n // Runs every frame (after update) as long as the host object exists\n draw() {\n // E.g. Custom drawXXX() operations.\n },\n\n // Runs when the host object is destroyed\n destroy() {\n // E.g. Clean up event handlers, etc.\n },\n\n // Get the info to present in inspect mode\n inspect() {\n return isFunky ? \"on\" : \"off\";\n },\n\n // ------------------\n // All other properties and methods are directly assigned to the host object\n\n getFunky() {\n isFunky = true;\n },\n };\n}\n\nconst bean = add([\n sprite(\"bean\"),\n pos(center()),\n anchor(\"center\"),\n scale(1),\n color(),\n area(),\n // Use our component here\n funky(),\n // Tags are empty components, it's equivalent to a { id: \"friend\" }\n \"friend\",\n // Plain objects here are components too and work the same way, except unnamed\n {\n coolness: 100,\n friends: [],\n },\n]);\n\nonKeyPress(\"space\", () => {\n // .coolness is from our unnamed component above\n if (bean.coolness >= 100) {\n // We can use .getFunky() provided by the funky() component now\n bean.getFunky();\n }\n});\n\nonKeyPress(\"r\", () => {\n // .use() is on every game object, it adds a component at runtime\n bean.use(rotate(rand(0, 360)));\n});\n\nonKeyPress(\"escape\", () => {\n // .unuse() removes a component from the game object\n bean.unuse(\"funky\");\n});\n\nadd([\n text(\"Press space to get funky\", { width: width() }),\n pos(12, 12),\n]);\n", - "index": "12" - }, - { - "name": "concert", - "code": "// @ts-check\n\n// bean is holding a concert to celebrate kaboom2000!\n\nkaplay({\n scale: 0.7,\n background: [128, 180, 255],\n font: \"happy\",\n});\n\nloadSprite(\"bag\", `/sprites/bag.png`);\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"bobo\", `/sprites/bobo.png`);\nloadSprite(\"gigagantrum\", \"/sprites/gigagantrum.png\");\nloadSprite(\"tga\", \"/sprites/dino.png\");\nloadSprite(\"ghostiny\", \"/sprites/ghostiny.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"note\", \"/sprites/note.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"cloud\", \"/sprites/cloud.png\");\nloadSprite(\"sun\", \"/sprites/sun.png\");\nloadSound(\"bell\", \"/examples/sounds/bell.mp3\");\nloadSound(\"kaboom2000\", \"/examples/sounds/kaboom2000.mp3\");\nloadBitmapFont(\"happy\", \"/examples/fonts/happy_28x36.png\", 28, 36);\n\nconst friends = [\n \"bag\",\n \"bobo\",\n \"ghosty\",\n \"gigagantrum\",\n \"tga\",\n \"ghostiny\",\n];\n\nconst FLOOR_HEIGHT = 64;\nconst JUMP_FORCE = 1320;\nconst CAPTION_SPEED = 220;\nconst PLAYER_SPEED = 640;\n\nlet started = false;\nlet music = null;\nlet burping = false;\n\n// define gravity\nsetGravity(2400);\n\n// add a game object to screen\nconst player = add([\n // list of components\n sprite(\"bean\"),\n pos(width() / 2, height() - FLOOR_HEIGHT),\n area(),\n body(),\n anchor(\"bot\"),\n z(100),\n]);\n\nconst gw = 200;\nconst gh = 140;\nconst maxRow = 4;\nconst notes = [0, 2, 4, 5, 6, 7, 8, 9, 11, 12];\n\nfor (let i = 1; i <= maxRow; i++) {\n for (let j = 0; j < i; j++) {\n const n = i * (i - 1) / 2 + j;\n const w = (i - 1) * gw + 64;\n add([\n sprite(\"note\"),\n pos(\n j * gw + (width() - w) / 2 + 32,\n height() - FLOOR_HEIGHT + 24 - (maxRow - i + 1) * gh,\n ),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n color(hsl2rgb((n * 20) / 255, 0.6, 0.7)),\n bounce(),\n scale(1),\n n === 0 ? \"burp\" : \"note\",\n { detune: notes[9 - n] * 100 + -800 },\n ]);\n }\n}\n\nfunction bounce() {\n let bouncing = false;\n let timer = 0;\n return {\n id: \"bounce\",\n require: [\"scale\"],\n update() {\n if (bouncing) {\n timer += dt() * 20;\n const w = Math.sin(timer) * 0.1;\n if (w < 0) {\n bouncing = false;\n timer = 0;\n }\n else {\n this.scale = vec2(1 + w);\n }\n }\n },\n bounce() {\n bouncing = true;\n },\n };\n}\n\n// floor\nfor (let x = 0; x < width(); x += 64) {\n add([\n pos(x, height()),\n sprite(\"grass\"),\n anchor(\"botleft\"),\n area(),\n body({ isStatic: true }),\n ]);\n}\n\nfunction jump() {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n}\n\n// jump when user press space\nonKeyPress(\"space\", jump);\nonKeyDown(\"left\", () => player.move(-PLAYER_SPEED, 0));\nonKeyDown(\"right\", () => player.move(PLAYER_SPEED, 0));\n\nplayer.onHeadbutt((block) => {\n if (block.is(\"note\")) {\n play(\"bell\", {\n detune: block.detune,\n volume: 0.1,\n });\n addKaboom(block.pos);\n shake(1);\n block.bounce();\n if (!started) {\n started = true;\n caption.hidden = false;\n caption.paused = false;\n music = play(\"kaboom2000\");\n }\n }\n else if (block.is(\"burp\")) {\n burp();\n shake(480);\n if (music) music.paused = true;\n burping = true;\n player.paused = true;\n }\n});\n\nonUpdate(() => {\n if (!burping) return;\n camPos(camPos().lerp(player.pos, dt() * 3));\n camScale(camScale().lerp(vec2(5), dt() * 3));\n});\n\nconst lyrics =\n \"kaboom2000 is out today, i have to go and try it out now... oh it's so fun it's so fun it's so fun...... it's so fun it's so fun it's so fun!\";\n\nconst caption = add([\n text(lyrics, {\n transform(idx) {\n return {\n color: hsl2rgb(\n ((time() * 60 + idx * 20) % 255) / 255,\n 0.9,\n 0.6,\n ),\n scale: wave(1.4, 1.6, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n };\n },\n }),\n pos(width(), 32),\n move(LEFT, CAPTION_SPEED),\n]);\n\ncaption.hidden = true;\ncaption.paused = true;\n\nfunction funky() {\n let timer = 0;\n return {\n id: \"funky\",\n require: [\"pos\", \"rotate\"],\n update() {\n timer += dt();\n this.angle = wave(-9, 9, timer * 4);\n },\n };\n}\n\nfunction spawnCloud() {\n const dir = choose([LEFT, RIGHT]);\n\n add([\n sprite(\"cloud\", { flipX: dir.eq(LEFT) }),\n move(dir, rand(20, 60)),\n offscreen({ destroy: true }),\n pos(dir.eq(LEFT) ? width() : 0, rand(-20, 480)),\n anchor(\"top\"),\n area(),\n z(-50),\n ]);\n\n wait(rand(6, 12), spawnCloud);\n}\n\nfunction spawnFriend() {\n const friend = choose(friends);\n const dir = choose([LEFT, RIGHT]);\n\n add([\n sprite(friend, { flipX: dir.eq(LEFT) }),\n move(dir, rand(120, 320)),\n offscreen({ destroy: true }),\n pos(dir.eq(LEFT) ? width() : 0, height() - FLOOR_HEIGHT),\n area(),\n rotate(),\n funky(),\n anchor(\"bot\"),\n z(50),\n ]);\n\n wait(rand(1, 3), spawnFriend);\n}\n\nspawnCloud();\nspawnFriend();\n\nconst sun = add([\n sprite(\"sun\"),\n anchor(\"center\"),\n pos(width() - 90, 90),\n rotate(),\n z(-100),\n]);\n\nsun.onUpdate(() => {\n sun.angle += dt() * 12;\n});\n", - "index": "13" - }, - { - "name": "confetti", - "code": "// @ts-check\n\nkaplay();\n\nconst DEF_COUNT = 80;\nconst DEF_GRAVITY = 800;\nconst DEF_AIR_DRAG = 0.9;\nconst DEF_VELOCITY = [1000, 4000];\nconst DEF_ANGULAR_VELOCITY = [-200, 200];\nconst DEF_FADE = 0.3;\nconst DEF_SPREAD = 60;\nconst DEF_SPIN = [2, 8];\nconst DEF_SATURATION = 0.7;\nconst DEF_LIGHTNESS = 0.6;\n\nadd([\n text(\"click for confetti\"),\n anchor(\"top\"),\n pos(center().x, 0),\n]);\n\nfunction addConfetti(opt = {}) {\n const sample = (s) => typeof s === \"function\" ? s() : s;\n for (let i = 0; i < (opt.count ?? DEF_COUNT); i++) {\n const p = add([\n pos(sample(opt.pos ?? vec2(0, 0))),\n choose([\n rect(rand(5, 20), rand(5, 20)),\n circle(rand(3, 10)),\n ]),\n color(\n sample(\n opt.color\n ?? hsl2rgb(rand(0, 1), DEF_SATURATION, DEF_LIGHTNESS),\n ),\n ),\n opacity(1),\n lifespan(4),\n scale(1),\n anchor(\"center\"),\n rotate(rand(0, 360)),\n ]);\n\n const spin = rand(DEF_SPIN[0], DEF_SPIN[1]);\n const gravity = opt.gravity ?? DEF_GRAVITY;\n const airDrag = opt.airDrag ?? DEF_AIR_DRAG;\n const heading = sample(opt.heading ?? 0) - 90;\n const spread = opt.spread ?? DEF_SPREAD;\n const head = heading + rand(-spread / 2, spread / 2);\n const fade = opt.fade ?? DEF_FADE;\n const vel = sample(\n opt.velocity ?? rand(DEF_VELOCITY[0], DEF_VELOCITY[1]),\n );\n let velX = Math.cos(deg2rad(head)) * vel;\n let velY = Math.sin(deg2rad(head)) * vel;\n const velA = sample(\n opt.angularVelocity\n ?? rand(DEF_ANGULAR_VELOCITY[0], DEF_ANGULAR_VELOCITY[1]),\n );\n\n p.onUpdate(() => {\n velY += gravity * dt();\n p.pos.x += velX * dt();\n p.pos.y += velY * dt();\n p.angle += velA * dt();\n p.opacity -= fade * dt();\n velX *= airDrag;\n velY *= airDrag;\n p.scale.x = wave(-1, 1, time() * spin);\n });\n }\n}\n\nonKeyPress(\"space\", () => addConfetti({ pos: mousePos() }));\nonMousePress(() => addConfetti({ pos: mousePos() }));\n", - "index": "14" - }, - { - "name": "curves", - "code": "// @ts-check\n\nkaplay();\n\nfunction addPoint(c, ...args) {\n return add([\n \"point\",\n rect(8, 8),\n anchor(\"center\"),\n pos(...args),\n area(),\n color(c),\n ]);\n}\n\nfunction addBezier(...objects) {\n const points = [...objects];\n\n let t = 0;\n return add([\n pos(0, 0),\n {\n draw() {\n const coords = points.map(p => p.pos);\n const c = normalizedCurve(t => evaluateBezier(...coords, t));\n drawCurve(t => evaluateBezier(...coords, t), {\n segments: 25,\n width: 4,\n });\n drawLine({\n p1: points[0].pos,\n p2: points[1].pos,\n width: 2,\n color: rgb(0, 0, 255),\n });\n drawLine({\n p1: points[3].pos,\n p2: points[2].pos,\n width: 2,\n color: rgb(0, 0, 255),\n });\n for (let i = 0; i <= 10; i++) {\n const p = evaluateBezier(...coords, i / 10);\n drawCircle({\n pos: p,\n radius: 4,\n color: YELLOW,\n });\n }\n for (let i = 0; i <= 10; i++) {\n const p = c(i / 10);\n drawCircle({\n pos: p,\n radius: 8,\n color: RED,\n opacity: 0.5,\n });\n }\n },\n update() {\n },\n },\n ]);\n}\n\nfunction drawCatmullRom(a, b, c, d) {\n drawCurve(t => evaluateCatmullRom(a, b, c, d, t), {\n segments: 25,\n width: 4,\n });\n}\n\nfunction normalizedFirstDerivative(curve, curveFirstDerivative) {\n const curveLength = curveLengthApproximation(curve);\n const length = curveLength(1);\n return s => {\n const l = s * length;\n const t = curveLength(l, true);\n return curveFirstDerivative(t);\n };\n}\n\nfunction addCatmullRom(...objects) {\n const points = [...objects];\n\n let t = 0;\n return add([\n pos(0, 0),\n {\n draw() {\n const coords = points.map(p => p.pos);\n const first = coords[0].add(coords[0].sub(coords[1]));\n const last = coords[coords.length - 1].add(\n coords[coords.length - 1].sub(coords[coords.length - 2]),\n );\n let curve;\n let ct;\n const curveCoords = [\n [first, ...coords.slice(0, 3)],\n coords,\n [...coords.slice(1), last],\n ];\n const curveLengths = curveCoords.map(cc =>\n curveLengthApproximation(t => evaluateCatmullRom(...cc, t))(\n 1,\n )\n );\n const length = curveLengths.reduce((sum, l) => sum + l, 0);\n const p0 = curveLengths[0] / length;\n const p1 = curveLengths[1] / length;\n const p2 = curveLengths[2] / length;\n if (t <= p0) {\n curve = curveCoords[0];\n ct = t * (1 / p0);\n }\n else if (t <= p0 + p1) {\n curve = curveCoords[1];\n ct = (t - p0) * (1 / p1);\n }\n else {\n curve = curveCoords[2];\n ct = (t - p0 - p1) * (1 / p2);\n }\n const c = normalizedCurve(t => evaluateCatmullRom(...curve, t));\n const cd = normalizedFirstDerivative(\n t => evaluateCatmullRom(...curve, t),\n t => evaluateCatmullRomFirstDerivative(...curve, t),\n );\n\n drawCatmullRom(first, ...coords.slice(0, 3), {\n segments: 10,\n width: 4,\n });\n drawCatmullRom(...coords, { segments: 10, width: 4 });\n drawCatmullRom(...coords.slice(1), last, {\n segments: 10,\n width: 4,\n });\n\n const cartPos1 = evaluateCatmullRom(...curve, ct);\n const tangent1 = evaluateCatmullRomFirstDerivative(\n ...curve,\n ct,\n );\n pushTransform();\n pushTranslate(cartPos1);\n pushRotate(tangent1.angle(1, 0));\n drawRect({\n width: 16,\n height: 8,\n pos: vec2(-8, -4),\n color: YELLOW,\n outline: { color: BLUE, width: 4 },\n });\n popTransform();\n\n const cartPos2 = c(ct);\n const tangent2 = cd(ct);\n pushTransform();\n pushTranslate(cartPos2);\n pushRotate(tangent2.angle(1, 0));\n drawRect({\n width: 16,\n height: 8,\n pos: vec2(-8, -4),\n color: RED,\n opacity: 0.5,\n outline: { color: BLACK, width: 4 },\n });\n popTransform();\n },\n update() {\n t += dt() / 10;\n t = t % 1;\n },\n },\n ]);\n}\n\n// Interraction\nlet obj = null;\n\nonClick(\"point\", (point) => {\n obj = point;\n});\n\nonMouseMove((pos) => {\n if (obj) {\n obj.moveTo(pos);\n }\n});\n\nonMouseRelease((pos) => {\n obj = null;\n});\n\n// Scene creation\nconst p0 = addPoint(RED, 100, 40);\nconst p1 = addPoint(BLUE, 80, 120);\nconst p2 = addPoint(BLUE, 300, 60);\nconst p3 = addPoint(RED, 250, 200);\n\naddBezier(p0, p1, p2, p3);\n\nadd([\n pos(20, 300),\n text(\"yellow: default spacing\\nred: constant spacing\", { size: 20 }),\n]);\n\nconst c0 = addPoint(RED, 400, 40);\nconst c1 = addPoint(RED, 380, 120);\nconst c2 = addPoint(RED, 500, 60);\nconst c3 = addPoint(RED, 450, 200);\n\naddCatmullRom(c0, c1, c2, c3);\n\nadd([\n pos(400, 300),\n text(\"yellow: default speed\\nred: constant speed\", { size: 20 }),\n]);\n\nadd([\n pos(20, 350),\n text(\n \"curves are non-linear in t. This means that for a given t,\\nthe distance traveled from the start doesn't grow at constant speed.\\nTo fix this, turn the curve into a normalized curve first.\\nUse derivatives to find the direction of the curve at a certain t.\",\n { size: 20 },\n ),\n]);\n", - "index": "15" - }, - { - "name": "dialog", - "code": "// @ts-check\n\n// Simple dialogues with character avatars\n\nkaplay({\n background: \"#ffb879\",\n width: 640,\n height: 480,\n buttons: {\n \"next\": {\n keyboard: \"space\",\n mouse: \"left\",\n },\n },\n font: \"happy\",\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"mark\", \"/sprites/mark.png\");\nloadSound(\"bean_voice\", \"examples/sounds/bean_voice.wav\");\nloadSound(\"mark_voice\", \"examples/sounds/mark_voice.wav\");\nloadBitmapFont(\"happy\", \"/examples/fonts/happy_28x36.png\", 28, 36);\n\n// Define the characters data\nconst characters = {\n \"bean\": {\n \"sprite\": \"bean\",\n \"name\": \"Bean\",\n \"sound\": \"bean_voice\",\n },\n \"mark\": {\n \"sprite\": \"mark\",\n \"name\": \"Mark\",\n \"sound\": \"mark_voice\",\n },\n};\n\n// Define the dialogue data [character, text, effects]\nconst dialogs = [\n [\"bean\", \"[default]Oh hi![/default]\"],\n [\"mark\", \"[default]Hey! That's my line![/default]\"],\n [\"bean\", \"[default]What! Mark??? How did you get here?[/default]\"],\n [\n \"mark\",\n \"[default]Ohhi! I'm just here to say that[/default] [kaboom]Kaboom.js[/kaboom] [default]is awesome![/default]\",\n ],\n [\n \"bean\",\n \"[default]Yes but... Nobody uses[/default] [kaboom]Kaboom.js[/kaboom] [default]anymore![/default]\",\n ],\n [\"mark\", \"[surprised]What? Why?[/surprised]\", \"shake\"],\n [\n \"bean\",\n \"[default]Because everyone is using[/default] [kaplay]KAPLAY[/kaplay] [default]now![/default]\",\n ],\n [\"bean\", \"[default]It's the new hotness![/default]\"],\n [\"bean\", \"[default]And now they released the beta of v3001[/default]\"],\n [\"mark\", \"[default]Wow! And what is new on this version?[/default]\"],\n [\"bean\", \"[default]A lot of things, global input handlers...[/default]\"],\n [\n \"bean\",\n \"[default]New component animate() for animating anything![/default]\",\n ],\n [\n \"bean\",\n \"[default]Particles![/default]\",\n ],\n [\"bean\", \"[default]Physics, effectors...[/default]\"],\n [\n \"bean\",\n \"[default]Components for pathfinding like sentry(), patrol()...[/default]\",\n ],\n\n [\n \"bean\",\n \"[default]And much more![/default]\",\n ],\n [\"mark\", \"[default]Wow! That's amazing![/default]\"],\n [\"bean\", \"[default]And the most important thing...[/default]\"],\n [\n \"bean\",\n \"[default]Full compatibilty with[/default] [kaboom]Kaboom.js![/kaboom]\",\n ],\n [\"bean\", \"[default]So, what are you waiting for?[/default]\"],\n [\"bean\", \"[default]Go and try it now![/default]\"],\n];\n\n// Some effects data\nconst effects = {\n \"shake\": () => {\n shake();\n },\n};\n\nlet curDialog = 0;\nlet isTalking = false;\n\n// Text bubble\nconst textbox = add([\n rect(width() - 140, 140, { radius: 4 }),\n anchor(\"center\"),\n pos(center().x, height() - 100),\n outline(4),\n]);\n\n// Text\nconst txt = add([\n text(\"\", {\n size: 32,\n width: width() - 230,\n align: \"center\",\n styles: {\n \"default\": {\n color: BLACK,\n },\n \"kaplay\": (idx, ch) => ({\n color: Color.fromHex(\"#6bc96c\"),\n pos: vec2(0, wave(-4, 4, time() * 6 + idx * 0.5)),\n }),\n \"kaboom\": (idx, ch) => ({\n color: Color.fromHex(\"#ff004d\"),\n pos: vec2(0, wave(-4, 4, time() * 4 + idx * 0.5)),\n scale: wave(1, 1.2, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n }),\n // a jump effect\n \"surprised\": (idx, ch) => ({\n color: Color.fromHex(\"#8465ec\"),\n scale: wave(1, 1.2, time() * 1 + idx),\n pos: vec2(0, wave(0, 4, time() * 10)),\n }),\n \"hot\": (idx, ch) => ({\n color: Color.fromHex(\"#ff004d\"),\n scale: wave(1, 1.2, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n }),\n },\n transform: (idx, ch) => {\n return {\n opacity: idx < txt.letterCount ? 1 : 0,\n };\n },\n }),\n pos(textbox.pos),\n anchor(\"center\"),\n {\n letterCount: 0,\n },\n]);\n\n// Character avatar\nconst avatar = add([\n sprite(\"bean\"),\n scale(3),\n anchor(\"center\"),\n pos(center().sub(0, 50)),\n]);\n\nonButtonPress(\"next\", () => {\n if (isTalking) return;\n\n // Cycle through the dialogs\n curDialog = (curDialog + 1) % dialogs.length;\n updateDialog();\n});\n\n// Update the on screen sprite & text\nfunction updateDialog() {\n const [char, dialog, eff] = dialogs[curDialog];\n\n // Use a new sprite component to replace the old one\n avatar.use(sprite(characters[char].sprite));\n // Update the dialog text\n startWriting(dialog, char);\n\n if (eff) {\n effects[eff]();\n }\n}\n\nfunction startWriting(dialog, char) {\n isTalking = true;\n txt.letterCount = 0;\n txt.text = dialog;\n\n const writing = loop(0.05, () => {\n txt.letterCount = Math.min(\n txt.letterCount + 1,\n txt.renderedText.length,\n );\n play(characters[char].sound, {\n volume: 0.2,\n });\n\n if (txt.letterCount === txt.renderedText.length) {\n isTalking = false;\n writing.cancel();\n }\n });\n}\n\nonLoad(() => {\n updateDialog();\n});\n", - "index": "16" - }, - { - "name": "doublejump", - "code": "// @ts-check\n\nkaplay({\n background: [141, 183, 255],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSound(\"coin\", \"/examples/sounds/score.mp3\");\n\nsetGravity(4000);\n\nconst PLAYER_SPEED = 640;\nconst JUMP_FORCE = 1200;\nconst NUM_PLATFORMS = 5;\n\n// a spinning component for fun\nfunction spin(speed = 1200) {\n let spinning = false;\n return {\n require: [\"rotate\"],\n update() {\n if (!spinning) {\n return;\n }\n this.angle -= speed * dt();\n if (this.angle <= -360) {\n spinning = false;\n this.angle = 0;\n }\n },\n spin() {\n spinning = true;\n },\n };\n}\n\nscene(\"game\", () => {\n const score = add([\n text(\"0\", { size: 24 }),\n pos(24, 24),\n { value: 0 },\n ]);\n\n const bean = add([\n sprite(\"bean\"),\n area(),\n anchor(\"center\"),\n pos(0, 0),\n body({ jumpForce: JUMP_FORCE }),\n doubleJump(),\n rotate(0),\n spin(),\n ]);\n\n for (let i = 1; i < NUM_PLATFORMS; i++) {\n add([\n sprite(\"grass\"),\n area(),\n pos(rand(0, width()), i * height() / NUM_PLATFORMS),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"platform\",\n {\n speed: rand(120, 320),\n dir: choose([-1, 1]),\n },\n ]);\n }\n\n // go to the first platform\n bean.pos = get(\"platform\")[0].pos.sub(0, 64);\n\n function genCoin(avoid) {\n const plats = get(\"platform\");\n let idx = randi(0, plats.length);\n // avoid the spawning on the same platforms\n if (avoid != null) {\n idx = choose([...plats.keys()].filter((i) => i !== avoid));\n }\n const plat = plats[idx];\n add([\n pos(),\n anchor(\"center\"),\n sprite(\"coin\"),\n area(),\n follow(plat, vec2(0, -60)),\n \"coin\",\n { idx: idx },\n ]);\n }\n\n genCoin(0);\n\n for (let i = 0; i < width() / 64; i++) {\n add([\n pos(i * 64, height()),\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n scale(),\n \"danger\",\n ]);\n }\n\n bean.onCollide(\"danger\", () => {\n go(\"lose\");\n });\n\n bean.onCollide(\"coin\", (c) => {\n destroy(c);\n play(\"coin\");\n score.value += 1;\n score.text = score.value.toString();\n genCoin(c.idx);\n });\n\n // spin on double jump\n bean.onDoubleJump(() => {\n bean.spin();\n });\n\n onUpdate(\"platform\", (p) => {\n p.move(p.dir * p.speed, 0);\n if (p.pos.x < 0 || p.pos.x > width()) {\n p.dir = -p.dir;\n }\n });\n\n onKeyPress(\"space\", () => {\n bean.doubleJump();\n });\n\n function move(x) {\n bean.move(x, 0);\n if (bean.pos.x < 0) {\n bean.pos.x = width();\n }\n else if (bean.pos.x > width()) {\n bean.pos.x = 0;\n }\n }\n\n // both keys will trigger\n onKeyDown(\"left\", () => {\n move(-PLAYER_SPEED);\n });\n\n onKeyDown(\"right\", () => {\n move(PLAYER_SPEED);\n });\n\n onGamepadButtonPress(\"south\", () => bean.doubleJump());\n\n onGamepadStick(\"left\", (v) => {\n move(v.x * PLAYER_SPEED);\n });\n\n let timeLeft = 30;\n\n const timer = add([\n anchor(\"topright\"),\n pos(width() - 24, 24),\n text(timeLeft.toString()),\n ]);\n\n onUpdate(() => {\n timeLeft -= dt();\n if (timeLeft <= 0) {\n go(\"win\", score.value);\n }\n timer.text = timeLeft.toFixed(2);\n });\n});\n\nscene(\"win\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 80),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 80),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onGamepadButtonPress(\"south\", () => go(\"game\"));\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n ]);\n onKeyPress(\"space\", () => go(\"game\"));\n onGamepadButtonPress(\"south\", () => go(\"game\"));\n});\n\ngo(\"game\");\n", - "index": "17" - }, - { - "name": "drag", - "code": "// @ts-check\n\n// Drag & drop interaction\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Keep track of the current draggin item\nlet curDraggin = null;\n\n// A custom component for handling drag & drop behavior\nfunction drag() {\n // The displacement between object pos and mouse pos\n let offset = vec2(0);\n\n return {\n // Name of the component\n id: \"drag\",\n // This component requires the \"pos\" and \"area\" component to work\n require: [\"pos\", \"area\"],\n pick() {\n // Set the current global dragged object to this\n curDraggin = this;\n offset = mousePos().sub(this.pos);\n this.trigger(\"drag\");\n },\n // \"update\" is a lifecycle method gets called every frame the obj is in scene\n update() {\n if (curDraggin === this) {\n this.pos = mousePos().sub(offset);\n this.trigger(\"dragUpdate\");\n }\n },\n onDrag(action) {\n return this.on(\"drag\", action);\n },\n onDragUpdate(action) {\n return this.on(\"dragUpdate\", action);\n },\n onDragEnd(action) {\n return this.on(\"dragEnd\", action);\n },\n };\n}\n\n// Check if someone is picked\nonMousePress(() => {\n if (curDraggin) {\n return;\n }\n // Loop all \"bean\"s in reverse, so we pick the topmost one\n for (const obj of get(\"drag\").reverse()) {\n // If mouse is pressed and mouse position is inside, we pick\n if (obj.isHovering()) {\n obj.pick();\n break;\n }\n }\n});\n\n// Drop whatever is dragged on mouse release\nonMouseRelease(() => {\n if (curDraggin) {\n curDraggin.trigger(\"dragEnd\");\n curDraggin = null;\n }\n});\n\n// Reset cursor to default at frame start for easier cursor management\nonUpdate(() => setCursor(\"default\"));\n\n// Add dragable objects\nfor (let i = 0; i < 48; i++) {\n const bean = add([\n sprite(\"bean\"),\n pos(rand(width()), rand(height())),\n area({ cursor: \"pointer\" }),\n scale(5),\n anchor(\"center\"),\n // using our custom component here\n drag(),\n i !== 0 ? color(255, 255, 255) : color(255, 0, 255),\n \"bean\",\n ]);\n\n bean.onDrag(() => {\n // Remove the object and re-add it, so it'll be drawn on top\n readd(bean);\n });\n\n bean.onDragUpdate(() => {\n setCursor(\"move\");\n });\n}\n", - "index": "18" - }, - { - "name": "draw", - "code": "// @ts-check\n\n// Kaboom as pure rendering lib (no component / game obj etc.)\n\nkaplay();\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nloadShader(\n \"spiral\",\n null,\n `\nuniform float u_time;\nuniform vec2 u_mpos;\nvec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {\n\tvec2 pp = uv - u_mpos;\n\tfloat angle = atan(pp.y, pp.x);\n\tfloat dis = length(pp);\n\tfloat c = sin(dis * 48.0 + u_time * 8.0 + angle);\n\treturn vec4(c, c, c, 1);\n}\n`,\n);\n\nconst t = (n = 1) => time() * n;\nconst w = (a, b, n) => wave(a, b, t(n));\nconst px = 160;\nconst py = 160;\nconst doodles = [];\nconst trail = [];\n\n/** @type { import(\"../dist/declaration\").Outline } */\nconst outline = {\n width: 4,\n color: rgb(0, 0, 0),\n join: \"miter\",\n};\n\nfunction drawStuff() {\n const mx = (width() - px * 2) / 2;\n const my = (height() - py * 2) / 1;\n const p = (x, y) => vec2(x, y).scale(mx, my).add(px, py);\n\n drawSprite({\n sprite: \"bean\",\n pos: p(0, 0),\n angle: t(40),\n anchor: \"center\",\n scale: w(1, 1.5, 4),\n color: rgb(w(128, 255, 4), w(128, 255, 8), 255),\n });\n\n drawRect({\n pos: p(1, 0),\n width: w(60, 120, 4),\n height: w(100, 140, 8),\n anchor: \"center\",\n radius: w(0, 32, 4),\n angle: t(80),\n color: rgb(w(128, 255, 4), 255, w(128, 255, 8)),\n outline,\n });\n\n drawEllipse({\n pos: p(2, 0),\n radiusX: w(40, 70, 2),\n radiusY: w(40, 70, 4),\n start: 0,\n end: w(180, 360, 1),\n color: rgb(255, w(128, 255, 8), w(128, 255, 4)),\n // gradient: [ Color.RED, Color.BLUE ],\n outline,\n });\n\n drawPolygon({\n pos: p(0, 1),\n pts: [\n vec2(w(-10, 10, 2), -80),\n vec2(80, w(-10, 10, 4)),\n vec2(w(30, 50, 4), 80),\n vec2(-30, w(50, 70, 2)),\n vec2(w(-50, -70, 4), 0),\n ],\n colors: [\n rgb(w(128, 255, 8), 255, w(128, 255, 4)),\n rgb(255, w(128, 255, 8), w(128, 255, 4)),\n rgb(w(128, 255, 8), w(128, 255, 4), 255),\n rgb(255, 128, w(128, 255, 4)),\n rgb(w(128, 255, 8), w(128, 255, 4), 128),\n ],\n outline,\n });\n\n drawText({\n text: \"yo\",\n pos: p(1, 1),\n anchor: \"center\",\n size: w(80, 120, 2),\n color: rgb(w(128, 255, 4), w(128, 255, 8), w(128, 255, 2)),\n });\n\n drawLines({\n ...outline,\n pts: trail,\n });\n\n doodles.forEach((pts) => {\n drawLines({\n ...outline,\n pts: pts,\n });\n });\n}\n\n// onDraw() is similar to onUpdate(), it runs every frame, but after all update events.\n// All drawXXX() functions need to be called every frame if you want them to persist\nonDraw(() => {\n const maskFunc = Math.floor(time()) % 2 === 0 ? drawSubtracted : drawMasked;\n\n if (isKeyDown(\"space\")) {\n maskFunc(() => {\n drawUVQuad({\n width: width(),\n height: height(),\n shader: \"spiral\",\n uniform: {\n \"u_time\": time(),\n \"u_mpos\": mousePos().scale(1 / width(), 1 / height()),\n },\n });\n }, drawStuff);\n }\n else {\n drawStuff();\n }\n});\n\n// It's a common practice to put all input handling and state updates before rendering.\nonUpdate(() => {\n trail.push(mousePos());\n\n if (trail.length > 16) {\n trail.shift();\n }\n\n if (isMousePressed()) {\n doodles.push([]);\n }\n\n if (isMouseDown() && isMouseMoved()) {\n doodles[doodles.length - 1].push(mousePos());\n }\n});\n", - "index": "19" - }, - { - "name": "easing", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n pos(20, 20),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"steps\",\n]);\n\nonClick(\"steps\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => {\n square.color = value;\n },\n easingSteps(5, \"jump-none\"),\n );\n});\n\nadd([\n pos(80, 20),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"stepsmove\",\n]);\n\nonClick(\"stepsmove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingSteps(5, \"jump-none\"),\n );\n});\n\nadd([\n pos(20, 120),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"linear\",\n]);\n\nonClick(\"linear\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => square.color = value,\n easingLinear([vec2(0, 0), vec2(0.5, 0.25), vec2(1, 1)]),\n );\n});\n\nadd([\n pos(80, 120),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"linearmove\",\n]);\n\nonClick(\"linearmove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingLinear([vec2(0, 0), vec2(0.5, 0.25), vec2(1, 1)]),\n );\n});\n\nadd([\n pos(20, 220),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"bezier\",\n]);\n\nonClick(\"bezier\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => square.color = value,\n easingCubicBezier(vec2(.17, .67), vec2(.77, .71)),\n );\n});\n\nadd([\n pos(80, 220),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"beziermove\",\n]);\n\nonClick(\"beziermove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingCubicBezier(vec2(.17, .67), vec2(.77, .71)),\n );\n});\n", - "index": "20" - }, - { - "name": "eatlove", - "code": "// @ts-check\n\nkaplay();\n\nconst fruits = [\n \"apple\",\n \"pineapple\",\n \"grape\",\n \"watermelon\",\n];\n\nfor (const fruit of fruits) {\n loadSprite(fruit, `/sprites/${fruit}.png`);\n}\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"heart\", \"/sprites/heart.png\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\n\nscene(\"start\", () => {\n play(\"wooosh\");\n\n add([\n text(\"Eat All\"),\n pos(center().sub(0, 100)),\n scale(2),\n anchor(\"center\"),\n ]);\n\n add([\n sprite(\"heart\"),\n pos(center().add(0, 100)),\n scale(2),\n anchor(\"center\"),\n ]);\n\n wait(1.5, () => go(\"game\"));\n});\n\n// main game scene content\nscene(\"game\", () => {\n const SPEED_MIN = 120;\n const SPEED_MAX = 640;\n\n // add the player game object\n const player = add([\n sprite(\"bean\"),\n pos(40, 20),\n area({ scale: 0.5 }),\n anchor(\"center\"),\n ]);\n\n // make the layer move by mouse\n player.onUpdate(() => {\n player.pos = mousePos();\n });\n\n // game over if player eats a fruit\n player.onCollide(\"fruit\", () => {\n go(\"lose\", score);\n play(\"hit\");\n });\n\n // move the food every frame, destroy it if far outside of screen\n onUpdate(\"food\", (food) => {\n food.move(-food.speed, 0);\n if (food.pos.x < -120) {\n destroy(food);\n }\n });\n\n onUpdate(\"heart\", (heart) => {\n if (heart.pos.x <= 0) {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(heart.pos);\n }\n });\n\n // score counter\n let score = 0;\n\n const scoreLabel = add([\n text(score.toString(), {\n size: 32,\n }),\n pos(12, 12),\n ]);\n\n // increment score if player eats a heart\n player.onCollide(\"heart\", (heart) => {\n addKaboom(player.pos);\n score += 1;\n destroy(heart);\n scoreLabel.text = score.toString();\n burp();\n shake(12);\n });\n\n // do this every 0.3 seconds\n loop(0.3, () => {\n // spawn from right side of the screen\n const x = width() + 24;\n // spawn from a random y position\n const y = rand(0, height());\n // get a random speed\n const speed = rand(SPEED_MIN, SPEED_MAX);\n // 50% percent chance is heart\n const isHeart = chance(0.5);\n const spriteName = isHeart ? \"heart\" : choose(fruits);\n\n add([\n sprite(spriteName),\n pos(x, y),\n area({ scale: 0.5 }),\n anchor(\"center\"),\n \"food\",\n isHeart ? \"heart\" : \"fruit\",\n { speed: speed },\n ]);\n });\n});\n\n// game over scene\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"start\"));\n onClick(() => go(\"start\"));\n});\n\n// start with the \"game\" scene\ngo(\"start\");\n", - "index": "21" - }, - { - "name": "egg", - "code": "// @ts-check\n// Egg minigames (yes, like Peppa)\n\nkaplay({\n background: [135, 62, 132],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"egg\", \"/sprites/egg.png\");\nloadSprite(\"egg_crack\", \"/sprites/egg_crack.png\");\n\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n anchor(\"center\"),\n z(50),\n]);\n\nconst counter = add([\n text(\"0\"),\n pos(24, 24),\n z(100),\n { value: 0 },\n]);\n\n// \"shake\" is taken, so..\nfunction rock() {\n let strength = 0;\n let time = 0;\n return {\n id: \"rock\",\n require: [\"rotate\"],\n update() {\n if (strength === 0) {\n return;\n }\n this.angle = Math.sin(time * 10) * strength;\n time += dt();\n strength -= dt() * 30;\n if (strength <= 0) {\n strength = 0;\n time = 0;\n }\n },\n rock(n = 15) {\n strength = n;\n },\n };\n}\n\nonKeyPress(\"space\", () => {\n add([\n sprite(\"egg\"),\n pos(player.pos.add(0, 24)),\n rotate(0),\n anchor(\"bot\"),\n rock(),\n \"egg\",\n { stage: 0 },\n ]);\n\n player.moveTo(rand(0, width()), rand(0, height()));\n});\n\n// HATCH\nonKeyPress(\"enter\", () => {\n get(\"egg\", { recursive: true }).forEach((e) => {\n if (e.stage === 0) {\n e.stage = 1;\n e.rock();\n e.use(sprite(\"egg_crack\"));\n }\n else if (e.stage === 1) {\n e.stage = 2;\n e.use(sprite(\"bean\"));\n addKaboom(e.pos.sub(0, e.height / 2));\n counter.value += 1;\n counter.text = counter.value.toString();\n }\n });\n});\n", - "index": "22" - }, - { - "name": "fadeIn", - "code": "// @ts-check\n\nkaplay();\n\nloadBean();\n\n// spawn a bean that takes a second to fade in\nconst bean = add([\n sprite(\"bean\"),\n pos(120, 80),\n opacity(1), // opacity() component gives it opacity which is required for fadeIn\n]);\n\nbean.fadeIn(1); // makes it fade in\n\n// spawn another bean that takes 5 seconds to fade in halfway\n// SPOOKY!\nlet spookyBean = add([\n sprite(\"bean\"),\n pos(240, 80),\n opacity(0.5), // opacity() component gives it opacity which is required for fadeIn (set to 0.5 so it will be half transparent)\n]);\n\nspookyBean.fadeIn(5); // makes it fade in (set to 5 so that it takes 5 seconds to fade in)\n", - "index": "23" - }, - { - "name": "fakeMouse", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"cursor\", \"/sprites/cursor_default.png\");\n\n// set the layers\nlayers([\n \"game\",\n \"ui\",\n], \"game\");\n\nconst MOUSE_VEL = 200;\n\nconst cursor = add([\n sprite(\"cursor\"),\n fakeMouse(),\n pos(),\n layer(\"ui\"),\n]);\n\n// Mouse press and release\ncursor.onKeyPress(\"f\", () => {\n cursor.press();\n});\n\ncursor.onKeyRelease(\"f\", () => {\n cursor.release();\n});\n\n// Mouse movement\ncursor.onKeyDown(\"left\", () => {\n cursor.move(-MOUSE_VEL, 0);\n});\n\ncursor.onKeyDown(\"right\", () => {\n cursor.move(MOUSE_VEL, 0);\n});\n\ncursor.onKeyDown(\"up\", () => {\n cursor.move(0, -MOUSE_VEL);\n});\n\ncursor.onKeyDown(\"down\", () => {\n cursor.move(0, MOUSE_VEL);\n});\n\n// Example with hovering and click\nconst bean = add([\n sprite(\"bean\"),\n area(),\n color(BLUE),\n]);\n\nbean.onClick(() => {\n debug.log(\"ohhi\");\n});\n\nbean.onHover(() => {\n bean.color = RED;\n});\n\nbean.onHoverEnd(() => {\n bean.color = BLUE;\n});\n", - "index": "24" - }, - { - "name": "fall", - "code": "// @ts-check\n\n// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\n\nsetGravity(2400);\n\naddLevel([\n // Design the level layout with symbols\n \" \",\n \" \",\n \" \",\n \" \",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100),\n // Define what each symbol means (in components)\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n ],\n },\n});\n\nloop(0.2, () => {\n const coin = add([\n pos(rand(100, 400), 0),\n sprite(\"coin\"),\n area(),\n body(),\n \"coin\",\n ]);\n wait(3, () => coin.destroy());\n});\n\ndebug.paused = true;\n\nonKeyPressRepeat(\"space\", () => {\n debug.stepFrame();\n});\n", - "index": "25" - }, - { - "name": "fixedUpdate", - "code": "// @ts-check\n\nkaplay();\n\nlet fixedCount = 0;\nlet count = 0;\n\nonFixedUpdate(() => {\n fixedCount++;\n});\n\nonUpdate(() => {\n count++;\n debug.log(\n `${fixedDt()} ${Math.floor(fixedCount / time())} ${dt()} ${\n Math.floor(count / time())\n }`,\n );\n});\n", - "index": "26" - }, - { - "name": "flamebar", - "code": "// @ts-check\n\n// Mario-like flamebar\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"pineapple\", \"/sprites/pineapple.png\");\n\n// Define player movement speed\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 40),\n area(),\n]);\n\n// Player movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\n// Function to add a flamebar\nfunction addFlamebar(position = vec2(0), angle = 0, num = 6) {\n // Create a parent game object for position and rotation\n const flameHead = add([\n pos(position),\n rotate(angle),\n ]);\n\n // Add each section of flame as children\n for (let i = 0; i < num; i++) {\n flameHead.add([\n sprite(\"pineapple\"),\n pos(0, i * 48),\n area(),\n anchor(\"center\"),\n \"flame\",\n ]);\n }\n\n // The flame head's rotation will affect all its children\n flameHead.onUpdate(() => {\n flameHead.angle += dt() * 60;\n });\n\n return flameHead;\n}\n\naddFlamebar(vec2(200, 300), -60);\naddFlamebar(vec2(480, 100), 180);\naddFlamebar(vec2(400, 480), 0);\n\n// Game over if player touches a flame\nplayer.onCollide(\"flame\", () => {\n addKaboom(player.pos);\n player.destroy();\n});\n", - "index": "27" - }, - { - "name": "flappy", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\n\n// define gravity\nsetGravity(3200);\n\nscene(\"game\", () => {\n const PIPE_OPEN = 240;\n const PIPE_MIN = 60;\n const JUMP_FORCE = 800;\n const SPEED = 320;\n const CEILING = -60;\n\n // a game object consists of a list of components and tags\n const bean = add([\n // sprite() means it's drawn with a sprite of name \"bean\" (defined above in 'loadSprite')\n sprite(\"bean\"),\n // give it a position\n pos(width() / 4, 0),\n // give it a collider\n area(),\n // body component enables it to fall and jump in a gravity world\n body(),\n ]);\n\n // check for fall death\n bean.onUpdate(() => {\n if (bean.pos.y >= height() || bean.pos.y <= CEILING) {\n // switch to \"lose\" scene\n go(\"lose\", score);\n }\n });\n\n // jump\n onKeyPress(\"space\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n onGamepadButtonPress(\"south\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n // mobile\n onClick(() => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n function spawnPipe() {\n // calculate pipe positions\n const h1 = rand(PIPE_MIN, height() - PIPE_MIN - PIPE_OPEN);\n const h2 = height() - h1 - PIPE_OPEN;\n\n add([\n pos(width(), 0),\n rect(64, h1),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n ]);\n\n add([\n pos(width(), h1 + PIPE_OPEN),\n rect(64, h2),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n // raw obj just assigns every field to the game obj\n { passed: false },\n ]);\n }\n\n // callback when bean onCollide with objects with tag \"pipe\"\n bean.onCollide(\"pipe\", () => {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(bean.pos);\n });\n\n // per frame event for all objects with tag 'pipe'\n onUpdate(\"pipe\", (p) => {\n // check if bean passed the pipe\n if (p.pos.x + p.width <= bean.pos.x && p.passed === false) {\n addScore();\n p.passed = true;\n }\n });\n\n // spawn a pipe every 1 sec\n loop(1, () => {\n spawnPipe();\n });\n\n let score = 0;\n\n // display score\n const scoreLabel = add([\n text(score.toString()),\n anchor(\"center\"),\n pos(width() / 2, 80),\n fixed(),\n z(100),\n ]);\n\n function addScore() {\n score++;\n scoreLabel.text = score.toString();\n play(\"score\");\n }\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", - "index": "28" - }, - { - "name": "gamepad", - "code": "// @ts-check\n\nkaplay({\n background: [0, 0, 0],\n});\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nsetGravity(2400);\n\nscene(\"nogamepad\", () => {\n add([\n text(\"Gamepad not found.\\nConnect a gamepad and press a button!\", {\n width: width() - 80,\n align: \"center\",\n }),\n pos(center()),\n anchor(\"center\"),\n ]);\n onGamepadConnect(() => {\n go(\"game\");\n });\n});\n\nscene(\"game\", () => {\n const player = add([\n pos(center()),\n anchor(\"center\"),\n sprite(\"bean\"),\n area(),\n body(),\n ]);\n\n // platform\n add([\n pos(0, height()),\n anchor(\"botleft\"),\n rect(width(), 140),\n area(),\n body({ isStatic: true }),\n ]);\n\n onGamepadButtonPress((b) => {\n debug.log(b);\n });\n\n onGamepadButtonPress([\"south\", \"west\"], () => {\n player.jump();\n });\n\n onGamepadStick(\"left\", (v) => {\n player.move(v.x * 400, 0);\n });\n\n onGamepadDisconnect(() => {\n go(\"nogamepad\");\n });\n});\n\nif (getGamepads().length > 0) {\n go(\"game\");\n}\nelse {\n go(\"nogamepad\");\n}\n", - "index": "29" - }, - { - "name": "ghosthunting", - "code": "// @ts-check\n\nkaplay({\n width: 1024,\n height: 768,\n letterbox: true,\n});\n\nloadSprite(\"bean\", \"./sprites/bean.png\");\nloadSprite(\"gun\", \"./sprites/gun.png\");\nloadSprite(\"ghosty\", \"./sprites/ghosty.png\");\nloadSprite(\"hexagon\", \"./examples/sprites/particle_hexagon_filled.png\");\nloadSprite(\"star\", \"./examples/sprites/particle_star_filled.png\");\n\nconst nav = new NavMesh();\n// Hallway\nnav.addPolygon([vec2(20, 20), vec2(1004, 20), vec2(620, 120), vec2(20, 120)]);\n// Living room\nnav.addPolygon([\n vec2(620, 120),\n vec2(1004, 20),\n vec2(1004, 440),\n vec2(620, 140),\n]);\nnav.addPolygon([vec2(20, 140), vec2(620, 140), vec2(1004, 440), vec2(20, 440)]);\n// Kitchen\nnav.addPolygon([vec2(20, 460), vec2(320, 460), vec2(320, 748), vec2(20, 748)]);\nnav.addPolygon([\n vec2(320, 440),\n vec2(420, 440),\n vec2(420, 748),\n vec2(320, 748),\n]);\nnav.addPolygon([\n vec2(420, 460),\n vec2(620, 460),\n vec2(620, 748),\n vec2(420, 748),\n]);\n// Storage room\nnav.addPolygon([\n vec2(640, 460),\n vec2(720, 460),\n vec2(720, 748),\n vec2(640, 748),\n]);\nnav.addPolygon([\n vec2(720, 440),\n vec2(820, 440),\n vec2(820, 748),\n vec2(720, 748),\n]);\nnav.addPolygon([\n vec2(820, 460),\n vec2(1004, 460),\n vec2(1004, 748),\n vec2(820, 748),\n]);\n\n// Border\nadd([\n pos(0, 0),\n rect(20, height()),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(0, 0),\n rect(width(), 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(width() - 20, 0),\n rect(20, height()),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(0, height() - 20),\n rect(width(), 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Hallway\nadd([\n pos(20, 20),\n rect(600, 100),\n color(rgb(128, 64, 64)),\n \"floor\",\n]);\nadd([\n pos(20, 120),\n rect(600, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Living room\nadd([\n pos(20, 140),\n rect(600, 300),\n color(rgb(64, 64, 128)),\n \"floor\",\n]);\nadd([\n pos(620, 20),\n rect(384, 420),\n color(rgb(64, 64, 128)),\n \"floor\",\n]);\nadd([\n pos(20, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(420, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(820, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Kitchen\nadd([\n pos(320, 440),\n rect(100, 20),\n color(rgb(128, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(20, 460),\n rect(600, 288),\n color(rgb(128, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(620, 460),\n rect(20, 288),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Storage\nadd([\n pos(720, 440),\n rect(100, 20),\n color(rgb(64, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(640, 460),\n rect(364, 288),\n color(rgb(64, 128, 64)),\n \"floor\",\n]);\n\nconst player = add([\n pos(50, 50),\n sprite(\"bean\"),\n anchor(vec2(0, 0)),\n area(),\n body(),\n \"player\",\n]);\n\nconst gun = player.add([\n sprite(\"gun\"),\n anchor(vec2(-2, 0)),\n rotate(0),\n \"player\",\n]);\n\nfunction addEnemy(p) {\n const enemy = add([\n {\n add() {\n this.onHurt(() => {\n this.opacity = this.hp() / 100;\n });\n this.onDeath(() => {\n const rect = this.localArea();\n rect.pos = rect.pos.sub(rect.width / 2, rect.height / 2);\n const dissipate = add([\n pos(this.pos),\n particles({\n max: 20,\n speed: [50, 100],\n angle: [0, 360],\n angularVelocity: [45, 90],\n lifeTime: [1.0, 1.5],\n colors: [rgb(128, 128, 255), WHITE],\n opacities: [0.1, 1.0, 0.0],\n texture: getSprite(\"star\").data.tex,\n quads: [getSprite(\"star\").data.frames[0]],\n }, {\n lifetime: 1.5,\n shape: rect,\n rate: 0,\n direction: -90,\n spread: 0,\n }),\n ]);\n dissipate.emit(20);\n dissipate.onEnd(() => {\n destroy(dissipate);\n });\n destroy(this);\n });\n this.onObjectsSpotted(objects => {\n const playerSeen = objects.some(o => o.is(\"player\"));\n if (playerSeen) {\n enemy.action = \"pursuit\";\n enemy.waypoints = null;\n }\n });\n this.onPatrolFinished(() => {\n enemy.action = \"observe\";\n });\n },\n },\n pos(p),\n sprite(\"ghosty\"),\n opacity(1),\n anchor(vec2(0, 0)),\n area(),\n body(),\n // Health provides properties and methods to keep track of the enemies health\n health(100),\n // Sentry makes it easy to check for visibility of the player\n sentry({ include: \"player\" }, {\n lineOfSight: true,\n raycastExclude: [\"enemy\"],\n }),\n // Patrol can make the enemy follow a computed path\n patrol({ speed: 100 }),\n // Navigator can compute a path given a graph\n navigation({\n graph: nav,\n navigationOpt: {\n type: \"edges\",\n },\n }),\n \"enemy\",\n { action: \"observing\", waypoint: null },\n ]);\n return enemy;\n}\n\naddEnemy(vec2(width() * 3 / 4, height() / 2));\naddEnemy(vec2(width() * 1 / 4, height() / 2));\naddEnemy(vec2(width() * 1 / 4, height() * 2 / 3));\naddEnemy(vec2(width() * 0.8, height() * 2 / 3));\n\nlet path;\nonUpdate(\"enemy\", enemy => {\n switch (enemy.action) {\n case \"observe\": {\n break;\n }\n case \"pursuit\": {\n if (enemy.hasLineOfSight(player)) {\n // We can see the player, just go straight to their location\n enemy.moveTo(player.pos, 100);\n }\n else {\n // We can't see the player, but we know where they are, plot a path\n path = enemy.navigateTo(player.pos);\n // enemy.waypoint = path[1];\n enemy.waypoints = path;\n enemy.action = \"observe\";\n }\n break;\n }\n }\n});\n\nconst SPEED = 200;\n\nconst dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n \"a\": LEFT,\n \"d\": RIGHT,\n \"w\": UP,\n \"s\": DOWN,\n};\n\nfor (const dir in dirs) {\n onKeyDown(dir, () => {\n player.move(dirs[dir].scale(SPEED));\n });\n}\n\nonMouseMove(() => {\n gun.angle = mousePos().sub(player.pos).angle();\n gun.flipY = Math.abs(gun.angle) > 90;\n});\n\nonMousePress(() => {\n const flash = gun.add([\n pos(\n getSprite(\"gun\").data.width * 1.5,\n Math.abs(gun.angle) > 90 ? 7 : -7,\n ),\n circle(10),\n color(YELLOW),\n opacity(0.5),\n ]);\n flash.fadeOut(0.5).then(() => {\n destroy(flash);\n });\n\n const dir = mousePos().sub(player.pos).unit().scale(1024);\n const hit = raycast(player.pos, dir, [\n \"player\",\n ]);\n if (hit) {\n const splatter = add([\n pos(hit.point),\n particles({\n max: 20,\n speed: [200, 250],\n lifeTime: [0.2, 0.75],\n colors: [WHITE],\n opacities: [1.0, 0.0],\n angle: [0, 360],\n texture: getSprite(\"hexagon\").data.tex,\n quads: [getSprite(\"hexagon\").data.frames[0]],\n }, {\n lifetime: 0.75,\n rate: 0,\n direction: dir.scale(-1).angle(),\n spread: 45,\n }),\n ]);\n splatter.emit(10);\n splatter.onEnd(() => {\n destroy(splatter);\n });\n if (hit.object && hit.object.is(\"enemy\")) {\n hit.object.moveBy(dir.unit().scale(10));\n hit.object.hurt(20);\n }\n }\n});\n", - "index": "30" - }, - { - "name": "gravity", - "code": "// @ts-check\n\n// Responding to gravity & jumping\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Set the gravity acceleration (pixels per second)\nsetGravity(1600);\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n area(),\n // body() component gives the ability to respond to gravity\n body(),\n]);\n\nonKeyPress(\"space\", () => {\n // .isGrounded() is provided by body()\n if (player.isGrounded()) {\n // .jump() is provided by body()\n player.jump();\n }\n});\n\n// .onGround() is provided by body(). It registers an event that runs whenever player hits the ground.\nplayer.onGround(() => {\n debug.log(\"ouch\");\n});\n\n// Accelerate falling when player holding down arrow key\nonKeyDown(\"down\", () => {\n if (!player.isGrounded()) {\n player.vel.y += dt() * 1200;\n }\n});\n\n// Jump higher if space is held\nonKeyDown(\"space\", () => {\n if (!player.isGrounded() && player.vel.y < 0) {\n player.vel.y -= dt() * 600;\n }\n});\n\n// Add a platform to hold the player\nadd([\n rect(width(), 48),\n outline(4),\n area(),\n pos(0, height() - 48),\n // Give objects a body() component if you don't want other solid objects pass through\n body({ isStatic: true }),\n]);\n\nadd([\n text(\"Press space key\", { width: width() / 2 }),\n pos(12, 12),\n]);\n\n// Check out https://kaplayjs.com/doc/BodyComp for everything body() provides\n", - "index": "31" - }, - { - "name": "hover", - "code": "// @ts-check\n\n// Differeces between onHover and onHoverUpdate\n\nkaplay({\n // Use logMax to see more messages on debug.log()\n logMax: 5,\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nadd([\n text(\"onHover()\\nonHoverEnd()\"),\n pos(80, 80),\n]);\n\nadd([\n text(\"onHoverUpdate()\"),\n pos(340, 80),\n]);\n\nconst redBean = add([\n sprite(\"bean\"),\n color(RED),\n pos(130, 180),\n anchor(\"center\"),\n area(),\n]);\n\nconst blueBean = add([\n sprite(\"bean\"),\n color(BLUE),\n pos(380, 180),\n anchor(\"center\"),\n area(),\n]);\n\n// Only runs once when bean is hovered, and when bean is unhovered\nredBean.onHover(() => {\n debug.log(\"red bean hovered\");\n\n redBean.color = GREEN;\n});\nredBean.onHoverEnd(() => {\n debug.log(\"red bean unhovered\");\n\n redBean.color = RED;\n});\n\n// Runs every frame when blue bean is hovered\nblueBean.onHoverUpdate(() => {\n const t = time() * 10;\n blueBean.color = rgb(\n wave(0, 255, t),\n wave(0, 255, t + 2),\n wave(0, 255, t + 4),\n );\n\n debug.log(\"blue bean on hover\");\n});\n", - "index": "32" - }, - { - "name": "inspectExample", - "code": "// @ts-check\n\nkaplay();\n\n// # will delete this file when changes get merged/declined i don't intend this to be an actual example\nfunction customComponent() {\n return {\n id: \"compy\",\n customing: true,\n // if it didn't have an inspect function it would appear as \"compy\"\n inspect() {\n return `customing: ${this.customing}`;\n },\n };\n}\n\nloadBean();\n\nlet bean = add([\n sprite(\"bean\"),\n area(),\n opacity(),\n pos(center()),\n scale(4),\n customComponent(),\n]);\n\nbean.onClick(() => {\n bean.customing = !bean.customing;\n});\n\n// # check sprite.ts and the other components in the object\n// now the inspect function says eg: `sprite: ${src}` instead of `${src}`\n", - "index": "33" - }, - { - "name": "kaboom", - "code": "// @ts-check\n\n// You can still use kaboom() instead of kaplay()!\nkaboom();\n\naddKaboom(center());\n\nonKeyPress(() => addKaboom(mousePos()));\nonMouseMove(() => addKaboom(mousePos()));\n", - "index": "34" - }, - { - "name": "largeTexture", - "code": "// @ts-check\n\nkaplay();\n\nlet cameraPosition = camPos();\nlet cameraScale = 1;\n\n// Loads a random 2500px image\nloadSprite(\"bigyoshi\", \"/examples/sprites/YOSHI.png\");\n\nadd([\n sprite(\"bigyoshi\"),\n]);\n\n// Adds a label\nconst label = make([\n text(\"Click and drag the mouse, scroll the wheel\"),\n]);\n\nadd([\n rect(label.width, label.height),\n color(0, 0, 0),\n]);\n\nadd(label);\n\n// Mouse handling\nonUpdate(() => {\n if (isMouseDown(\"left\") && isMouseMoved()) {\n cameraPosition = cameraPosition.sub(\n mouseDeltaPos().scale(1 / cameraScale),\n );\n camPos(cameraPosition);\n }\n});\n\nonScroll((delta) => {\n cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y));\n camScale(cameraScale);\n});\n", - "index": "35" - }, - { - "name": "layer", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Create a parent node that won't be affected by camera (fixed) and will be drawn on top (z of 100)\nconst ui = add([\n fixed(),\n z(100),\n]);\n\n// This will be on top, because the parent node has z(100)\nui.add([\n sprite(\"bean\"),\n scale(5),\n color(0, 0, 255),\n]);\n\nadd([\n sprite(\"bean\"),\n pos(100, 100),\n scale(5),\n]);\n", - "index": "36" - }, - { - "name": "layers", - "code": "// @ts-check\n\nkaplay();\n\nlayers([\"bg\", \"game\", \"ui\"], \"game\");\n\n// bg layer\nadd([\n rect(width(), height()),\n layer(\"bg\"),\n color(rgb(64, 128, 255)),\n // opacity(0.5)\n]).add([text(\"BG\")]);\n\n// game layer explicit\nadd([\n pos(width() / 5, height() / 5),\n rect(width() / 3, height() / 3),\n layer(\"game\"),\n color(rgb(255, 128, 64)),\n]).add([text(\"GAME\")]);\n\n// game layer implicit\nadd([\n pos(3 * width() / 5, 3 * height() / 5),\n rect(width() / 3, height() / 3),\n color(rgb(255, 128, 64)),\n]).add([pos(width() / 3, height() / 3), text(\"GAME\"), anchor(\"botright\")]);\n\n// ui layer\nadd([\n pos(center()),\n rect(width() / 2, height() / 2),\n anchor(\"center\"),\n color(rgb(64, 255, 128)),\n]).add([text(\"UI\"), anchor(\"center\")]);\n", - "index": "37" - }, - { - "name": "level", - "code": "// @ts-check\n\n// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst SPEED = 480;\n\nsetGravity(2400);\n\nconst level = addLevel([\n // Design the level layout with symbols\n \"@ ^ $$\",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100, 200),\n // Define what each symbol means (in components)\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\n// Back to the original position if hit a \"danger\" item\nplayer.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n});\n\n// Eat the coin!\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n});\n", - "index": "38" - }, - { - "name": "levelraycast", - "code": "// @ts-check\n\nkaplay({\n background: [31, 16, 42],\n});\n\nloadSprite(\"grass\", \"/sprites/grass.png\");\n\nconst level = addLevel([\n \"===\",\n \"= =\",\n \"===\",\n], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(256, 128),\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n ],\n },\n});\nlevel.use(rotate(45));\n\nonLoad(() => {\n level.spawn([\n pos(\n level.tileWidth() * 1.5,\n level.tileHeight() * 1.5,\n ),\n circle(6),\n color(\"#ea6262\"),\n {\n add() {\n const rayHit = level.raycast(\n this.pos,\n Vec2.fromAngle(0).scale(100),\n );\n\n debug.log(\n `${rayHit != null} ${\n rayHit && rayHit.object ? rayHit.object.id : -1\n }`,\n );\n },\n },\n ]);\n});\n\ndebug.inspect = true;\n", - "index": "39" - }, - { - "name": "linecap", - "code": "// @ts-check\n\nkaplay();\n\nonDraw(() => {\n // No line cap\n drawLines({\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 200),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 200),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 200),\n radius: 4,\n color: RED,\n });\n\n // Square line cap\n drawLines({\n pos: vec2(0, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 450),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 450),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 450),\n radius: 4,\n color: RED,\n });\n\n // Round line cap\n drawLines({\n pos: vec2(0, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 700),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 700),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 700),\n radius: 4,\n color: RED,\n });\n});\n", - "index": "40" - }, - { - "name": "linejoin", - "code": "// @ts-check\n\nkaplay();\n\nonDraw(() => {\n // Rectangles\n drawLines({\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"bevel\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(200, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"round\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(400, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"miter\",\n width: 20,\n });\n\n // Parallelograms\n drawLines({\n pos: vec2(0, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"bevel\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(200, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"round\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(400, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"miter\",\n width: 20,\n });\n});\n\nadd([\n pos(0, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 1, \"bevel\"),\n]);\n\nadd([\n pos(200, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 1, \"round\"),\n]);\n\nadd([\n pos(400, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 0.5, \"miter\"),\n]);\n", - "index": "41" - }, - { - "name": "loader", - "code": "// @ts-check\n\n// Customizing the asset loader\n\nkaplay({\n // Optionally turn off loading screen entirely\n // Unloaded assets simply won't be drawn\n // loadingScreen: false,\n});\n\nlet spr = null;\n\n// Every loadXXX() function returns a Asset where you can customize the error handling (by default it'll stop the game and log on screen), or deal with the raw asset data yourself instead of using a name.\nloadSprite(\"bean\", \"/sprites/bean.png\").onError(() => {\n alert(\"oh no we failed to load bean\");\n}).onLoad((data) => {\n // The promise resolves to the raw sprite data\n spr = data;\n});\n\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// load() adds a Promise under KAPLAY's management, which affects loadProgress()\n// Here we intentionally stall the loading by 1sec to see the loading screen\nload(\n new Promise((res) => {\n // wait() won't work here because timers are not run during loading so we use setTimeout\n setTimeout(() => {\n res();\n }, 1000);\n }),\n);\n\n// make loader wait for a fetch() call\nload(fetch(\"https://kaboomjs.com/\"));\n\n// You can also use the handle returned by loadXXX() as the resource handle\nconst bugSound = loadSound(\"bug\", \"/examples/sounds/bug.mp3\");\n\nvolume(0.1);\n\nonKeyPress(\"space\", () => play(bugSound));\n\n// Custom loading screen\n// Runs the callback every frame during loading\nonLoading((progress) => {\n // Black background\n drawRect({\n width: width(),\n height: height(),\n color: rgb(0, 0, 0),\n });\n\n // A pie representing current load progress\n drawCircle({\n pos: center(),\n radius: 32,\n end: map(progress, 0, 1, 0, 360),\n });\n\n drawText({\n text: \"loading\" + \".\".repeat(wave(1, 4, time() * 12)),\n font: \"monospace\",\n size: 24,\n anchor: \"center\",\n pos: center().add(0, 70),\n });\n});\n\nonDraw(() => {\n if (spr) {\n drawSprite({\n // You can pass raw sprite data here instead of the name\n sprite: spr,\n });\n }\n});\n", - "index": "42" - }, - { - "name": "maze", - "code": "// @ts-check\n\nkaplay({\n scale: 0.5,\n background: [0, 0, 0],\n});\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"steel\", \"sprites/steel.png\");\n\nconst TILE_WIDTH = 64;\nconst TILE_HEIGHT = TILE_WIDTH;\n\nfunction createMazeMap(width, height) {\n const size = width * height;\n function getUnvisitedNeighbours(map, index) {\n const n = [];\n const x = Math.floor(index / width);\n if (x > 1 && map[index - 2] === 2) n.push(index - 2);\n if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);\n if (index >= 2 * width && map[index - 2 * width] === 2) {\n n.push(index - 2 * width);\n }\n if (index < size - 2 * width && map[index + 2 * width] === 2) {\n n.push(index + 2 * width);\n }\n return n;\n }\n const map = new Array(size).fill(1, 0, size);\n map.forEach((_, index) => {\n const x = Math.floor(index / width);\n const y = Math.floor(index % width);\n if ((x & 1) === 1 && (y & 1) === 1) {\n map[index] = 2;\n }\n });\n\n const stack = [];\n const startX = Math.floor(Math.random() * (width - 1)) | 1;\n const startY = Math.floor(Math.random() * (height - 1)) | 1;\n const start = startX + startY * width;\n map[start] = 0;\n stack.push(start);\n while (stack.length) {\n const index = stack.pop();\n const neighbours = getUnvisitedNeighbours(map, index);\n if (neighbours.length > 0) {\n stack.push(index);\n const neighbour =\n neighbours[Math.floor(neighbours.length * Math.random())];\n const between = (index + neighbour) / 2;\n map[neighbour] = 0;\n map[between] = 0;\n stack.push(neighbour);\n }\n }\n return map;\n}\n\nfunction createMazeLevelMap(width, height, options) {\n const symbols = options?.symbols || {};\n const map = createMazeMap(width, height);\n const space = symbols[\" \"] || \" \";\n const fence = symbols[\"#\"] || \"#\";\n const detail = [\n space,\n symbols[\"╸\"] || \"╸\", // 1\n symbols[\"╹\"] || \"╹\", // 2\n symbols[\"┛\"] || \"┛\", // 3\n symbols[\"╺\"] || \"╺\", // 4\n symbols[\"━\"] || \"━\", // 5\n symbols[\"┗\"] || \"┗\", // 6\n symbols[\"┻\"] || \"┻\", // 7\n symbols[\"╻\"] || \"╻\", // 8\n symbols[\"┓\"] || \"┓\", // 9\n symbols[\"┃\"] || \"┃\", // a\n symbols[\"┫\"] || \"┫\", // b\n symbols[\"┏\"] || \"┏\", // c\n symbols[\"┳\"] || \"┳\", // d\n symbols[\"┣\"] || \"┣\", // e\n symbols[\"╋ \"] || \"╋ \", // f\n ];\n const symbolMap = options?.detailed\n ? map.map((s, index) => {\n if (s === 0) return space;\n const x = Math.floor(index % width);\n const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;\n const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;\n const topWall = index >= width && map[index - width] == 1 ? 2 : 0;\n const bottomWall =\n index < height * width - width && map[index + width] == 1\n ? 8\n : 0;\n return detail[leftWall | rightWall | topWall | bottomWall];\n })\n : map.map((s) => {\n return s == 1 ? fence : space;\n });\n const levelMap = [];\n for (let i = 0; i < height; i++) {\n levelMap.push(symbolMap.slice(i * width, i * width + width).join(\"\"));\n }\n return levelMap;\n}\n\nconst level = addLevel(\n createMazeLevelMap(15, 15, {}),\n {\n tileWidth: TILE_WIDTH,\n tileHeight: TILE_HEIGHT,\n tiles: {\n \"#\": () => [\n sprite(\"steel\"),\n tile({ isObstacle: true }),\n ],\n },\n },\n);\n\nconst bean = level.spawn(\n [\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(32, 32),\n tile(),\n agent({ speed: 640, allowDiagonals: true }),\n \"bean\",\n ],\n 1,\n 1,\n);\n\nonClick(() => {\n const pos = mousePos();\n bean.setTarget(vec2(\n Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,\n Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,\n ));\n});\n", - "index": "43" - }, - { - "name": "mazeRaycastedLight", - "code": "// @ts-check\n\nkaplay({\n scale: 0.5,\n background: [0, 0, 0],\n});\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"steel\", \"sprites/steel.png\");\n\nconst TILE_WIDTH = 64;\nconst TILE_HEIGHT = TILE_WIDTH;\n\nfunction createMazeMap(width, height) {\n const size = width * height;\n function getUnvisitedNeighbours(map, index) {\n const n = [];\n const x = Math.floor(index / width);\n if (x > 1 && map[index - 2] === 2) n.push(index - 2);\n if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);\n if (index >= 2 * width && map[index - 2 * width] === 2) {\n n.push(index - 2 * width);\n }\n if (index < size - 2 * width && map[index + 2 * width] === 2) {\n n.push(index + 2 * width);\n }\n return n;\n }\n const map = new Array(size).fill(1, 0, size);\n map.forEach((_, index) => {\n const x = Math.floor(index / width);\n const y = Math.floor(index % width);\n if ((x & 1) === 1 && (y & 1) === 1) {\n map[index] = 2;\n }\n });\n\n const stack = [];\n const startX = Math.floor(Math.random() * (width - 1)) | 1;\n const startY = Math.floor(Math.random() * (height - 1)) | 1;\n const start = startX + startY * width;\n map[start] = 0;\n stack.push(start);\n while (stack.length) {\n const index = stack.pop();\n const neighbours = getUnvisitedNeighbours(map, index);\n if (neighbours.length > 0) {\n stack.push(index);\n const neighbour =\n neighbours[Math.floor(neighbours.length * Math.random())];\n const between = (index + neighbour) / 2;\n map[neighbour] = 0;\n map[between] = 0;\n stack.push(neighbour);\n }\n }\n return map;\n}\n\nfunction createMazeLevelMap(width, height, options) {\n const symbols = options?.symbols || {};\n const map = createMazeMap(width, height);\n const space = symbols[\" \"] || \" \";\n const fence = symbols[\"#\"] || \"#\";\n const detail = [\n space,\n symbols[\"╸\"] || \"╸\", // 1\n symbols[\"╹\"] || \"╹\", // 2\n symbols[\"┛\"] || \"┛\", // 3\n symbols[\"╺\"] || \"╺\", // 4\n symbols[\"━\"] || \"━\", // 5\n symbols[\"┗\"] || \"┗\", // 6\n symbols[\"┻\"] || \"┻\", // 7\n symbols[\"╻\"] || \"╻\", // 8\n symbols[\"┓\"] || \"┓\", // 9\n symbols[\"┃\"] || \"┃\", // a\n symbols[\"┫\"] || \"┫\", // b\n symbols[\"┏\"] || \"┏\", // c\n symbols[\"┳\"] || \"┳\", // d\n symbols[\"┣\"] || \"┣\", // e\n symbols[\"╋ \"] || \"╋ \", // f\n ];\n const symbolMap = options?.detailed\n ? map.map((s, index) => {\n if (s === 0) return space;\n const x = Math.floor(index % width);\n const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;\n const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;\n const topWall = index >= width && map[index - width] == 1 ? 2 : 0;\n const bottomWall =\n index < height * width - width && map[index + width] == 1\n ? 8\n : 0;\n return detail[leftWall | rightWall | topWall | bottomWall];\n })\n : map.map((s) => {\n return s == 1 ? fence : space;\n });\n const levelMap = [];\n for (let i = 0; i < height; i++) {\n levelMap.push(symbolMap.slice(i * width, i * width + width).join(\"\"));\n }\n return levelMap;\n}\n\nconst level = addLevel(\n createMazeLevelMap(15, 15, {}),\n {\n pos: vec2(100, 100),\n tileWidth: TILE_WIDTH,\n tileHeight: TILE_HEIGHT,\n tiles: {\n \"#\": () => [\n sprite(\"steel\"),\n tile({ isObstacle: true }),\n ],\n },\n },\n);\n\nconst bean = level.spawn(\n [\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(32, 32),\n tile(),\n agent({ speed: 640, allowDiagonals: true }),\n \"bean\",\n ],\n 1,\n 1,\n);\n\nonClick(() => {\n const pos = level.fromScreen(mousePos());\n bean.setTarget(vec2(\n Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,\n Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,\n ));\n});\n\nonUpdate(() => {\n const pts = [bean.pos];\n // This is overkill, since you theoretically only need to shoot rays to grid positions\n for (let i = 0; i < 360; i += 1) {\n const hit = level.raycast(bean.pos, Vec2.fromAngle(i));\n pts.push(hit.point);\n }\n pts.push(pts[1]);\n drawPolygon({\n pos: vec2(100, 100),\n pts: pts,\n color: rgb(255, 255, 100),\n });\n});\n", - "index": "44" - }, - { - "name": "movement", - "code": "// @ts-check\n\n// Input handling and basic player movement\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Define player movement speed (pixels per second)\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n // center() returns the center point vec2(width() / 2, height() / 2)\n pos(center()),\n]);\n\n// onKeyDown() registers an event that runs every frame as long as user is holding a certain key\nonKeyDown(\"left\", () => {\n // .move() is provided by pos() component, move by pixels per second\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\n// onClick() registers an event that runs once when left mouse is clicked\nonClick(() => {\n // .moveTo() is provided by pos() component, changes the position\n player.moveTo(mousePos());\n});\n\nadd([\n // text() component is similar to sprite() but renders text\n text(\"Press arrow keys\", { width: width() / 2 }),\n pos(12, 12),\n]);\n", - "index": "45" - }, - { - "name": "multigamepad", - "code": "// @ts-check\n\nkaplay();\nsetGravity(2400);\nsetBackground(0, 0, 0);\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nconst playerColors = [\n rgb(252, 53, 43),\n rgb(0, 255, 0),\n rgb(43, 71, 252),\n rgb(255, 255, 0),\n rgb(255, 0, 255),\n];\n\nlet playerCount = 0;\n\nfunction addPlayer(gamepad) {\n const player = add([\n pos(center()),\n anchor(\"center\"),\n sprite(\"bean\"),\n color(playerColors[playerCount]),\n area(),\n body(),\n doubleJump(),\n ]);\n\n playerCount++;\n\n onUpdate(() => {\n const leftStick = gamepad.getStick(\"left\");\n\n if (gamepad.isPressed(\"south\")) {\n player.doubleJump();\n }\n\n if (leftStick.x !== 0) {\n player.move(leftStick.x * 400, 0);\n }\n });\n}\n\n// platform\nadd([\n pos(0, height()),\n anchor(\"botleft\"),\n rect(width(), 140),\n area(),\n body({ isStatic: true }),\n]);\n\n// add players on every gamepad connect\nonGamepadConnect((gamepad) => {\n addPlayer(gamepad);\n});\n", - "index": "46" - }, - { - "name": "out", - "code": "// @ts-check\n\n// detect if obj is out of screen\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// custom comp\nfunction handleout() {\n return {\n id: \"handleout\",\n require: [\"pos\"],\n update() {\n const spos = this.screenPos();\n if (\n spos.x < 0\n || spos.x > width()\n || spos.y < 0\n || spos.y > height()\n ) {\n // triggers a custom event when out\n this.trigger(\"out\");\n }\n },\n };\n}\n\nconst SPEED = 640;\n\nfunction shoot() {\n const center = vec2(width() / 2, height() / 2);\n const mpos = mousePos();\n add([\n pos(center),\n sprite(\"bean\"),\n anchor(\"center\"),\n handleout(),\n \"bean\",\n { dir: mpos.sub(center).unit() },\n ]);\n}\n\nonKeyPress(\"space\", shoot);\nonClick(shoot);\n\nonUpdate(\"bean\", (m) => {\n m.move(m.dir.scale(SPEED));\n});\n\n// binds a custom event \"out\" to tag group \"bean\"\non(\"out\", \"bean\", (m) => {\n addKaboom(m.pos);\n destroy(m);\n});\n", - "index": "47" - }, - { - "name": "overlap", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawRect({\n pos: vec2(-1, -1),\n width: 3,\n height: 3,\n color: this.color,\n });\n },\n getShape() {\n // This would be point if we had a real class for it\n return new Rect(vec2(-1, -1).add(this.pos), 3, 3);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nlet selection;\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.getShape().contains(pos));\n selection = pickList[pickList.length - 1];\n});\n\nonMouseMove((pos, delta) => {\n if (selection) {\n selection.moveBy(delta);\n }\n});\n\nonMouseRelease(() => {\n selection = null;\n});\n\nonDraw(() => {\n if (selection) {\n const rect = selection.getShape().bbox();\n drawRect({\n pos: rect.pos,\n width: rect.width,\n height: rect.height,\n outline: {\n width: 1,\n color: YELLOW,\n },\n fill: false,\n });\n }\n});\n", - "index": "48" - }, - { - "name": "particle", - "code": "// @ts-check\n\n// Particle spawning\n\nkaplay();\n\nconst sprites = [\n \"apple\",\n \"heart\",\n \"coin\",\n \"meat\",\n \"lightening\",\n];\n\nsprites.forEach((spr) => {\n loadSprite(spr, `/sprites/${spr}.png`);\n});\n\nsetGravity(800);\n\n// Spawn one particle every 0.1 second\nloop(0.1, () => {\n // TODO: they are resolving collision with each other for some reason\n // Compose particle properties with components\n const item = add([\n pos(mousePos()),\n sprite(choose(sprites)),\n anchor(\"center\"),\n scale(rand(0.5, 1)),\n area({ collisionIgnore: [\"particle\"] }),\n body(),\n lifespan(1, { fade: 0.5 }),\n opacity(1),\n move(choose([LEFT, RIGHT]), rand(60, 240)),\n \"particle\",\n ]);\n\n item.onCollide(\"particle\", (p) => {\n console.log(\"dea\");\n });\n\n item.jump(rand(320, 640));\n});\n", - "index": "49" - }, - { - "name": "pauseMenu", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\n\n// define gravity\nsetGravity(3200);\n\nsetBackground(141, 183, 255);\n\nscene(\"game\", () => {\n const game = add([\n timer(),\n ]);\n\n const PIPE_OPEN = 240;\n const PIPE_MIN = 60;\n const JUMP_FORCE = 800;\n const SPEED = 320;\n const CEILING = -60;\n\n // a game object consists of a list of components and tags\n const bean = game.add([\n // sprite() means it's drawn with a sprite of name \"bean\" (defined above in 'loadSprite')\n sprite(\"bean\"),\n // give it a position\n pos(width() / 4, 0),\n // give it a collider\n area(),\n // body component enables it to fall and jump in a gravity world\n body(),\n ]);\n\n // check for fall death\n bean.onUpdate(() => {\n if (bean.pos.y >= height() || bean.pos.y <= CEILING) {\n // switch to \"lose\" scene\n go(\"lose\", score);\n }\n });\n\n // jump\n onKeyPress(\"space\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n onGamepadButtonPress(\"south\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n // mobile\n onClick(() => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n function spawnPipe() {\n // calculate pipe positions\n const h1 = rand(PIPE_MIN, height() - PIPE_MIN - PIPE_OPEN);\n const h2 = height() - h1 - PIPE_OPEN;\n\n game.add([\n pos(width(), 0),\n rect(64, h1),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n ]);\n\n game.add([\n pos(width(), h1 + PIPE_OPEN),\n rect(64, h2),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n // raw obj just assigns every field to the game obj\n { passed: false },\n ]);\n }\n\n // callback when bean onCollide with objects with tag \"pipe\"\n bean.onCollide(\"pipe\", () => {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(bean.pos);\n });\n\n // per frame event for all objects with tag 'pipe'\n onUpdate(\"pipe\", (p) => {\n // check if bean passed the pipe\n if (p.pos.x + p.width <= bean.pos.x && p.passed === false) {\n addScore();\n p.passed = true;\n }\n });\n\n // spawn a pipe every 1 sec\n game.loop(1, () => {\n spawnPipe();\n });\n\n let score = 0;\n\n // display score\n const scoreLabel = game.add([\n text(score.toString()),\n anchor(\"center\"),\n pos(width() / 2, 80),\n fixed(),\n z(100),\n ]);\n\n function addScore() {\n score++;\n scoreLabel.text = score.toString();\n play(\"score\");\n }\n\n let curTween = null;\n\n onKeyPress(\"p\", () => {\n game.paused = !game.paused;\n if (curTween) curTween.cancel();\n curTween = tween(\n pauseMenu.pos,\n game.paused ? center() : center().add(0, 700),\n 1,\n (p) => pauseMenu.pos = p,\n easings.easeOutElastic,\n );\n if (game.paused) {\n pauseMenu.hidden = false;\n pauseMenu.paused = false;\n }\n else {\n curTween.onEnd(() => {\n pauseMenu.hidden = true;\n pauseMenu.paused = true;\n });\n }\n });\n\n const pauseMenu = add([\n rect(300, 400),\n color(255, 255, 255),\n outline(4),\n anchor(\"center\"),\n pos(center().add(0, 700)),\n ]);\n\n pauseMenu.hidden = true;\n pauseMenu.paused = true;\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", - "index": "50" - }, - { - "name": "physics", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\nsetGravity(300);\n\nconst trajectoryText = add([\n pos(20, 20),\n text(`0`),\n]);\n\nfunction ballistics(pos, vel, t) {\n return pos.add(vel.scale(t)).add(\n vec2(0, 1).scale(getGravity() * t * t * 0.5),\n );\n}\n\nlet y;\n\nonDraw(() => {\n drawCurve(t => ballistics(vec2(50, 100), vec2(200, -100), t * 2), {\n width: 2,\n color: RED,\n });\n});\n\nonClick(() => {\n const startTime = time();\n let results = [];\n const bean = add([\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(50, 100),\n body(),\n offscreen({ destroy: true }),\n {\n draw() {\n drawLine({\n p1: vec2(-40, 0),\n p2: vec2(40, 0),\n width: 2,\n color: GREEN,\n });\n drawLine({\n p1: vec2(0, -40),\n p2: vec2(0, 40),\n width: 2,\n color: GREEN,\n });\n },\n update() {\n const t = time() - startTime;\n if (t >= 2) return;\n results.push([\n t,\n this.pos.y,\n ballistics(vec2(50, 100), vec2(200, -100), t).y,\n ]);\n },\n destroy() {\n const a = results.map(d =>\n Math.sqrt((d[1] - d[2]) * (d[1] - d[2]))\n ).reduce((s, v) => s + v, 0) / results.length;\n trajectoryText.text = `${a.toFixed(2)}`;\n },\n },\n ]);\n bean.vel = vec2(200, -100);\n});\n\nfunction highestPoint(pos, vel) {\n return pos.y - vel.y * vel.y / (2 * getGravity());\n}\n\nconst heightGoal = highestPoint(vec2(50, 300), vec2(0, -200));\nlet heightResult = 0;\n\nonDraw(() => {\n y = highestPoint(vec2(50, 300), vec2(0, -200));\n drawLine({\n p1: vec2(10, y),\n p2: vec2(90, y),\n width: 2,\n color: RED,\n });\n});\n\nconst heightText = add([\n pos(100, heightGoal),\n text(`0%`),\n]);\n\nonUpdate(() => {\n heightText.text =\n `${((100 * (heightResult - heightGoal) / heightGoal).toFixed(2))}%`;\n});\n\nonClick(() => {\n y = highestPoint(vec2(50, 300), vec2(0, -200));\n const bean = add([\n sprite(\"bag\"),\n anchor(\"center\"),\n pos(50, 300),\n body(),\n offscreen({ destroy: true }),\n {\n draw() {\n drawLine({\n p1: vec2(-40, 0),\n p2: vec2(40, 0),\n width: 2,\n color: GREEN,\n });\n },\n update() {\n if (this.vel.y <= 0) {\n heightResult = this.pos.y;\n }\n },\n },\n ]);\n bean.vel = vec2(0, -200);\n});\n", - "index": "51" - }, - { - "name": "physicsfactory", - "code": "// @ts-check\n\nkaplay();\n\nsetGravity(300);\n\n// Conveyor belt moving right\nadd([\n pos(100, 300),\n rect(200, 20),\n area(),\n body({ isStatic: true }),\n surfaceEffector({ speed: 20 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(2, 2),\n vec2(12, 10),\n vec2(2, 18),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Conveyor belt moving left\nadd([\n pos(80, 400),\n rect(250, 20),\n area(),\n body({ isStatic: true }),\n surfaceEffector({ speed: -20 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(12, 2),\n vec2(2, 10),\n vec2(12, 18),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Windtunnel moving up\nadd([\n pos(20, 150),\n rect(50, 300),\n area(),\n areaEffector({ forceAngle: -90, forceMagnitude: 150 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(25, 2),\n vec2(48, 12),\n vec2(2, 12),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Magnet\nadd([\n pos(85, 50),\n rect(90, 90),\n anchor(\"center\"),\n area(),\n pointEffector({ forceMagnitude: 300 }),\n {\n draw() {\n drawCircle({\n pos: vec2(0, 0),\n radius: 5,\n color: RED,\n });\n },\n },\n]);\n\n// Continuous boxes\nloop(5, () => {\n add([\n pos(100, 100),\n rect(20, 20),\n color(RED),\n area(),\n body(),\n offscreen({ destroy: true, distance: 10 }),\n ]);\n});\n\n// A box\nadd([\n pos(500, 100),\n rect(20, 20),\n color(RED),\n area(),\n body({ mass: 10 }),\n // offscreen({ destroy: true }),\n]);\n\n// Water\nadd([\n pos(400, 200),\n rect(200, 100),\n color(BLUE),\n opacity(0.5),\n area(),\n buoyancyEffector({ surfaceLevel: 200, density: 6 }),\n]);\n", - "index": "52" - }, - { - "name": "platformer", - "code": "// @ts-check\n\nkaplay({\n background: [141, 183, 255],\n});\n\n// load assets\nloadSprite(\"bigyoshi\", \"/examples/sprites/YOSHI.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"bag\", \"/sprites/bag.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\nloadSprite(\"prize\", \"/sprites/jumpy.png\");\nloadSprite(\"apple\", \"/sprites/apple.png\");\nloadSprite(\"portal\", \"/sprites/portal.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSound(\"coin\", \"/examples/sounds/score.mp3\");\nloadSound(\"powerup\", \"/examples/sounds/powerup.mp3\");\nloadSound(\"blip\", \"/examples/sounds/blip.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"portal\", \"/examples/sounds/portal.mp3\");\n\nsetGravity(3200);\n\n// custom component controlling enemy patrol movement\nfunction patrol(speed = 60, dir = 1) {\n return {\n id: \"patrol\",\n require: [\"pos\", \"area\"],\n add() {\n this.on(\"collide\", (obj, col) => {\n if (col.isLeft() || col.isRight()) {\n dir = -dir;\n }\n });\n },\n update() {\n this.move(speed * dir, 0);\n },\n };\n}\n\n// custom component that makes stuff grow big\nfunction big() {\n let timer = 0;\n let isBig = false;\n let destScale = 1;\n return {\n // component id / name\n id: \"big\",\n // it requires the scale component\n require: [\"scale\"],\n // this runs every frame\n update() {\n if (isBig) {\n timer -= dt();\n if (timer <= 0) {\n this.smallify();\n }\n }\n this.scale = this.scale.lerp(vec2(destScale), dt() * 6);\n },\n // custom methods\n isBig() {\n return isBig;\n },\n smallify() {\n destScale = 1;\n timer = 0;\n isBig = false;\n },\n biggify(time) {\n destScale = 2;\n timer = time;\n isBig = true;\n },\n };\n}\n\n// define some constants\nconst JUMP_FORCE = 1320;\nconst MOVE_SPEED = 480;\nconst FALL_DEATH = 2400;\n\nconst LEVELS = [\n [\n \" 0 \",\n \" -- \",\n \" $$ \",\n \" % === \",\n \" \",\n \" ^^ > = @\",\n \"============\",\n ],\n [\n \" $\",\n \" $\",\n \" $\",\n \" $\",\n \" $\",\n \" $$ = $\",\n \" % ==== = $\",\n \" = $\",\n \" = \",\n \" ^^ = > = @\",\n \"===========================\",\n ],\n [\n \" $ $ $ $ $\",\n \" $ $ $ $ $\",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" ^^^^>^^^^>^^^^>^^^^>^^^^^@\",\n \"===========================\",\n ],\n];\n\n// define what each symbol means in the level graph\nconst levelConf = {\n tileWidth: 64,\n tileHeight: 64,\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"platform\",\n ],\n \"-\": () => [\n sprite(\"steel\"),\n area(),\n body({ isStatic: true }),\n offscreen({ hide: true }),\n anchor(\"bot\"),\n ],\n \"0\": () => [\n sprite(\"bag\"),\n area(),\n body({ isStatic: true }),\n offscreen({ hide: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n pos(0, -9),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"coin\",\n ],\n \"%\": () => [\n sprite(\"prize\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"prize\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"danger\",\n ],\n \"#\": () => [\n sprite(\"apple\"),\n area(),\n anchor(\"bot\"),\n body(),\n offscreen({ hide: true }),\n \"apple\",\n ],\n \">\": () => [\n sprite(\"ghosty\"),\n area(),\n anchor(\"bot\"),\n body(),\n patrol(),\n offscreen({ hide: true }),\n \"enemy\",\n ],\n \"@\": () => [\n sprite(\"portal\"),\n area({ scale: 0.5 }),\n anchor(\"bot\"),\n pos(0, -12),\n offscreen({ hide: true }),\n \"portal\",\n ],\n },\n};\n\nscene(\"game\", ({ levelId, coins } = { levelId: 0, coins: 0 }) => {\n // add level to scene\n const level = addLevel(LEVELS[levelId ?? 0], levelConf);\n\n // define player object\n const player = add([\n sprite(\"bean\"),\n pos(0, 0),\n area(),\n scale(1),\n // makes it fall to gravity and jumpable\n body(),\n // the custom component we defined above\n big(),\n anchor(\"bot\"),\n ]);\n\n // action() runs every frame\n player.onUpdate(() => {\n // center camera to player\n camPos(player.pos);\n // check fall death\n if (player.pos.y >= FALL_DEATH) {\n go(\"lose\");\n }\n });\n\n player.onBeforePhysicsResolve((collision) => {\n if (collision.target.is([\"platform\", \"soft\"]) && player.isJumping()) {\n collision.preventResolution();\n }\n });\n\n player.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.pos);\n });\n\n // if player onCollide with any obj with \"danger\" tag, lose\n player.onCollide(\"danger\", () => {\n go(\"lose\");\n play(\"hit\");\n });\n\n player.onCollide(\"portal\", () => {\n play(\"portal\");\n if (levelId + 1 < LEVELS.length) {\n go(\"game\", {\n levelId: levelId + 1,\n coins: coins,\n });\n }\n else {\n go(\"win\");\n }\n });\n\n player.onGround((l) => {\n if (l.is(\"enemy\")) {\n player.jump(JUMP_FORCE * 1.5);\n destroy(l);\n addKaboom(player.pos);\n play(\"powerup\");\n }\n });\n\n player.onCollide(\"enemy\", (e, col) => {\n // if it's not from the top, die\n if (!col?.isBottom()) {\n go(\"lose\");\n play(\"hit\");\n }\n });\n\n let hasApple = false;\n\n // grow an apple if player's head bumps into an obj with \"prize\" tag\n player.onHeadbutt((obj) => {\n if (obj.is(\"prize\") && !hasApple) {\n const apple = level.spawn(\"#\", obj.tilePos.sub(0, 1));\n apple.jump();\n hasApple = true;\n play(\"blip\");\n }\n });\n\n // player grows big onCollide with an \"apple\" obj\n player.onCollide(\"apple\", (a) => {\n destroy(a);\n // as we defined in the big() component\n player.biggify(3);\n hasApple = false;\n play(\"powerup\");\n });\n\n let coinPitch = 0;\n\n onUpdate(() => {\n if (coinPitch > 0) {\n coinPitch = Math.max(0, coinPitch - dt() * 100);\n }\n });\n\n player.onCollide(\"coin\", (c) => {\n destroy(c);\n play(\"coin\", {\n detune: coinPitch,\n });\n coinPitch += 100;\n coins += 1;\n coinsLabel.text = coins;\n });\n\n const coinsLabel = add([\n text(coins),\n pos(24, 24),\n fixed(),\n ]);\n\n function jump() {\n // these 2 functions are provided by body() component\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n }\n\n // jump with space\n onKeyPress(\"space\", jump);\n\n onKeyDown(\"left\", () => {\n player.move(-MOVE_SPEED, 0);\n });\n\n onKeyDown(\"right\", () => {\n player.move(MOVE_SPEED, 0);\n });\n\n onKeyPress(\"down\", () => {\n player.gravityScale = 3;\n });\n\n onKeyRelease(\"down\", () => {\n player.gravityScale = 1;\n });\n\n onGamepadButtonPress(\"south\", jump);\n\n onGamepadStick(\"left\", (v) => {\n player.move(v.x * MOVE_SPEED, 0);\n });\n\n onKeyPress(\"f\", () => {\n setFullscreen(!isFullscreen());\n });\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n ]);\n onKeyPress(() => go(\"game\"));\n});\n\nscene(\"win\", () => {\n add([\n text(\"You Win\"),\n ]);\n onKeyPress(() => go(\"game\"));\n});\n\ngo(\"game\");\n", - "index": "53" - }, - { - "name": "polygon", - "code": "// @ts-check\n\nkaplay({\n background: [0, 0, 0],\n});\n\nadd([\n text(\"Drag corners of the polygon\"),\n pos(20, 20),\n]);\n\n// Make a weird shape\nconst poly = add([\n polygon([\n vec2(0, 0),\n vec2(100, 0),\n vec2(100, 200),\n vec2(200, 200),\n vec2(200, 300),\n vec2(100, 300),\n vec2(100, 200),\n vec2(0, 200),\n ], {\n colors: [\n rgb(128, 255, 128),\n rgb(255, 128, 128),\n rgb(128, 128, 255),\n rgb(255, 128, 128),\n rgb(128, 128, 128),\n rgb(128, 255, 128),\n rgb(255, 128, 128),\n rgb(128, 255, 128),\n ],\n triangulate: true,\n }),\n pos(150, 150),\n area(),\n color(),\n]);\n\nlet dragging = null;\nlet hovering = null;\n\npoly.onDraw(() => {\n const triangles = triangulate(poly.pts);\n for (const triangle of triangles) {\n drawTriangle({\n p1: triangle[0],\n p2: triangle[1],\n p3: triangle[2],\n fill: false,\n outline: { color: BLACK },\n });\n }\n if (hovering !== null) {\n drawCircle({\n pos: poly.pts[hovering],\n radius: 16,\n });\n }\n});\n\nonUpdate(() => {\n if (isConvex(poly.pts)) {\n poly.color = WHITE;\n }\n else {\n poly.color = rgb(192, 192, 192);\n }\n});\n\nonMousePress(() => {\n dragging = hovering;\n});\n\nonMouseRelease(() => {\n dragging = null;\n});\n\nonMouseMove(() => {\n hovering = null;\n const mp = mousePos().sub(poly.pos);\n for (let i = 0; i < poly.pts.length; i++) {\n if (mp.dist(poly.pts[i]) < 16) {\n hovering = i;\n break;\n }\n }\n if (dragging !== null) {\n poly.pts[dragging] = mousePos().sub(poly.pos);\n }\n});\n\npoly.onHover(() => {\n poly.color = rgb(200, 200, 255);\n});\n\npoly.onHoverEnd(() => {\n poly.color = rgb(255, 255, 255);\n});\n", - "index": "54" - }, - { - "name": "polygonuv", - "code": "// @ts-check\n\nkaplay();\n\n// Load a sprite asset from \"sprites/bean.png\", with the name \"bean\"\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// A \"Game Object\" is the basic unit of entity in kaboom\n// Game objects are composed from components\n// Each component gives a game object certain capabilities\n\n// add() assembles a game object from a list of components and add to game, returns the reference of the game object\nconst player = add([\n sprite(\"bean\"), // sprite() component makes it render as a sprite\n pos(120, 80), // pos() component gives it position, also enables movement\n rotate(0), // rotate() component gives it rotation\n anchor(\"center\"), // anchor() component defines the pivot point (defaults to \"topleft\")\n]);\n\n// .onUpdate() is a method on all game objects, it registers an event that runs every frame\nplayer.onUpdate(() => {\n // .angle is a property provided by rotate() component, here we're incrementing the angle by 120 degrees per second, dt() is the time elapsed since last frame in seconds\n player.angle += 120 * dt();\n});\n\n// Make sure all sprites have been loaded\nonLoad(() => {\n // Get the texture and uv for ghosty\n const data = getSprite(\"ghosty\").data;\n const tex = data.tex;\n const quad = data.frames[0];\n // Add multiple game objects\n for (let i = 0; i < 3; i++) {\n // generate a random point on screen\n // width() and height() gives the game dimension\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n pos(x, y),\n {\n q: quad.clone(),\n pts: [\n vec2(-32, -32),\n vec2(32, -32),\n vec2(32, 32),\n vec2(-32, 32),\n ],\n // Draw the polygon\n draw() {\n const q = this.q;\n drawPolygon({\n pts: pts,\n uv: [\n vec2(q.x, q.y),\n vec2(q.x + q.w, q.y),\n vec2(q.x + q.w, q.y + q.h),\n vec2(q.x, q.y + q.h),\n ],\n tex: tex,\n });\n },\n // Update the vertices each frame\n update() {\n pts = [\n vec2(-32, -32),\n vec2(32, -32),\n vec2(32, 32),\n vec2(-32, 32),\n ].map((p, index) =>\n p.add(\n 5 * Math.cos((time() + index * 0.25) * Math.PI),\n 5 * Math.sin((time() + index * 0.25) * Math.PI),\n )\n );\n },\n },\n ]);\n }\n});\n", - "index": "55" - }, - { - "name": "pong", - "code": "// @ts-check\n\nkaplay({\n background: [255, 255, 128],\n});\n\n// add paddles\nadd([\n pos(40, 0),\n rect(20, 80),\n outline(4),\n anchor(\"center\"),\n area(),\n \"paddle\",\n]);\n\nadd([\n pos(width() - 40, 0),\n rect(20, 80),\n outline(4),\n anchor(\"center\"),\n area(),\n \"paddle\",\n]);\n\n// move paddles with mouse\nonUpdate(\"paddle\", (p) => {\n p.pos.y = mousePos().y;\n});\n\n// score counter\nlet score = 0;\n\nadd([\n text(score.toString()),\n pos(center()),\n anchor(\"center\"),\n z(50),\n {\n update() {\n this.text = score.toString();\n },\n },\n]);\n\n// ball\nlet speed = 480;\n\nconst ball = add([\n pos(center()),\n circle(16),\n outline(4),\n area({ shape: new Rect(vec2(-16), 32, 32) }),\n { vel: Vec2.fromAngle(rand(-20, 20)) },\n]);\n\n// move ball, bounce it when touche horizontal edges, respawn when touch vertical edges\nball.onUpdate(() => {\n ball.move(ball.vel.scale(speed));\n if (ball.pos.x < 0 || ball.pos.x > width()) {\n score = 0;\n ball.pos = center();\n ball.vel = Vec2.fromAngle(rand(-20, 20));\n speed = 320;\n }\n if (ball.pos.y < 0 || ball.pos.y > height()) {\n ball.vel.y = -ball.vel.y;\n }\n});\n\n// bounce when touch paddle\nball.onCollide(\"paddle\", (p) => {\n speed += 60;\n ball.vel = Vec2.fromAngle(ball.pos.angle(p.pos));\n score++;\n});\n", - "index": "56" - }, - { - "name": "postEffect", - "code": "// @ts-check\n\n// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst effects = {\n crt: () => ({\n \"u_flatness\": 3,\n }),\n vhs: () => ({\n \"u_intensity\": 12,\n }),\n pixelate: () => ({\n \"u_resolution\": vec2(width(), height()),\n \"u_size\": wave(2, 16, time() * 2),\n }),\n invert: () => ({\n \"u_invert\": 1,\n }),\n light: () => ({\n \"u_radius\": 64,\n \"u_blur\": 64,\n \"u_resolution\": vec2(width(), height()),\n \"u_mouse\": mousePos(),\n }),\n};\n\nfor (const effect in effects) {\n loadShaderURL(effect, null, `/examples/shaders/${effect}.frag`);\n}\n\nlet curEffect = 0;\nconst SPEED = 480;\n\nsetGravity(2400);\n\nconst level = addLevel([\n // Design the level layout with symbols\n \"@ ^ $$\",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100, 200),\n // Define what each symbol means (in components)\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\n// Back to the original position if hit a \"danger\" item\nplayer.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n});\n\n// Eat the coin!\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n});\n\nonKeyPress(\"up\", () => {\n const list = Object.keys(effects);\n curEffect = curEffect === 0 ? list.length - 1 : curEffect - 1;\n label.text = list[curEffect];\n});\n\nonKeyPress(\"down\", () => {\n const list = Object.keys(effects);\n curEffect = (curEffect + 1) % list.length;\n label.text = list[curEffect];\n});\n\nconst label = add([\n pos(8, 8),\n text(Object.keys(effects)[curEffect]),\n]);\n\nadd([\n pos(8, height() - 8),\n text(\"Press up / down to switch effects\"),\n anchor(\"botleft\"),\n]);\n\nonUpdate(() => {\n const effect = Object.keys(effects)[curEffect];\n usePostEffect(effect, effects[effect]());\n});\n", - "index": "57" - }, - { - "name": "query", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst bean = add([\n pos(50, 50),\n sprite(\"bean\"),\n color(WHITE),\n \"bean\",\n]);\n\nadd([\n pos(200, 50),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n]);\n\nadd([\n pos(400, 50),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n]);\n\nadd([\n pos(100, 250),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n named(\"Candy&Carmel\"),\n]);\n\nfunction makeButton(p, t, cb) {\n const button = add([\n pos(p),\n rect(150, 40, { radius: 5 }),\n anchor(\"center\"),\n color(WHITE),\n area(),\n \"button\",\n ]);\n button.add([\n text(t),\n color(BLACK),\n anchor(\"center\"),\n area(),\n ]);\n button.onClick(() => {\n get(\"button\").forEach(o => o.color = WHITE);\n button.color = GREEN;\n cb();\n });\n}\n\nmakeButton(vec2(200, 400), \"bean\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ include: \"bean\" }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(360, 400), \"ghosty\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ include: \"ghosty\" }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(200, 450), \"near\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n bean.query({\n distance: 150,\n distanceOp: \"near\",\n hierarchy: \"siblings\",\n exclude: \"button\",\n }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(360, 450), \"far\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n bean.query({\n distance: 150,\n distanceOp: \"far\",\n hierarchy: \"siblings\",\n exclude: \"button\",\n }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(520, 400), \"name\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ name: \"Candy&Carmel\" }).forEach(o => o.color = RED);\n});\n", - "index": "58" - }, - { - "name": "raycastLevelTest", - "code": "// @ts-check\n\nkaplay();\n\nconst level = addLevel([\n \"a\",\n], {\n tileHeight: 100,\n tileWidth: 100,\n tiles: {\n a: () => [\n rect(32, 32),\n area(),\n color(RED),\n ],\n },\n});\ntry {\n level.raycast(vec2(50, 50), vec2(-50, -50));\n} catch (e) {\n debug.error(e.stack);\n throw e;\n}\n", - "index": "59" - }, - { - "name": "raycastObject", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n area(),\n]);\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonDraw(\"selected\", (s) => {\n const bbox = s.worldArea().bbox();\n drawRect({\n pos: bbox.pos.sub(s.pos),\n width: bbox.width,\n height: bbox.height,\n outline: {\n color: YELLOW,\n width: 1,\n },\n fill: false,\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"area\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.hasPoint(pos));\n const selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n\nfunction laser() {\n return {\n draw() {\n drawTriangle({\n p1: vec2(-16, -16),\n p2: vec2(16, 0),\n p3: vec2(-16, 16),\n pos: vec2(0, 0),\n color: this.color,\n });\n if (this.showRing || this.is(\"turn\")) {\n drawCircle({\n pos: vec2(0, 0),\n radius: 28,\n outline: {\n color: RED,\n width: 4,\n },\n fill: false,\n });\n }\n pushTransform();\n pushRotate(-this.angle);\n const MAX_TRACE_DEPTH = 3;\n const MAX_DISTANCE = 400;\n let origin = this.pos;\n let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);\n let traceDepth = 0;\n while (traceDepth < MAX_TRACE_DEPTH) {\n const hit = raycast(origin, direction, [\"laser\"]);\n if (!hit) {\n drawLine({\n p1: origin.sub(this.pos),\n p2: origin.add(direction).sub(this.pos),\n width: 1,\n color: this.color,\n });\n break;\n }\n const pos = hit.point.sub(this.pos);\n // Draw hit point\n drawCircle({\n pos: pos,\n radius: 4,\n color: this.color,\n });\n // Draw hit normal\n drawLine({\n p1: pos,\n p2: pos.add(hit.normal.scale(20)),\n width: 1,\n color: BLUE,\n });\n // Draw hit distance\n drawLine({\n p1: origin.sub(this.pos),\n p2: pos,\n width: 1,\n color: this.color,\n });\n // Offset the point slightly, otherwise it might be too close to the surface\n // and give internal reflections\n origin = hit.point.add(hit.normal.scale(0.001));\n // Reflect vector\n direction = direction.reflect(hit.normal);\n traceDepth++;\n }\n popTransform();\n },\n showRing: false,\n };\n}\n\nconst ray = add([\n pos(150, 270),\n rotate(-45),\n anchor(\"center\"),\n rect(64, 64),\n area(),\n laser(0),\n color(RED),\n opacity(0.0),\n \"laser\",\n]);\n\nget(\"laser\").forEach(laser => {\n laser.onHover(() => {\n laser.showRing = true;\n });\n laser.onHoverEnd(() => {\n laser.showRing = false;\n });\n laser.onClick(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n if (laser.pos.sub(mousePos()).slen() > 28 * 28) {\n laser.use(\"turn\");\n }\n else {\n laser.use(\"selected\");\n }\n });\n});\n", - "index": "60" - }, - { - "name": "raycastShape", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawRect({\n pos: vec2(-1, -1),\n width: 3,\n height: 3,\n color: this.color,\n });\n },\n getShape() {\n // This would be point if we had a real class for it\n return new Rect(vec2(-1, -1).add(this.pos), 3, 3);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nfunction rayCastShapes(origin, direction) {\n let minHit;\n const shapes = get(\"shape\");\n shapes.forEach(s => {\n const shape = s.getShape();\n const hit = shape.raycast(origin, direction);\n if (hit) {\n if (minHit) {\n if (hit.fraction < minHit.fraction) {\n minHit = hit;\n }\n }\n else {\n minHit = hit;\n }\n }\n });\n return minHit;\n}\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.getShape().contains(pos));\n const selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n\nfunction laser() {\n return {\n draw() {\n drawTriangle({\n p1: vec2(-16, -16),\n p2: vec2(16, 0),\n p3: vec2(-16, 16),\n pos: vec2(0, 0),\n color: this.color,\n });\n if (this.showRing || this.is(\"turn\")) {\n drawCircle({\n pos: vec2(0, 0),\n radius: 28,\n outline: {\n color: RED,\n width: 4,\n },\n fill: false,\n });\n }\n pushTransform();\n pushRotate(-this.angle);\n const MAX_TRACE_DEPTH = 3;\n const MAX_DISTANCE = 400;\n let origin = this.pos;\n let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);\n let traceDepth = 0;\n while (traceDepth < MAX_TRACE_DEPTH) {\n const hit = rayCastShapes(origin, direction);\n if (!hit) {\n drawLine({\n p1: origin.sub(this.pos),\n p2: origin.add(direction).sub(this.pos),\n width: 1,\n color: this.color,\n });\n break;\n }\n const pos = hit.point.sub(this.pos);\n // Draw hit point\n drawCircle({\n pos: pos,\n radius: 4,\n color: this.color,\n });\n // Draw hit normal\n drawLine({\n p1: pos,\n p2: pos.add(hit.normal.scale(20)),\n width: 1,\n color: BLUE,\n });\n // Draw hit distance\n drawLine({\n p1: origin.sub(this.pos),\n p2: pos,\n width: 1,\n color: this.color,\n });\n // Offset the point slightly, otherwise it might be too close to the surface\n // and give internal reflections\n origin = hit.point.add(hit.normal.scale(0.001));\n // Reflect vector\n direction = direction.reflect(hit.normal);\n traceDepth++;\n }\n popTransform();\n },\n showRing: false,\n };\n}\n\nconst ray = add([\n pos(150, 270),\n rotate(-45),\n anchor(\"center\"),\n rect(64, 64),\n area(),\n laser(0),\n color(RED),\n opacity(0.0),\n \"laser\",\n]);\n\nget(\"laser\").forEach(laser => {\n laser.onHover(() => {\n laser.showRing = true;\n });\n laser.onHoverEnd(() => {\n laser.showRing = false;\n });\n laser.onClick(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n if (laser.pos.sub(mousePos()).slen() > 28 * 28) {\n laser.use(\"turn\");\n }\n else {\n laser.use(\"selected\");\n }\n });\n});\n", - "index": "61" - }, - { - "name": "raycaster3d", - "code": "// @ts-check\n\n// Start kaboom\nkaplay();\n\n// load assets\nlet bean;\nlet objSlices = [];\nlet wall;\nlet slices = [];\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"wall\", \"sprites/brick_wall.png\");\n\nonLoad(() => {\n bean = getSprite(\"bean\").data;\n for (let i = 0; i < bean.width; i++) {\n objSlices.push(\n bean.frames[0].scale(\n new Quad(i / bean.width, 0, 1 / bean.width, 1),\n ),\n );\n }\n\n wall = getSprite(\"wall\").data;\n for (let i = 0; i < wall.width; i++) {\n slices.push(\n wall.frames[0].scale(\n new Quad(i / wall.width, 0, 1 / wall.width, 1),\n ),\n );\n }\n});\n\nfunction rayCastGrid(origin, direction, gridPosHit, maxDistance = 64) {\n const pos = origin;\n const len = direction.len();\n const dir = direction.scale(1 / len);\n let t = 0;\n let gridPos = vec2(Math.floor(origin.x), Math.floor(origin.y));\n const step = vec2(dir.x > 0 ? 1 : -1, dir.y > 0 ? 1 : -1);\n const tDelta = vec2(Math.abs(1 / dir.x), Math.abs(1 / dir.y));\n let dist = vec2(\n (step.x > 0) ? (gridPos.x + 1 - origin.x) : (origin.x - gridPos.x),\n (step.y > 0) ? (gridPos.y + 1 - origin.y) : (origin.y - gridPos.y),\n );\n let tMax = vec2(\n (tDelta.x < Infinity) ? tDelta.x * dist.x : Infinity,\n (tDelta.y < Infinity) ? tDelta.y * dist.y : Infinity,\n );\n let steppedIndex = -1;\n while (t <= maxDistance) {\n const hit = gridPosHit(gridPos);\n if (hit === true) {\n return {\n point: pos.add(dir.scale(t)),\n normal: vec2(\n steppedIndex === 0 ? -step.x : 0,\n steppedIndex === 1 ? -step.y : 0,\n ),\n t: t / len, // Since dir is normalized, t is len times too large\n gridPos,\n };\n }\n else if (hit) {\n return hit;\n }\n if (tMax.x < tMax.y) {\n gridPos.x += step.x;\n t = tMax.x;\n tMax.x += tDelta.x;\n steppedIndex = 0;\n }\n else {\n gridPos.y += step.y;\n t = tMax.y;\n tMax.y += tDelta.y;\n steppedIndex = 1;\n }\n }\n\n return null;\n}\n\nfunction raycastEdge(origin, direction, line) {\n const a = origin;\n const c = line.p1.add(line.pos);\n const d = line.p2.add(line.pos);\n const ab = direction;\n const cd = d.sub(c);\n let abxcd = ab.cross(cd);\n // If parallel, no intersection\n if (Math.abs(abxcd) < Number.EPSILON) {\n return false;\n }\n const ac = c.sub(a);\n const s = ac.cross(cd) / abxcd;\n // s is the percentage of the position of the intersection on cd\n if (s <= 0 || s >= 1) {\n return false;\n }\n const t = ac.cross(ab) / abxcd;\n // t is the percentage of the position of the intersection on ab\n if (t <= 0 || t >= 1) {\n return false;\n }\n\n const normal = cd.normal().unit();\n if (direction.dot(normal) > 0) {\n normal.x *= -1;\n normal.y *= -1;\n }\n\n return {\n point: a.add(ab.scale(s)),\n normal: normal,\n t: s,\n s: t,\n object: line,\n };\n}\n\nfunction rayCastAsciiGrid(origin, direction, grid) {\n origin = origin.scale(1 / 16);\n direction = direction.scale(1 / 16);\n const objects = [];\n const hit = rayCastGrid(origin, direction, ({ x, y }) => {\n if (y >= 0 && y < grid.length) {\n const row = grid[y];\n if (x >= 0 && x < row.length) {\n if (row[x] === \"&\") {\n const perp = direction.normal().unit();\n const planeP1 = perp.scale(-0.2);\n const planeP2 = perp.scale(0.2);\n const objectHit = raycastEdge(origin, direction, {\n pos: vec2(x + 0.5, y + 0.5),\n p1: planeP1,\n p2: planeP2,\n });\n if (objectHit) {\n objects.push(objectHit);\n }\n }\n return row[x] !== \" \" && row[x] !== \"&\";\n }\n }\n }, direction.len());\n if (hit) {\n hit.point = hit.point.scale(16);\n hit.object = { color: colors[grid[hit.gridPos.y][hit.gridPos.x]] };\n hit.objects = objects;\n }\n return hit;\n}\n\nconst colors = {\n \"#\": RED,\n \"$\": GREEN,\n \"%\": BLUE,\n \"&\": YELLOW,\n};\n\nconst grid = [\n \"##################\",\n \"# #\",\n \"# $$$$$$$ $$$$$$ #\",\n \"# $ $ #\",\n \"# $ %% %%%%%%% $ #\",\n \"# $ % % $ #\",\n \"#&$&%%%%% %%%&$&#\",\n \"# $ % $ #\",\n \"# $ %%%%%%%%%% #\",\n \"# $ $ #\",\n \"# $$$$$$$ $$$$$$ #\",\n \"# & #\",\n \"##################\",\n];\n\nconst camera = add([\n pos(7 * 16, 11 * 16 + 8),\n rotate(0),\n z(-1),\n rect(8, 8),\n anchor(\"center\"),\n area(),\n opacity(0),\n body(),\n {\n draw() {\n pushTransform();\n pushRotate(-this.angle);\n drawCircle({\n pos: vec2(),\n radius: 4,\n color: RED,\n });\n const dir = Vec2.fromAngle(this.angle);\n const perp = dir.normal();\n const planeP1 = this.pos.add(dir.scale(this.focalLength)).add(\n perp.scale(this.fov),\n ).sub(this.pos);\n const planeP2 = this.pos.add(dir.scale(this.focalLength)).sub(\n perp.scale(this.fov),\n ).sub(this.pos);\n drawLine({\n p1: planeP1,\n p2: planeP2,\n width: 1,\n color: RED,\n });\n pushTranslate(this.pos.scale(-1).add(300, 50));\n drawRect({\n width: 240,\n height: 120,\n color: rgb(100, 100, 100),\n });\n drawRect({\n pos: vec2(0, 120),\n width: 240,\n height: 120,\n color: rgb(128, 128, 128),\n });\n for (let x = 0; x <= 120; x++) {\n let direction = lerp(planeP1, planeP2, x / 120).scale(6);\n const hit = rayCastAsciiGrid(this.pos, direction, grid);\n if (hit) {\n const t = hit.t;\n // Distance to attenuate light\n const d = (1 - t)\n * ((hit.normal.x + hit.normal.y) < 0 ? 0.5 : 1);\n // Horizontal texture slice\n let u = Math.abs(hit.normal.x) > Math.abs(hit.normal.y)\n ? hit.point.y\n : hit.point.x;\n u = (u % 16) / 16;\n u = u - Math.floor(u);\n // Height of the wall\n const h = 240 / (t * direction.len() / 16);\n\n drawUVQuad({\n width: 2,\n height: h,\n pos: vec2(x * 2, 120 - h / 2),\n tex: wall.tex,\n quad: slices[Math.round(u * (wall.width - 1))],\n color: BLACK.lerp(WHITE, d),\n });\n\n // If we hit any objects\n if (hit.objects) {\n hit.objects.reverse().forEach(o => {\n const t = o.t;\n // Wall and object height\n const wh = 240 / (t * direction.len() / 16);\n const oh = 140 / (t * direction.len() / 16);\n // Slice to render\n let u = o.s;\n drawUVQuad({\n width: 2,\n height: oh,\n pos: vec2(x * 2, 120 + wh / 2 - oh),\n tex: bean.tex,\n quad:\n objSlices[Math.round(u * (bean.width - 1))],\n color: BLACK.lerp(WHITE, u),\n });\n });\n }\n }\n }\n popTransform();\n },\n focalLength: 40,\n fov: 10,\n },\n]);\n\naddLevel(grid, {\n pos: vec2(0, 0),\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \"#\": () => [\n rect(16, 16),\n color(RED),\n area(),\n body({ isStatic: true }),\n ],\n \"$\": () => [\n rect(16, 16),\n color(GREEN),\n area(),\n body({ isStatic: true }),\n ],\n \"%\": () => [\n rect(16, 16),\n color(BLUE),\n area(),\n body({ isStatic: true }),\n ],\n \"&\": () => [\n pos(4, 4),\n rect(8, 8),\n color(YELLOW),\n ],\n },\n});\n\nonKeyDown(\"up\", () => {\n camera.move(Vec2.fromAngle(camera.angle).scale(40));\n});\n\nonKeyDown(\"down\", () => {\n camera.move(Vec2.fromAngle(camera.angle).scale(-40));\n});\n\nonKeyDown(\"left\", () => {\n camera.angle -= 90 * dt();\n});\n\nonKeyDown(\"right\", () => {\n camera.angle += 90 * dt();\n});\n\nonKeyDown(\"f\", () => {\n camera.focalLength = Math.max(1, camera.focalLength - 10 * dt());\n});\n\nonKeyDown(\"g\", () => {\n camera.focalLength += 10 * dt();\n});\n\nonKeyDown(\"r\", () => {\n camera.fov = Math.max(1, camera.fov - 10 * dt());\n});\n\nonKeyDown(\"t\", () => {\n camera.fov += 10 * dt();\n});\n\nonKeyDown(\"p\", () => {\n debug.paused = !debug.paused;\n});\n\nlet lastPos = vec2();\n\nonTouchStart(pos => {\n lastPos = pos;\n});\n\nonTouchMove(pos => {\n const delta = pos.sub(lastPos);\n if (delta.x < 0) {\n camera.angle -= 90 * dt();\n }\n else if (delta.x > 0) {\n camera.angle += 90 * dt();\n }\n if (delta.y < 0) {\n camera.move(Vec2.fromAngle(camera.angle).scale(40));\n }\n else if (delta.y > 0) {\n camera.move(Vec2.fromAngle(camera.angle).scale(-40));\n }\n lastPos = pos;\n});\n", - "index": "62" - }, - { - "name": "rect", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n rect(100, 100, { radius: 20 }),\n pos(100, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: [10, 20, 30, 40] }),\n pos(250, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: [0, 20, 0, 20] }),\n pos(400, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: 20 }),\n pos(100, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n\nadd([\n rect(100, 100, { radius: [10, 20, 30, 40] }),\n pos(250, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n\nadd([\n rect(100, 100, { radius: [0, 20, 0, 20] }),\n pos(400, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n", - "index": "63" - }, - { - "name": "rpg", - "code": "// @ts-check\n\n// simple rpg style walk and talk\n\nkaplay({\n background: [74, 48, 82],\n});\n\nloadSprite(\"bag\", \"/sprites/bag.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\nloadSprite(\"door\", \"/sprites/door.png\");\nloadSprite(\"key\", \"/sprites/key.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nscene(\"main\", (levelIdx) => {\n const SPEED = 320;\n\n // character dialog data\n const characters = {\n \"a\": {\n sprite: \"bag\",\n msg: \"Hi Bean! You should get that key!\",\n },\n \"b\": {\n sprite: \"ghosty\",\n msg: \"Who are you? You can see me??\",\n },\n };\n\n // level layouts\n const levels = [\n [\n \"===|====\",\n \"= =\",\n \"= $ =\",\n \"= a =\",\n \"= =\",\n \"= @ =\",\n \"========\",\n ],\n [\n \"--------\",\n \"- -\",\n \"- $ -\",\n \"| -\",\n \"- b -\",\n \"- @ -\",\n \"--------\",\n ],\n ];\n\n const level = addLevel(levels[levelIdx], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(64, 64),\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n ],\n \"-\": () => [\n sprite(\"steel\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n ],\n \"$\": () => [\n sprite(\"key\"),\n area(),\n anchor(\"center\"),\n \"key\",\n ],\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"center\"),\n \"player\",\n ],\n \"|\": () => [\n sprite(\"door\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"door\",\n ],\n },\n // any() is a special function that gets called everytime there's a\n // symbole not defined above and is supposed to return what that symbol\n // means\n wildcardTile(ch) {\n const char = characters[ch];\n if (char) {\n return [\n sprite(char.sprite),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"character\",\n { msg: char.msg },\n ];\n }\n },\n });\n\n // get the player game obj by tag\n const player = level.get(\"player\")[0];\n\n function addDialog() {\n const h = 160;\n const pad = 16;\n const bg = add([\n pos(0, height() - h),\n rect(width(), h),\n color(0, 0, 0),\n z(100),\n ]);\n const txt = add([\n text(\"\", {\n width: width(),\n }),\n pos(0 + pad, height() - h + pad),\n z(100),\n ]);\n bg.hidden = true;\n txt.hidden = true;\n return {\n say(t) {\n txt.text = t;\n bg.hidden = false;\n txt.hidden = false;\n },\n dismiss() {\n if (!this.active()) {\n return;\n }\n txt.text = \"\";\n bg.hidden = true;\n txt.hidden = true;\n },\n active() {\n return !bg.hidden;\n },\n destroy() {\n bg.destroy();\n txt.destroy();\n },\n };\n }\n\n let hasKey = false;\n const dialog = addDialog();\n\n player.onCollide(\"key\", (key) => {\n destroy(key);\n hasKey = true;\n });\n\n player.onCollide(\"door\", () => {\n if (hasKey) {\n if (levelIdx + 1 < levels.length) {\n go(\"main\", levelIdx + 1);\n }\n else {\n go(\"win\");\n }\n }\n else {\n dialog.say(\"you got no key!\");\n }\n });\n\n // talk on touch\n player.onCollide(\"character\", (ch) => {\n dialog.say(ch.msg);\n });\n\n const dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n };\n\n for (const dir in dirs) {\n onKeyPress(dir, () => {\n dialog.dismiss();\n });\n onKeyDown(dir, () => {\n player.move(dirs[dir].scale(SPEED));\n });\n }\n});\n\nscene(\"win\", () => {\n add([\n text(\"You Win!\"),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n ]);\n});\n\ngo(\"main\", 0);\n", - "index": "64" - }, - { - "name": "runner", - "code": "// @ts-check\n\nconst FLOOR_HEIGHT = 48;\nconst JUMP_FORCE = 800;\nconst SPEED = 480;\n\n// initialize context\nkaplay();\n\nsetBackground(141, 183, 255);\n\n// load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nscene(\"game\", () => {\n // define gravity\n setGravity(2400);\n\n // add a game object to screen\n const player = add([\n // list of components\n sprite(\"bean\"),\n pos(80, 40),\n area(),\n body(),\n ]);\n\n // floor\n add([\n rect(width(), FLOOR_HEIGHT),\n outline(4),\n pos(0, height()),\n anchor(\"botleft\"),\n area(),\n body({ isStatic: true }),\n color(132, 101, 236),\n ]);\n\n function jump() {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n }\n\n // jump when user press space\n onKeyPress(\"space\", jump);\n onClick(jump);\n\n function spawnTree() {\n // add tree obj\n add([\n rect(48, rand(32, 96)),\n area(),\n outline(4),\n pos(width(), height() - FLOOR_HEIGHT),\n anchor(\"botleft\"),\n color(238, 143, 203),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n \"tree\",\n ]);\n\n // wait a random amount of time to spawn next tree\n wait(rand(0.5, 1.5), spawnTree);\n }\n\n // start spawning trees\n spawnTree();\n\n // lose if player collides with any game obj with tag \"tree\"\n player.onCollide(\"tree\", () => {\n // go to \"lose\" scene and pass the score\n go(\"lose\", score);\n burp();\n addKaboom(player.pos);\n });\n\n // keep track of score\n let score = 0;\n\n const scoreLabel = add([\n text(score.toString()),\n pos(24, 24),\n ]);\n\n // increment score every frame\n onUpdate(() => {\n score++;\n scoreLabel.text = score.toString();\n });\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 64),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 64),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", - "index": "65" - }, - { - "name": "scenes", - "code": "// @ts-check\n\n// Extend our game with multiple scenes\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"portal\", \"/sprites/portal.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"portal\", \"/examples/sounds/portal.mp3\");\n\nsetGravity(2400);\n\nconst SPEED = 480;\n\n// Design 2 levels\nconst LEVELS = [\n [\n \"@ ^ $$ >\",\n \"=========\",\n ],\n [\n \"@ $ >\",\n \"= = =\",\n ],\n];\n\n// Define a scene called \"game\". The callback will be run when we go() to the scene\n// Scenes can accept argument from go()\nscene(\"game\", ({ levelIdx, score }) => {\n // Use the level passed, or first level\n const level = addLevel(LEVELS[levelIdx || 0], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(100, 200),\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n \">\": () => [\n sprite(\"portal\"),\n area(),\n anchor(\"bot\"),\n \"portal\",\n ],\n },\n });\n\n // Get the player object from tag\n const player = level.get(\"player\")[0];\n\n // Movements\n onKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n });\n\n onKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n });\n\n onKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n });\n\n player.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n // Go to \"lose\" scene when we hit a \"danger\"\n go(\"lose\");\n });\n\n player.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n score++;\n scoreLabel.text = score;\n });\n\n // Fall death\n player.onUpdate(() => {\n if (player.pos.y >= 480) {\n go(\"lose\");\n }\n });\n\n // Enter the next level on portal\n player.onCollide(\"portal\", () => {\n play(\"portal\");\n if (levelIdx < LEVELS.length - 1) {\n // If there's a next level, go() to the same scene but load the next level\n go(\"game\", {\n levelIdx: levelIdx + 1,\n score: score,\n });\n }\n else {\n // Otherwise we have reached the end of game, go to \"win\" scene!\n go(\"win\", { score: score });\n }\n });\n\n // Score counter text\n const scoreLabel = add([\n text(score),\n pos(12),\n ]);\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n pos(12),\n ]);\n\n // Press any key to go back\n onKeyPress(start);\n});\n\nscene(\"win\", ({ score }) => {\n add([\n text(`You grabbed ${score} coins!!!`, {\n width: width(),\n }),\n pos(12),\n ]);\n\n onKeyPress(start);\n});\n\nfunction start() {\n // Start with the \"game\" scene, with initial parameters\n go(\"game\", {\n levelIdx: 0,\n score: 0,\n });\n}\n\nstart();\n", - "index": "66" - }, - { - "name": "shader", - "code": "// @ts-check\n\n// Custom shader\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Load a shader with custom fragment shader code\n// The fragment shader should define a function \"frag\", which returns a color and receives the vertex position, texture coodinate, vertex color, and texture as arguments\n// There's also the def_frag() function which returns the default fragment color\nloadShader(\n \"invert\",\n null,\n `\nuniform float u_time;\n\nvec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {\n\tvec4 c = def_frag();\n\tfloat t = (sin(u_time * 4.0) + 1.0) / 2.0;\n\treturn mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t);\n}\n`,\n);\n\nadd([\n sprite(\"bean\"),\n pos(80, 40),\n scale(8),\n // Use the shader with shader() component and pass uniforms\n shader(\"invert\", () => ({\n \"u_time\": time(),\n })),\n]);\n", - "index": "67" - }, - { - "name": "shooter", - "code": "// @ts-check\n\nkaplay({\n background: [74, 48, 82],\n});\n\nconst objs = [\n \"apple\",\n \"lightening\",\n \"coin\",\n \"egg\",\n \"key\",\n \"door\",\n \"meat\",\n \"mushroom\",\n];\n\nfor (const obj of objs) {\n loadSprite(obj, `/sprites/${obj}.png`);\n}\n\nloadBean();\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"shoot\", \"/examples/sounds/shoot.mp3\");\nloadSound(\"explode\", \"/examples/sounds/explode.mp3\");\nloadSound(\"OtherworldlyFoe\", \"/examples/sounds/OtherworldlyFoe.mp3\");\n\nscene(\"battle\", () => {\n const BULLET_SPEED = 1200;\n const TRASH_SPEED = 120;\n const BOSS_SPEED = 48;\n const PLAYER_SPEED = 480;\n const STAR_SPEED = 120;\n const BOSS_HEALTH = 1000;\n const OBJ_HEALTH = 4;\n\n const bossName = choose(objs);\n\n let insaneMode = false;\n\n const music = play(\"OtherworldlyFoe\");\n\n volume(0.5);\n\n function grow(rate) {\n return {\n update() {\n const n = rate * dt();\n this.scale.x += n;\n this.scale.y += n;\n },\n };\n }\n\n function late(t) {\n let timer = 0;\n return {\n add() {\n this.hidden = true;\n },\n update() {\n timer += dt();\n if (timer >= t) {\n this.hidden = false;\n }\n },\n };\n }\n\n add([\n text(\"KILL\", { size: 160 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(1),\n fixed(),\n ]);\n\n add([\n text(\"THE\", { size: 80 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(2),\n late(1),\n fixed(),\n ]);\n\n add([\n text(bossName.toUpperCase(), { size: 120 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(4),\n late(2),\n fixed(),\n ]);\n\n const sky = add([\n rect(width(), height()),\n color(0, 0, 0),\n opacity(0),\n ]);\n\n sky.onUpdate(() => {\n if (insaneMode) {\n const t = time() * 10;\n sky.color.r = wave(127, 255, t);\n sky.color.g = wave(127, 255, t + 1);\n sky.color.b = wave(127, 255, t + 2);\n sky.opacity = 1;\n }\n else {\n sky.color = rgb(0, 0, 0);\n sky.opacity = 0;\n }\n });\n\n // \tadd([\n // \t\tsprite(\"stars\"),\n // \t\tscale(width() / 240, height() / 240),\n // \t\tpos(0, 0),\n // \t\t\"stars\",\n // \t])\n\n // \tadd([\n // \t\tsprite(\"stars\"),\n // \t\tscale(width() / 240, height() / 240),\n // \t\tpos(0, -height()),\n // \t\t\"stars\",\n // \t])\n\n // \tonUpdate(\"stars\", (r) => {\n // \t\tr.move(0, STAR_SPEED * (insaneMode ? 10 : 1))\n // \t\tif (r.pos.y >= height()) {\n // \t\t\tr.pos.y -= height() * 2\n // \t\t}\n // \t})\n\n const player = add([\n sprite(\"bean\"),\n area(),\n pos(width() / 2, height() - 64),\n anchor(\"center\"),\n ]);\n\n onKeyDown(\"left\", () => {\n player.move(-PLAYER_SPEED, 0);\n if (player.pos.x < 0) {\n player.pos.x = width();\n }\n });\n\n onKeyDown(\"right\", () => {\n player.move(PLAYER_SPEED, 0);\n if (player.pos.x > width()) {\n player.pos.x = 0;\n }\n });\n\n onKeyPress(\"up\", () => {\n insaneMode = true;\n music.speed = 2;\n });\n\n onKeyRelease(\"up\", () => {\n insaneMode = false;\n music.speed = 1;\n });\n\n player.onCollide(\"enemy\", (e) => {\n destroy(e);\n destroy(player);\n shake(120);\n play(\"explode\");\n music.detune = -1200;\n addExplode(center(), 12, 120, 30);\n wait(1, () => {\n music.paused = true;\n go(\"battle\");\n });\n });\n\n function addExplode(p, n, rad, size) {\n for (let i = 0; i < n; i++) {\n wait(rand(n * 0.1), () => {\n for (let i = 0; i < 2; i++) {\n add([\n pos(p.add(rand(vec2(-rad), vec2(rad)))),\n rect(4, 4),\n scale(1 * size, 1 * size),\n opacity(),\n lifespan(0.1),\n grow(rand(48, 72) * size),\n anchor(\"center\"),\n ]);\n }\n });\n }\n }\n\n function spawnBullet(p) {\n add([\n rect(12, 48),\n area(),\n pos(p),\n anchor(\"center\"),\n color(127, 127, 255),\n outline(4),\n move(UP, BULLET_SPEED),\n offscreen({ destroy: true }),\n // strings here means a tag\n \"bullet\",\n ]);\n }\n\n onUpdate(\"bullet\", (b) => {\n if (insaneMode) {\n b.color = rand(rgb(0, 0, 0), rgb(255, 255, 255));\n }\n });\n\n onKeyPress(\"space\", () => {\n spawnBullet(player.pos.sub(16, 0));\n spawnBullet(player.pos.add(16, 0));\n play(\"shoot\", {\n volume: 0.3,\n detune: rand(-1200, 1200),\n });\n });\n\n function spawnTrash() {\n const name = choose(objs.filter(n => n != bossName));\n add([\n sprite(name),\n area(),\n pos(rand(0, width()), 0),\n health(OBJ_HEALTH),\n anchor(\"bot\"),\n \"trash\",\n \"enemy\",\n { speed: rand(TRASH_SPEED * 0.5, TRASH_SPEED * 1.5) },\n ]);\n wait(insaneMode ? 0.1 : 0.3, spawnTrash);\n }\n\n const boss = add([\n sprite(bossName),\n area(),\n pos(width() / 2, 40),\n health(BOSS_HEALTH),\n scale(3),\n anchor(\"top\"),\n \"enemy\",\n {\n dir: 1,\n },\n ]);\n\n on(\"death\", \"enemy\", (e) => {\n destroy(e);\n shake(2);\n addKaboom(e.pos);\n });\n\n on(\"hurt\", \"enemy\", (e) => {\n shake(1);\n play(\"hit\", {\n detune: rand(-1200, 1200),\n speed: rand(0.2, 2),\n });\n });\n\n const timer = add([\n text(\"0\"),\n pos(12, 32),\n fixed(),\n { time: 0 },\n ]);\n\n timer.onUpdate(() => {\n timer.time += dt();\n timer.text = timer.time.toFixed(2);\n });\n\n onCollide(\"bullet\", \"enemy\", (b, e) => {\n destroy(b);\n e.hurt(insaneMode ? 10 : 1);\n addExplode(b.pos, 1, 24, 1);\n });\n\n onUpdate(\"trash\", (t) => {\n t.move(0, t.speed * (insaneMode ? 5 : 1));\n if (t.pos.y - t.height > height()) {\n destroy(t);\n }\n });\n\n boss.onUpdate((p) => {\n boss.move(BOSS_SPEED * boss.dir * (insaneMode ? 3 : 1), 0);\n if (boss.dir === 1 && boss.pos.x >= width() - 20) {\n boss.dir = -1;\n }\n if (boss.dir === -1 && boss.pos.x <= 20) {\n boss.dir = 1;\n }\n });\n\n boss.onHurt(() => {\n healthbar.set(boss.hp());\n });\n\n boss.onDeath(() => {\n music.stop();\n go(\"win\", {\n time: timer.time,\n boss: bossName,\n });\n });\n\n const healthbar = add([\n rect(width(), 24),\n pos(0, 0),\n color(107, 201, 108),\n fixed(),\n {\n max: BOSS_HEALTH,\n set(hp) {\n this.width = width() * hp / this.max;\n this.flash = true;\n },\n },\n ]);\n\n healthbar.onUpdate(() => {\n if (healthbar.flash) {\n healthbar.color = rgb(255, 255, 255);\n healthbar.flash = false;\n }\n else {\n healthbar.color = rgb(127, 255, 127);\n }\n });\n\n add([\n text(\"UP: insane mode\", { width: width() / 2, size: 32 }),\n anchor(\"botleft\"),\n pos(24, height() - 24),\n ]);\n\n spawnTrash();\n});\n\nscene(\"win\", ({ time, boss }) => {\n const b = burp({\n loop: true,\n });\n\n loop(0.5, () => {\n b.detune = rand(-1200, 1200);\n });\n\n add([\n sprite(boss),\n color(255, 0, 0),\n anchor(\"center\"),\n scale(8),\n pos(width() / 2, height() / 2),\n ]);\n\n add([\n text(time.toFixed(2), 24),\n anchor(\"center\"),\n pos(width() / 2, height() / 2),\n ]);\n});\n\ngo(\"battle\");\n", - "index": "68" - }, - { - "name": "size", - "code": "// @ts-check\n\nkaplay({\n // without specifying \"width\" and \"height\", kaboom will size to the container (document.body by default)\n width: 200,\n height: 100,\n // \"stretch\" stretches the defined width and height to fullscreen\n // stretch: true,\n // \"letterbox\" makes stretching keeps aspect ratio (leaves black bars on empty spaces), have no effect without \"stretch\"\n letterbox: true,\n});\n\nloadBean();\n\nadd([\n sprite(\"bean\"),\n]);\n\nonClick(() => addKaboom(mousePos()));\n", - "index": "69" - }, - { - "name": "slice9", - "code": "// @ts-check\n\n// 9 slice sprite scaling\n\nkaplay();\n\n// Load a sprite that's made for 9 slice scaling\nloadSprite(\"9slice\", \"/examples/sprites/9slice.png\", {\n // Define the slice by the margins of 4 sides\n slice9: {\n left: 32,\n right: 32,\n top: 32,\n bottom: 32,\n },\n});\n\nconst g = add([\n sprite(\"9slice\"),\n]);\n\nonMouseMove(() => {\n const mpos = mousePos();\n // Scaling the image will keep the aspect ratio of the sliced frames\n g.width = mpos.x;\n g.height = mpos.y;\n});\n", - "index": "70" - }, - { - "name": "sprite", - "code": "// @ts-check\n\n// Sprite animation\n\n// Start a kaboom game\nkaplay({\n // Scale the whole game up\n scale: 4,\n // Set the default font\n font: \"monospace\",\n});\n\n// Loading a multi-frame sprite\nloadSprite(\"dino\", \"/examples/sprites/dino.png\", {\n // The image contains 9 frames layed out horizontally, slice it into individual frames\n sliceX: 9,\n // Define animations\n anims: {\n \"idle\": {\n // Starts from frame 0, ends at frame 3\n from: 0,\n to: 3,\n // Frame per second\n speed: 5,\n loop: true,\n },\n \"run\": {\n from: 4,\n to: 7,\n speed: 10,\n loop: true,\n },\n // This animation only has 1 frame\n \"jump\": 8,\n },\n});\n\nconst SPEED = 120;\nconst JUMP_FORCE = 240;\n\nsetGravity(640);\n\n// Add our player character\nconst player = add([\n sprite(\"dino\"),\n pos(center()),\n anchor(\"center\"),\n area(),\n body(),\n]);\n\n// .play is provided by sprite() component, it starts playing the specified animation (the animation information of \"idle\" is defined above in loadSprite)\nplayer.play(\"idle\");\n\n// Add a platform\nadd([\n rect(width(), 24),\n area(),\n outline(1),\n pos(0, height() - 24),\n body({ isStatic: true }),\n]);\n\n// Switch to \"idle\" or \"run\" animation when player hits ground\nplayer.onGround(() => {\n if (!isKeyDown(\"left\") && !isKeyDown(\"right\")) {\n player.play(\"idle\");\n }\n else {\n player.play(\"run\");\n }\n});\n\nplayer.onAnimEnd((anim) => {\n if (anim === \"idle\") {\n // You can also register an event that runs when certain anim ends\n }\n});\n\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n player.play(\"jump\");\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n player.flipX = true;\n // .play() will reset to the first frame of the anim, so we want to make sure it only runs when the current animation is not \"run\"\n if (player.isGrounded() && player.curAnim() !== \"run\") {\n player.play(\"run\");\n }\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n player.flipX = false;\n if (player.isGrounded() && player.curAnim() !== \"run\") {\n player.play(\"run\");\n }\n});\n[\"left\", \"right\"].forEach((key) => {\n onKeyRelease(key, () => {\n // Only reset to \"idle\" if player is not holding any of these keys\n if (player.isGrounded() && !isKeyDown(\"left\") && !isKeyDown(\"right\")) {\n player.play(\"idle\");\n }\n });\n});\n\nconst getInfo = () =>\n `\nAnim: ${player.curAnim()}\nFrame: ${player.frame}\n`.trim();\n\n// Add some text to show the current animation\nconst label = add([\n text(getInfo(), { size: 12 }),\n color(0, 0, 0),\n pos(4),\n]);\n\nlabel.onUpdate(() => {\n label.text = getInfo();\n});\n\n// Check out https://kaboomjs.com#SpriteComp for everything sprite() provides\n", - "index": "71" - }, - { - "name": "spriteatlas", - "code": "// @ts-check\n\nkaplay({\n scale: 4,\n background: [0, 0, 0],\n});\n\n// https://0x72.itch.io/dungeontileset-ii\nloadSpriteAtlas(\"/examples/sprites/dungeon.png\", {\n \"hero\": {\n \"x\": 128,\n \"y\": 196,\n \"width\": 144,\n \"height\": 28,\n \"sliceX\": 9,\n \"anims\": {\n \"idle\": {\n \"from\": 0,\n \"to\": 3,\n \"speed\": 3,\n \"loop\": true,\n },\n \"run\": {\n \"from\": 4,\n \"to\": 7,\n \"speed\": 10,\n \"loop\": true,\n },\n \"hit\": 8,\n },\n },\n \"ogre\": {\n \"x\": 16,\n \"y\": 320,\n \"width\": 256,\n \"height\": 32,\n \"sliceX\": 8,\n \"anims\": {\n \"idle\": {\n \"from\": 0,\n \"to\": 3,\n \"speed\": 3,\n \"loop\": true,\n },\n \"run\": {\n \"from\": 4,\n \"to\": 7,\n \"speed\": 10,\n \"loop\": true,\n },\n },\n },\n \"floor\": {\n \"x\": 16,\n \"y\": 64,\n \"width\": 48,\n \"height\": 48,\n \"sliceX\": 3,\n \"sliceY\": 3,\n },\n \"chest\": {\n \"x\": 304,\n \"y\": 304,\n \"width\": 48,\n \"height\": 16,\n \"sliceX\": 3,\n \"anims\": {\n \"open\": {\n \"from\": 0,\n \"to\": 2,\n \"speed\": 20,\n \"loop\": false,\n },\n \"close\": {\n \"from\": 2,\n \"to\": 0,\n \"speed\": 20,\n \"loop\": false,\n },\n },\n },\n \"sword\": {\n \"x\": 322,\n \"y\": 81,\n \"width\": 12,\n \"height\": 30,\n },\n \"wall\": {\n \"x\": 16,\n \"y\": 16,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_top\": {\n \"x\": 16,\n \"y\": 0,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_left\": {\n \"x\": 16,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_right\": {\n \"x\": 0,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_topleft\": {\n \"x\": 32,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_topright\": {\n \"x\": 48,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_botleft\": {\n \"x\": 32,\n \"y\": 144,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_botright\": {\n \"x\": 48,\n \"y\": 144,\n \"width\": 16,\n \"height\": 16,\n },\n});\n\n// Can also load from external JSON url\n// loadSpriteAtlas(\"/sprites/dungeon.png\", \"/sprites/dungeon.json\")\n\n// floor\naddLevel([\n \"xxxxxxxxxx\",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n], {\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \" \": () => [\n sprite(\"floor\", { frame: ~~rand(0, 8) }),\n ],\n },\n});\n\n// objects\nconst map = addLevel([\n \"tttttttttt\",\n \"cwwwwwwwwd\",\n \"l r\",\n \"l r\",\n \"l r\",\n \"l $ r\",\n \"l r\",\n \"l $ r\",\n \"attttttttb\",\n \"wwwwwwwwww\",\n], {\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \"$\": () => [\n sprite(\"chest\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n { opened: false },\n \"chest\",\n ],\n \"a\": () => [\n sprite(\"wall_botleft\"),\n area({ shape: new Rect(vec2(0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"b\": () => [\n sprite(\"wall_botright\"),\n area({ shape: new Rect(vec2(12, 0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"c\": () => [\n sprite(\"wall_topleft\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"d\": () => [\n sprite(\"wall_topright\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"w\": () => [\n sprite(\"wall\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"t\": () => [\n sprite(\"wall_top\"),\n area({ shape: new Rect(vec2(0, 12), 16, 4) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"l\": () => [\n sprite(\"wall_left\"),\n area({ shape: new Rect(vec2(0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"r\": () => [\n sprite(\"wall_right\"),\n area({ shape: new Rect(vec2(12, 0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n },\n});\n\nconst player = map.spawn(\n [\n sprite(\"hero\", { anim: \"idle\" }),\n area({ shape: new Rect(vec2(0, 6), 12, 12) }),\n body(),\n anchor(\"center\"),\n tile(),\n ],\n 2,\n 2,\n);\n\nconst sword = player.add([\n pos(-4, 9),\n sprite(\"sword\"),\n anchor(\"bot\"),\n rotate(0),\n spin(),\n]);\n\n// TODO: z\nmap.spawn(\n [\n sprite(\"ogre\"),\n anchor(\"bot\"),\n area({ scale: 0.5 }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n 5,\n 4,\n);\n\nfunction spin() {\n let spinning = false;\n return {\n id: \"spin\",\n update() {\n if (spinning) {\n this.angle += 1200 * dt();\n if (this.angle >= 360) {\n this.angle = 0;\n spinning = false;\n }\n }\n },\n spin() {\n spinning = true;\n },\n };\n}\n\nfunction interact() {\n let interacted = false;\n for (const col of player.getCollisions()) {\n const c = col.target;\n if (c.is(\"chest\")) {\n if (c.opened) {\n c.play(\"close\");\n c.opened = false;\n }\n else {\n c.play(\"open\");\n c.opened = true;\n }\n interacted = true;\n }\n }\n if (!interacted) {\n sword.spin();\n }\n}\n\nonKeyPress(\"space\", interact);\n\nconst SPEED = 120;\n\nconst dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n};\n\nplayer.onUpdate(() => {\n camPos(player.pos);\n});\n\nplayer.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.pos);\n});\n\nonKeyDown(\"right\", () => {\n player.flipX = false;\n sword.flipX = false;\n player.move(SPEED, 0);\n sword.pos = vec2(-4, 9);\n});\n\nonKeyDown(\"left\", () => {\n player.flipX = true;\n sword.flipX = true;\n player.move(-SPEED, 0);\n sword.pos = vec2(4, 9);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\nonGamepadButtonPress(\"south\", interact);\n\nonGamepadStick(\"left\", (v) => {\n if (v.x < 0) {\n player.flipX = true;\n sword.flipX = true;\n sword.pos = vec2(4, 9);\n }\n else if (v.x > 0) {\n player.flipX = false;\n sword.flipX = false;\n sword.pos = vec2(-4, 9);\n }\n player.move(v.scale(SPEED));\n if (v.isZero()) {\n if (player.curAnim() !== \"idle\") player.play(\"idle\");\n }\n else {\n if (player.curAnim() !== \"run\") player.play(\"run\");\n }\n});\n[\"left\", \"right\", \"up\", \"down\"].forEach((key) => {\n onKeyPress(key, () => {\n player.play(\"run\");\n });\n onKeyRelease(key, () => {\n if (\n !isKeyDown(\"left\")\n && !isKeyDown(\"right\")\n && !isKeyDown(\"up\")\n && !isKeyDown(\"down\")\n ) {\n player.play(\"idle\");\n }\n });\n});\n", - "index": "72" - }, - { - "name": "text", - "code": "// @ts-check\n\nkaplay({\n background: [212, 110, 179],\n});\n\n// Load a custom font from a .ttf file\nloadFont(\"FlowerSketches\", \"/examples/fonts/FlowerSketches.ttf\");\n\n// Load a custom font with options\nloadFont(\"apl386\", \"/examples/fonts/apl386.ttf\", {\n outline: 4,\n filter: \"linear\",\n});\n\n// Load custom bitmap font, specifying the width and height of each character in the image\nloadBitmapFont(\"unscii\", \"/examples/fonts/unscii_8x8.png\", 8, 8);\nloadBitmapFont(\"4x4\", \"/examples/fonts/4x4.png\", 4, 4);\n\n// List of built-in fonts (\"o\" at the end means the outlined version)\nconst builtinFonts = [\n \"monospace\",\n];\n\n// Make a list of fonts that we cycle through\nconst fonts = [\n ...builtinFonts,\n \"4x4\",\n \"unscii\",\n \"FlowerSketches\",\n \"apl386\",\n \"Sans-Serif\",\n];\n\n// Keep track which is the current font\nlet curFont = 0;\nlet curSize = 48;\nconst pad = 24;\n\n// Add a game object with text() component + options\nconst input = add([\n pos(pad),\n // Render text with the text() component\n text(\"Type! And try arrow keys!\", {\n // What font to use\n font: fonts[curFont],\n // It'll wrap to next line if the text width exceeds the width option specified here\n width: width() - pad * 2,\n // The height of character\n size: curSize,\n // Text alignment (\"left\", \"center\", \"right\", default \"left\")\n align: \"left\",\n lineSpacing: 8,\n letterSpacing: 4,\n // Transform each character for special effects\n transform: (idx, ch) => ({\n color: hsl2rgb((time() * 0.2 + idx * 0.1) % 1, 0.7, 0.8),\n pos: vec2(0, wave(-4, 4, time() * 4 + idx * 0.5)),\n scale: wave(1, 1.2, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n }),\n }),\n]);\n\n// Like onKeyPressRepeat() but more suitable for text input.\nonCharInput((ch) => {\n input.text += ch;\n});\n\n// Like onKeyPress() but will retrigger when key is being held (which is similar to text input behavior)\n// Insert new line when user presses enter\nonKeyPressRepeat(\"enter\", () => {\n input.text += \"\\n\";\n});\n\n// Delete last character\nonKeyPressRepeat(\"backspace\", () => {\n input.text = input.text.substring(0, input.text.length - 1);\n});\n\n// Go to previous font\nonKeyPress(\"left\", () => {\n if (--curFont < 0) curFont = fonts.length - 1;\n input.font = fonts[curFont];\n});\n\n// Go to next font\nonKeyPress(\"right\", () => {\n curFont = (curFont + 1) % fonts.length;\n input.font = fonts[curFont];\n});\n\nconst SIZE_SPEED = 32;\nconst SIZE_MIN = 12;\nconst SIZE_MAX = 120;\n\n// Increase text size\nonKeyDown(\"up\", () => {\n curSize = Math.min(curSize + dt() * SIZE_SPEED, SIZE_MAX);\n input.textSize = curSize;\n});\n\n// Decrease text size\nonKeyDown(\"down\", () => {\n curSize = Math.max(curSize - dt() * SIZE_SPEED, SIZE_MIN);\n input.textSize = curSize;\n});\n\n// Use this syntax and style option to style chunks of text, with CharTransformFunc.\nadd([\n text(\n \"[green]oh hi[/green] here's some [wavy][rainbow]styled[/rainbow][/wavy] text\",\n {\n width: width(),\n styles: {\n \"green\": {\n color: rgb(128, 128, 255),\n },\n \"wavy\": (idx, ch) => ({\n pos: vec2(0, wave(-4, 4, time() * 6 + idx * 0.5)),\n }),\n \"rainbow\": (idx, ch) => ({\n color: hsl2rgb((time() * 0.2 + idx * 0.1) % 1, 0.7, 0.8),\n }),\n },\n },\n ),\n pos(pad, height() - pad),\n anchor(\"botleft\"),\n // scale(0.5),\n]);\n", - "index": "73" - }, - { - "name": "textInput", - "code": "// @ts-check\n\n// Start kaboom\nkaplay();\n\nsetBackground(BLACK);\n\n// Add the game object that asks a question\nadd([\n anchor(\"top\"),\n pos(width() / 2, 0),\n text(\"Whats your favorite food :D\"),\n]);\n\n// Add the node that you write in\nconst food = add([\n text(\"\"),\n textInput(true, 10), // make it have focus and only be 20 chars max\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n]);\n\n// add the response\nadd([\n text(\"\"),\n anchor(\"bot\"),\n pos(width() / 2, height()),\n {\n update() {\n this.text =\n `wow i didnt know you love ${food.text} so much, but i like it too :D`;\n },\n },\n]);\n", - "index": "74" - }, - { - "name": "tiled", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nadd([\n pos(150, 150),\n sprite(\"bean\", {\n tiled: true,\n width: 200,\n height: 200,\n }),\n anchor(\"center\"),\n]);\n\n// debug.inspect = true\n", - "index": "75" - }, - { - "name": "timer", - "code": "// @ts-check\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Execute something after every 0.5 seconds.\nloop(0.5, () => {\n const bean = add([\n sprite(\"bean\"),\n pos(rand(vec2(0), vec2(width(), height()))),\n ]);\n\n // Execute something after 3 seconds.\n wait(3, () => {\n destroy(bean);\n });\n});\n", - "index": "76" - }, - { - "name": "transformShape", - "code": "// @ts-check\n\nkaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(vec2(), this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(vec2(), this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(), this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(), this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(190, 40),\n rotate(45),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts);\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts);\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n rotate(45),\n rect(30, 60),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(0, 0), 30, 60);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(vec2(), 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(vec2(), 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nfunction getGlobalShape(s) {\n const t = s.transform;\n return s.getShape().transform(t);\n}\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && getGlobalShape(s1).collides(getGlobalShape(s2))\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonDraw(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s => {\n const shape = getGlobalShape(s);\n // console.log(tshape)\n switch (shape.constructor.name) {\n case \"Ellipse\":\n pushTransform();\n pushTranslate(shape.center);\n pushRotate(shape.angle);\n drawEllipse({\n pos: vec2(),\n radiusX: shape.radiusX,\n radiusY: shape.radiusY,\n fill: false,\n outline: {\n width: 4,\n color: rgb(255, 255, 0),\n },\n });\n popTransform();\n break;\n case \"Polygon\":\n drawPolygon({\n pts: shape.pts,\n fill: false,\n outline: {\n width: 4,\n color: rgb(255, 255, 0),\n },\n });\n break;\n }\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) =>\n getGlobalShape(shape).contains(pos)\n );\n selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n", - "index": "77" - }, - { - "name": "tween", - "code": "// @ts-check\n\n// Tweeeeeening!\n\nkaplay({\n background: [141, 183, 255],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nconst duration = 1;\nconst easeTypes = Object.keys(easings);\nlet curEaseType = 0;\n\nconst bean = add([\n sprite(\"bean\"),\n scale(2),\n pos(center()),\n rotate(0),\n anchor(\"center\"),\n]);\n\nconst label = add([\n text(easeTypes[curEaseType], { size: 64 }),\n pos(24, 24),\n]);\n\nadd([\n text(\"Click anywhere & use arrow keys\", { width: width() }),\n anchor(\"botleft\"),\n pos(24, height() - 24),\n]);\n\nonKeyPress([\"left\", \"a\"], () => {\n curEaseType = curEaseType === 0 ? easeTypes.length - 1 : curEaseType - 1;\n label.text = easeTypes[curEaseType];\n});\n\nonKeyPress([\"right\", \"d\"], () => {\n curEaseType = (curEaseType + 1) % easeTypes.length;\n label.text = easeTypes[curEaseType];\n});\n\nlet curTween = null;\n\nonMousePress(() => {\n const easeType = easeTypes[curEaseType];\n // stop previous lerp, or there will be jittering\n if (curTween) curTween.cancel();\n // start the tween\n curTween = tween(\n // start value (accepts number, Vec2 and Color)\n bean.pos,\n // destination value\n mousePos(),\n // duration (in seconds)\n duration,\n // how value should be updated\n (val) => bean.pos = val,\n // interpolation function (defaults to easings.linear)\n easings[easeType],\n );\n});\n", - "index": "78" - } -] + { + "name": "add", + "code": "// Adding game objects to screen\n\n// Start a KAPLAY game\nkaplay();\n\n// Load a sprite asset from \"sprites/bean.png\", with the name \"bean\"\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// A \"Game Object\" is the basic unit of entity in KAPLAY\n// Game objects are composed from components\n// Each component gives a game object certain capabilities\n\n// add() assembles a game object from a list of components and add to game, returns the reference of the game object\nconst player = add([\n sprite(\"bean\"), // sprite() component makes it render as a sprite\n pos(120, 80), // pos() component gives it position, also enables movement\n rotate(0), // rotate() component gives it rotation\n anchor(\"center\"), // anchor() component defines the pivot point (defaults to \"topleft\")\n]);\n\n// .onUpdate() is a method on all game objects, it registers an event that runs every frame\nplayer.onUpdate(() => {\n // .angle is a property provided by rotate() component, here we're incrementing the angle by 120 degrees per second, dt() is the time elapsed since last frame in seconds\n player.angle += 120 * dt();\n});\n\n// Add multiple game objects\nfor (let i = 0; i < 3; i++) {\n // generate a random point on screen\n // width() and height() gives the game dimension\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n sprite(\"ghosty\"),\n pos(x, y),\n ]);\n}\n", + "index": "0" + }, + { + "name": "ai", + "code": "// Use state() component to handle basic AI\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst SPEED = 320;\nconst ENEMY_SPEED = 160;\nconst BULLET_SPEED = 800;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 80),\n area(),\n anchor(\"center\"),\n]);\n\nconst enemy = add([\n sprite(\"ghosty\"),\n pos(width() - 80, height() - 80),\n anchor(\"center\"),\n // This enemy cycle between 3 states, and start from \"idle\" state\n state(\"move\", [\"idle\", \"attack\", \"move\"]),\n]);\n\n// Run the callback once every time we enter \"idle\" state.\n// Here we stay \"idle\" for 0.5 second, then enter \"attack\" state.\nenemy.onStateEnter(\"idle\", async () => {\n await wait(0.5);\n enemy.enterState(\"attack\");\n});\n\n// When we enter \"attack\" state, we fire a bullet, and enter \"move\" state after 1 sec\nenemy.onStateEnter(\"attack\", async () => {\n // Don't do anything if player doesn't exist anymore\n if (player.exists()) {\n const dir = player.pos.sub(enemy.pos).unit();\n\n add([\n pos(enemy.pos),\n move(dir, BULLET_SPEED),\n rect(12, 12),\n area(),\n offscreen({ destroy: true }),\n anchor(\"center\"),\n color(BLUE),\n \"bullet\",\n ]);\n }\n\n await wait(1);\n enemy.enterState(\"move\");\n});\n\nenemy.onStateEnter(\"move\", async () => {\n await wait(2);\n enemy.enterState(\"idle\");\n});\n\n// Like .onUpdate() which runs every frame, but only runs when the current state is \"move\"\n// Here we move towards the player every frame if the current state is \"move\"\nenemy.onStateUpdate(\"move\", () => {\n if (!player.exists()) return;\n const dir = player.pos.sub(enemy.pos).unit();\n enemy.move(dir.scale(ENEMY_SPEED));\n});\n\n// Taking a bullet makes us disappear\nplayer.onCollide(\"bullet\", (bullet) => {\n destroy(bullet);\n destroy(player);\n addKaboom(bullet.pos);\n});\n\n// Register input handlers & movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n", + "index": "1" + }, + { + "name": "animation", + "code": "// Start kaboom\nkaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\n// Rotating\nconst rotatingBean = add([\n sprite(\"bean\"),\n pos(50, 50),\n anchor(\"center\"),\n rotate(0),\n animate(),\n]);\n\n// Trying sprite change\nrotatingBean.sprite = \"bag\";\n\nrotatingBean.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"forward\",\n});\n\n// Moving right to left using ping-pong\nconst movingBean = add([\n sprite(\"bean\"),\n pos(50, 150),\n anchor(\"center\"),\n animate(),\n]);\n\nmovingBean.animate(\"pos\", [vec2(50, 150), vec2(150, 150)], {\n duration: 2,\n direction: \"ping-pong\",\n});\n\n// Same animation as before, but relative to the spawn position\nconst secondMovingBean = add([\n sprite(\"bean\"),\n pos(150, 0),\n anchor(\"center\"),\n animate({ relative: true }),\n]);\n\nsecondMovingBean.animate(\"pos\", [vec2(50, 150), vec2(150, 150)], {\n duration: 2,\n direction: \"ping-pong\",\n});\n\n// Changing color using a color list\nconst coloringBean = add([\n sprite(\"bean\"),\n pos(50, 300),\n anchor(\"center\"),\n color(WHITE),\n animate(),\n]);\n\ncoloringBean.animate(\"color\", [WHITE, RED, GREEN, BLUE, WHITE], {\n duration: 8,\n});\n\n// Changing opacity using an opacity list\nconst opacitingBean = add([\n sprite(\"bean\"),\n pos(150, 300),\n anchor(\"center\"),\n opacity(1),\n animate(),\n]);\n\nopacitingBean.animate(\"opacity\", [1, 0, 1], {\n duration: 8,\n easing: easings.easeInOutCubic,\n});\n\n// Moving in a square like motion\nconst squaringBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate(),\n]);\n\nsquaringBean.animate(\n \"pos\",\n [\n vec2(50, 400),\n vec2(150, 400),\n vec2(150, 500),\n vec2(50, 500),\n vec2(50, 400),\n ],\n { duration: 8 },\n);\n\n// Moving in a square like motion, but with custom spaced keyframes\nconst timedSquaringBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate(),\n]);\n\ntimedSquaringBean.animate(\n \"pos\",\n [\n vec2(50, 400),\n vec2(150, 400),\n vec2(150, 500),\n vec2(50, 500),\n vec2(50, 400),\n ],\n {\n duration: 8,\n timing: [\n 0,\n 0.1,\n 0.3,\n 0.7,\n 1.0,\n ],\n },\n);\n\n// Using spline interpolation to move according to a smoothened path\nconst curvingBean = add([\n sprite(\"bean\"),\n pos(50, 400),\n anchor(\"center\"),\n animate({ followMotion: true }),\n rotate(0),\n]);\n\ncurvingBean.animate(\n \"pos\",\n [\n vec2(200, 400),\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500),\n vec2(400, 400),\n ],\n { duration: 8, direction: \"ping-pong\", interpolation: \"spline\" },\n);\n\nconst littleBeanPivot = curvingBean.add([\n animate(),\n rotate(0),\n named(\"littlebeanpivot\"),\n]);\n\nlittleBeanPivot.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"reverse\",\n});\n\nconst littleBean = littleBeanPivot.add([\n sprite(\"bean\"),\n pos(50, 50),\n anchor(\"center\"),\n scale(0.25),\n animate(),\n rotate(0),\n named(\"littlebean\"),\n]);\n\nlittleBean.animate(\"angle\", [0, 360], {\n duration: 2,\n direction: \"forward\",\n});\n\nconsole.log(JSON.stringify(serializeAnimation(curvingBean, \"root\"), \"\", 2));\n\n/*onDraw(() => {\n drawCurve(t => evaluateCatmullRom(\n vec2(200, 400),\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500), t), { color: RED })\n drawCurve(catmullRom(\n vec2(200, 400),\n vec2(250, 500),\n vec2(300, 400),\n vec2(350, 500)), { color: GREEN })\n})*/\n", + "index": "2" + }, + { + "name": "audio", + "code": "// audio playback & control\n\nkaplay({\n // Don't pause audio when tab is not active\n backgroundAudio: true,\n background: [0, 0, 0],\n});\n\nloadSound(\"bell\", \"/examples/sounds/bell.mp3\");\nloadSound(\"OtherworldlyFoe\", \"/examples/sounds/OtherworldlyFoe.mp3\");\n\n// play() to play audio\n// (This might not play until user input due to browser policy)\nconst music = play(\"OtherworldlyFoe\", {\n loop: true,\n paused: true,\n});\n\n// Adjust global volume\nvolume(0.5);\n\nconst label = add([\n text(),\n]);\n\nfunction updateText() {\n label.text = `\n${music.paused ? \"Paused\" : \"Playing\"}\nTime: ${music.time().toFixed(2)}\nVolume: ${music.volume.toFixed(2)}\nSpeed: ${music.speed.toFixed(2)}\n\n[space] play/pause\n[up/down] volume\n[left/right] speed\n\t`.trim();\n}\n\nupdateText();\n\n// Update text every frame\nonUpdate(updateText);\n\n// Adjust music properties through input\nonKeyPress(\"space\", () => music.paused = !music.paused);\nonKeyPressRepeat(\"up\", () => music.volume += 0.1);\nonKeyPressRepeat(\"down\", () => music.volume -= 0.1);\nonKeyPressRepeat(\"left\", () => music.speed -= 0.1);\nonKeyPressRepeat(\"right\", () => music.speed += 0.1);\nonKeyPress(\"m\", () => music.seek(4.24));\n\nconst keyboard = \"awsedftgyhujk\";\n\n// Simple piano with \"bell\" sound and the second row of a QWERTY keyboard\nfor (let i = 0; i < keyboard.length; i++) {\n onKeyPress(keyboard[i], () => {\n play(\"bell\", {\n // The original \"bell\" sound is F, -500 will make it C for the first key\n detune: i * 100 - 500,\n });\n });\n}\n\n// Draw music progress bar\nonDraw(() => {\n if (!music.duration()) return;\n const h = 16;\n drawRect({\n pos: vec2(0, height() - h),\n width: music.time() / music.duration() * width(),\n height: h,\n });\n});\n", + "index": "3" + }, + { + "name": "bench", + "code": "// bench marking sprite rendering performance\n\nkaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\nfor (let i = 0; i < 5000; i++) {\n add([\n sprite(i % 2 === 0 ? \"bean\" : \"bag\"),\n pos(rand(0, width()), rand(0, height())),\n anchor(\"center\"),\n ]);\n}\n\nonDraw(() => {\n drawText({\n text: debug.fps(),\n pos: vec2(width() / 2, height() / 2),\n anchor: \"center\",\n color: rgb(255, 127, 255),\n });\n});\n", + "index": "4" + }, + { + "name": "binding", + "code": "kaplay({\n buttons: {\n \"jump\": {\n gamepad: [\"south\"],\n keyboard: [\"up\", \"w\"],\n mouse: \"left\",\n },\n \"inspect\": {\n gamepad: \"east\",\n keyboard: \"f\",\n mouse: \"right\",\n },\n },\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Set the gravity acceleration (pixels per second)\nsetGravity(1600);\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n area(),\n // body() component gives the ability to respond to gravity\n body(),\n]);\n\n// Add a platform to hold the player\nadd([\n rect(width(), 48),\n outline(4),\n area(),\n pos(0, height() - 48),\n // Give objects a body() component if you don't want other solid objects pass through\n body({ isStatic: true }),\n]);\n\nadd([\n text(\"Press jump button\", { width: width() / 2 }),\n pos(12, 12),\n]);\n\nonButtonPress(\"jump\", () => {\n debug.log(getLastInputDeviceType());\n\n if (player.isGrounded()) {\n // .jump() is provided by body()\n player.jump();\n }\n});\n\nonButtonDown(\"inspect\", () => {\n debug.log(\"inspecting\");\n});\n", + "index": "5" + }, + { + "name": "burp", + "code": "// Start the game in burp mode\nkaplay({\n burp: true,\n});\n\n// \"b\" triggers a burp in burp mode\nadd([\n text(\"press b\"),\n]);\n\n// burp() on click / tap for our friends on mobile\nonClick(burp);\n", + "index": "6" + }, + { + "name": "button", + "code": "// Simple Button UI\n\nkaplay({\n background: [135, 62, 132],\n});\n\n// reset cursor to default on frame start for easier cursor management\nonUpdate(() => setCursor(\"default\"));\n\nfunction addButton(txt, p, f) {\n // add a parent background object\n const btn = add([\n rect(240, 80, { radius: 8 }),\n pos(p),\n area(),\n scale(1),\n anchor(\"center\"),\n outline(4),\n ]);\n\n // add a child object that displays the text\n btn.add([\n text(txt),\n anchor(\"center\"),\n color(0, 0, 0),\n ]);\n\n // onHoverUpdate() comes from area() component\n // it runs every frame when the object is being hovered\n btn.onHoverUpdate(() => {\n const t = time() * 10;\n btn.color = hsl2rgb((t / 10) % 1, 0.6, 0.7);\n btn.scale = vec2(1.2);\n setCursor(\"pointer\");\n });\n\n // onHoverEnd() comes from area() component\n // it runs once when the object stopped being hovered\n btn.onHoverEnd(() => {\n btn.scale = vec2(1);\n btn.color = rgb();\n });\n\n // onClick() comes from area() component\n // it runs once when the object is clicked\n btn.onClick(f);\n\n return btn;\n}\n\naddButton(\"Start\", vec2(200, 100), () => debug.log(\"oh hi\"));\naddButton(\"Quit\", vec2(200, 200), () => debug.log(\"bye\"));\n", + "index": "7" + }, + { + "name": "camera", + "code": "// Adjust camera / viewport\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst SPEED = 480;\n\nsetGravity(2400);\n\n// Setup a basic level\nconst level = addLevel([\n \"@ = $\",\n \"=======\",\n], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(100, 200),\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\nplayer.onUpdate(() => {\n // Set the viewport center to player.pos\n camPos(player.worldPos());\n});\n\nplayer.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.worldPos());\n});\n\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n score++;\n // Zoooom in!\n camScale(2);\n});\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => player.move(-SPEED, 0));\nonKeyDown(\"right\", () => player.move(SPEED, 0));\n\nlet score = 0;\n\n// Add a ui layer with fixed() component to make the object not affected by camera\nconst ui = add([\n fixed(),\n]);\n\n// Add a score counter\nui.add([\n text(\"0\"),\n pos(12),\n {\n update() {\n this.text = score;\n },\n },\n]);\n\nonClick(() => {\n // Use toWorld() to transform a screen-space coordinate (like mousePos()) to the world-space coordinate, which has the camera transform applied\n addKaboom(toWorld(mousePos()));\n});\n", + "index": "8" + }, + { + "name": "children", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst nucleus = add([\n sprite(\"ghosty\"),\n pos(center()),\n anchor(\"center\"),\n]);\n\n// Add children\nfor (let i = 12; i < 24; i++) {\n nucleus.add([\n sprite(\"bean\"),\n rotate(0),\n anchor(vec2(i).scale(0.25)),\n {\n speed: i * 8,\n },\n ]);\n}\n\nnucleus.onUpdate(() => {\n nucleus.pos = mousePos();\n\n // update children\n nucleus.children.forEach((child) => {\n child.angle += child.speed * dt();\n });\n});\n", + "index": "9" + }, + { + "name": "collision", + "code": "// Collision handling\n\n// Start kaboom\nkaplay({\n scale: 2,\n});\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\n\n// Define player movement speed\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 40),\n color(),\n rotate(0),\n // area() component gives the object a collider, which enables collision checking\n area(),\n // area({ shape: new Polygon([vec2(0), vec2(100), vec2(-100, 100)]) }),\n // area({ shape: new Rect(vec2(0), 12, 120) }),\n // area({ scale: 0.5 }),\n // body() component makes an object respond to physics\n body(),\n]);\n\n// Register input handlers & movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\nonKeyDown(\"q\", () => {\n player.angle -= SPEED * dt();\n});\n\nonKeyDown(\"e\", () => {\n player.angle += SPEED * dt();\n});\n\n// Add enemies\nfor (let i = 0; i < 3; i++) {\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n sprite(\"ghosty\"),\n pos(x, y),\n // Both objects must have area() component to enable collision detection between\n area(),\n \"enemy\",\n ]);\n}\n\nadd([\n sprite(\"grass\"),\n pos(center()),\n area(),\n // This game object also has isStatic, so our player won't be able to move pass this\n body({ isStatic: true }),\n \"grass\",\n]);\n\nadd([\n sprite(\"steel\"),\n pos(100, 200),\n area(),\n // This will not be static, but have a big mass that's hard to push over\n body({ mass: 10 }),\n]);\n\n// .onCollide() is provided by area() component, it registers an event that runs when an objects collides with another object with certain tag\n// In this case we destroy (remove from game) the enemy when player hits one\nplayer.onCollide(\"enemy\", (enemy) => {\n destroy(enemy);\n});\n\n// .onCollideUpdate() runs every frame when an object collides with another object\nplayer.onCollideUpdate(\"enemy\", () => {\n});\n\n// .onCollideEnd() runs once when an object stopped colliding with another object\nplayer.onCollideEnd(\"grass\", (a) => {\n debug.log(\"leave grass\");\n});\n\n// .clicks() is provided by area() component, it registers an event that runs when the object is clicked\nplayer.onClick(() => {\n debug.log(\"what up\");\n});\n\nplayer.onUpdate(() => {\n // .isHovering() is provided by area() component, which returns a boolean of if the object is currently being hovered on\n if (player.isHovering()) {\n player.color = rgb(0, 0, 255);\n }\n else {\n player.color = rgb();\n }\n});\n\n// Enter inspect mode, which shows the collider outline of each object with area() component, handy for debugging\n// Can also be toggled by pressing F1\ndebug.inspect = true;\n\n// Check out https://kaboomjs.com#AreaComp for everything area() provides\n", + "index": "10" + }, + { + "name": "component", + "code": "// Custom component\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Components are just functions that returns an object that follows a certain format\nfunction funky() {\n // Can use local closed variables to store component state\n let isFunky = false;\n\n return {\n // ------------------\n // Special properties that controls the behavior of the component (all optional)\n\n // The name of the component\n id: \"funky\",\n // If this component depend on any other components\n require: [\"scale\", \"color\"],\n\n // Runs when the host object is added to the game\n add() {\n // E.g. Register some events from other components, do some bookkeeping, etc.\n },\n\n // Runs every frame as long as the host object exists\n update() {\n if (!isFunky) return;\n\n // \"this\" in all component methods refers to the host game object\n // Here we're updating some properties provided by other components\n this.color = rgb(rand(0, 255), rand(0, 255), rand(0, 255));\n this.scale = vec2(rand(1, 2));\n },\n\n // Runs every frame (after update) as long as the host object exists\n draw() {\n // E.g. Custom drawXXX() operations.\n },\n\n // Runs when the host object is destroyed\n destroy() {\n // E.g. Clean up event handlers, etc.\n },\n\n // Get the info to present in inspect mode\n inspect() {\n return isFunky ? \"on\" : \"off\";\n },\n\n // ------------------\n // All other properties and methods are directly assigned to the host object\n\n getFunky() {\n isFunky = true;\n },\n };\n}\n\nconst bean = add([\n sprite(\"bean\"),\n pos(center()),\n anchor(\"center\"),\n scale(1),\n color(),\n area(),\n // Use our component here\n funky(),\n // Tags are empty components, it's equivalent to a { id: \"friend\" }\n \"friend\",\n // Plain objects here are components too and work the same way, except unnamed\n {\n coolness: 100,\n friends: [],\n },\n]);\n\nonKeyPress(\"space\", () => {\n // .coolness is from our unnamed component above\n if (bean.coolness >= 100) {\n // We can use .getFunky() provided by the funky() component now\n bean.getFunky();\n }\n});\n\nonKeyPress(\"r\", () => {\n // .use() is on every game object, it adds a component at runtime\n bean.use(rotate(rand(0, 360)));\n});\n\nonKeyPress(\"escape\", () => {\n // .unuse() removes a component from the game object\n bean.unuse(\"funky\");\n});\n\nadd([\n text(\"Press space to get funky\", { width: width() }),\n pos(12, 12),\n]);\n", + "index": "11" + }, + { + "name": "concert", + "code": "// bean is holding a concert to celebrate kaboom2000!\n\nkaplay({\n scale: 0.7,\n background: [128, 180, 255],\n font: \"happy\",\n});\n\nloadSprite(\"bag\", `/sprites/bag.png`);\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"bobo\", `/sprites/bobo.png`);\nloadSprite(\"gigagantrum\", \"/sprites/gigagantrum.png\");\nloadSprite(\"tga\", \"/sprites/dino.png\");\nloadSprite(\"ghostiny\", \"/sprites/ghostiny.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"note\", \"/sprites/note.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"cloud\", \"/sprites/cloud.png\");\nloadSprite(\"sun\", \"/sprites/sun.png\");\nloadSound(\"bell\", \"/examples/sounds/bell.mp3\");\nloadSound(\"kaboom2000\", \"/examples/sounds/kaboom2000.mp3\");\nloadBitmapFont(\"happy\", \"/examples/fonts/happy_28x36.png\", 28, 36);\n\nconst friends = [\n \"bag\",\n \"bobo\",\n \"ghosty\",\n \"gigagantrum\",\n \"tga\",\n \"ghostiny\",\n];\n\nconst FLOOR_HEIGHT = 64;\nconst JUMP_FORCE = 1320;\nconst CAPTION_SPEED = 220;\nconst PLAYER_SPEED = 640;\n\nlet started = false;\nlet music = null;\nlet burping = false;\n\n// define gravity\nsetGravity(2400);\n\n// add a game object to screen\nconst player = add([\n // list of components\n sprite(\"bean\"),\n pos(width() / 2, height() - FLOOR_HEIGHT),\n area(),\n body(),\n anchor(\"bot\"),\n z(100),\n]);\n\nconst gw = 200;\nconst gh = 140;\nconst maxRow = 4;\nconst notes = [0, 2, 4, 5, 6, 7, 8, 9, 11, 12];\n\nfor (let i = 1; i <= maxRow; i++) {\n for (let j = 0; j < i; j++) {\n const n = i * (i - 1) / 2 + j;\n const w = (i - 1) * gw + 64;\n add([\n sprite(\"note\"),\n pos(\n j * gw + (width() - w) / 2 + 32,\n height() - FLOOR_HEIGHT + 24 - (maxRow - i + 1) * gh,\n ),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n color(hsl2rgb((n * 20) / 255, 0.6, 0.7)),\n bounce(),\n scale(1),\n n === 0 ? \"burp\" : \"note\",\n { detune: notes[9 - n] * 100 + -800 },\n ]);\n }\n}\n\nfunction bounce() {\n let bouncing = false;\n let timer = 0;\n return {\n id: \"bounce\",\n require: [\"scale\"],\n update() {\n if (bouncing) {\n timer += dt() * 20;\n const w = Math.sin(timer) * 0.1;\n if (w < 0) {\n bouncing = false;\n timer = 0;\n }\n else {\n this.scale = vec2(1 + w);\n }\n }\n },\n bounce() {\n bouncing = true;\n },\n };\n}\n\n// floor\nfor (let x = 0; x < width(); x += 64) {\n add([\n pos(x, height()),\n sprite(\"grass\"),\n anchor(\"botleft\"),\n area(),\n body({ isStatic: true }),\n ]);\n}\n\nfunction jump() {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n}\n\n// jump when user press space\nonKeyPress(\"space\", jump);\nonKeyDown(\"left\", () => player.move(-PLAYER_SPEED, 0));\nonKeyDown(\"right\", () => player.move(PLAYER_SPEED, 0));\n\nplayer.onHeadbutt((block) => {\n if (block.is(\"note\")) {\n play(\"bell\", {\n detune: block.detune,\n volume: 0.1,\n });\n addKaboom(block.pos);\n shake(1);\n block.bounce();\n if (!started) {\n started = true;\n caption.hidden = false;\n caption.paused = false;\n music = play(\"kaboom2000\");\n }\n }\n else if (block.is(\"burp\")) {\n burp();\n shake(480);\n if (music) music.paused = true;\n burping = true;\n player.paused = true;\n }\n});\n\nonUpdate(() => {\n if (!burping) return;\n camPos(camPos().lerp(player.pos, dt() * 3));\n camScale(camScale().lerp(vec2(5), dt() * 3));\n});\n\nconst lyrics =\n \"kaboom2000 is out today, i have to go and try it out now... oh it's so fun it's so fun it's so fun...... it's so fun it's so fun it's so fun!\";\n\nconst caption = add([\n text(lyrics, {\n transform(idx) {\n return {\n color: hsl2rgb(\n ((time() * 60 + idx * 20) % 255) / 255,\n 0.9,\n 0.6,\n ),\n scale: wave(1.4, 1.6, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n };\n },\n }),\n pos(width(), 32),\n move(LEFT, CAPTION_SPEED),\n]);\n\ncaption.hidden = true;\ncaption.paused = true;\n\nfunction funky() {\n let timer = 0;\n return {\n id: \"funky\",\n require: [\"pos\", \"rotate\"],\n update() {\n timer += dt();\n this.angle = wave(-9, 9, timer * 4);\n },\n };\n}\n\nfunction spawnCloud() {\n const dir = choose([LEFT, RIGHT]);\n\n add([\n sprite(\"cloud\", { flipX: dir.eq(LEFT) }),\n move(dir, rand(20, 60)),\n offscreen({ destroy: true }),\n pos(dir.eq(LEFT) ? width() : 0, rand(-20, 480)),\n anchor(\"top\"),\n area(),\n z(-50),\n ]);\n\n wait(rand(6, 12), spawnCloud);\n}\n\nfunction spawnFriend() {\n const friend = choose(friends);\n const dir = choose([LEFT, RIGHT]);\n\n add([\n sprite(friend, { flipX: dir.eq(LEFT) }),\n move(dir, rand(120, 320)),\n offscreen({ destroy: true }),\n pos(dir.eq(LEFT) ? width() : 0, height() - FLOOR_HEIGHT),\n area(),\n rotate(),\n funky(),\n anchor(\"bot\"),\n z(50),\n ]);\n\n wait(rand(1, 3), spawnFriend);\n}\n\nspawnCloud();\nspawnFriend();\n\nconst sun = add([\n sprite(\"sun\"),\n anchor(\"center\"),\n pos(width() - 90, 90),\n rotate(),\n z(-100),\n]);\n\nsun.onUpdate(() => {\n sun.angle += dt() * 12;\n});\n", + "index": "12" + }, + { + "name": "confetti", + "code": "const DEF_COUNT = 80;\nconst DEF_GRAVITY = 800;\nconst DEF_AIR_DRAG = 0.9;\nconst DEF_VELOCITY = [1000, 4000];\nconst DEF_ANGULAR_VELOCITY = [-200, 200];\nconst DEF_FADE = 0.3;\nconst DEF_SPREAD = 60;\nconst DEF_SPIN = [2, 8];\nconst DEF_SATURATION = 0.7;\nconst DEF_LIGHTNESS = 0.6;\n\nkaplay();\n\nadd([\n text(\"click for confetti\"),\n anchor(\"top\"),\n pos(center().x, 0),\n]);\n\nfunction addConfetti(opt = {}) {\n const sample = (s) => typeof s === \"function\" ? s() : s;\n for (let i = 0; i < (opt.count ?? DEF_COUNT); i++) {\n const p = add([\n pos(sample(opt.pos ?? vec2(0, 0))),\n choose([\n rect(rand(5, 20), rand(5, 20)),\n circle(rand(3, 10)),\n ]),\n color(\n sample(\n opt.color\n ?? hsl2rgb(rand(0, 1), DEF_SATURATION, DEF_LIGHTNESS),\n ),\n ),\n opacity(1),\n lifespan(4),\n scale(1),\n anchor(\"center\"),\n rotate(rand(0, 360)),\n ]);\n\n const spin = rand(DEF_SPIN[0], DEF_SPIN[1]);\n const gravity = opt.gravity ?? DEF_GRAVITY;\n const airDrag = opt.airDrag ?? DEF_AIR_DRAG;\n const heading = sample(opt.heading ?? 0) - 90;\n const spread = opt.spread ?? DEF_SPREAD;\n const head = heading + rand(-spread / 2, spread / 2);\n const fade = opt.fade ?? DEF_FADE;\n const vel = sample(\n opt.velocity ?? rand(DEF_VELOCITY[0], DEF_VELOCITY[1]),\n );\n let velX = Math.cos(deg2rad(head)) * vel;\n let velY = Math.sin(deg2rad(head)) * vel;\n const velA = sample(\n opt.angularVelocity\n ?? rand(DEF_ANGULAR_VELOCITY[0], DEF_ANGULAR_VELOCITY[1]),\n );\n\n p.onUpdate(() => {\n velY += gravity * dt();\n p.pos.x += velX * dt();\n p.pos.y += velY * dt();\n p.angle += velA * dt();\n p.opacity -= fade * dt();\n velX *= airDrag;\n velY *= airDrag;\n p.scale.x = wave(-1, 1, time() * spin);\n });\n }\n}\n\nonKeyPress(\"space\", () => addConfetti({ pos: mousePos() }));\nonMousePress(() => addConfetti({ pos: mousePos() }));\n", + "index": "13" + }, + { + "name": "curves", + "code": "kaplay();\n\nfunction addPoint(c, ...args) {\n return add([\n \"point\",\n rect(8, 8),\n anchor(\"center\"),\n pos(...args),\n area(),\n color(c),\n ]);\n}\n\nfunction addBezier(...objects) {\n const points = [...objects];\n\n let t = 0;\n return add([\n pos(0, 0),\n {\n draw() {\n const coords = points.map(p => p.pos);\n const c = normalizedCurve(t => evaluateBezier(...coords, t));\n drawCurve(t => evaluateBezier(...coords, t), {\n segments: 25,\n width: 4,\n });\n drawLine({\n p1: points[0].pos,\n p2: points[1].pos,\n width: 2,\n color: rgb(0, 0, 255),\n });\n drawLine({\n p1: points[3].pos,\n p2: points[2].pos,\n width: 2,\n color: rgb(0, 0, 255),\n });\n for (let i = 0; i <= 10; i++) {\n const p = evaluateBezier(...coords, i / 10);\n drawCircle({\n pos: p,\n radius: 4,\n color: YELLOW,\n });\n }\n for (let i = 0; i <= 10; i++) {\n const p = c(i / 10);\n drawCircle({\n pos: p,\n radius: 8,\n color: RED,\n opacity: 0.5,\n });\n }\n },\n update() {\n },\n },\n ]);\n}\n\nfunction drawCatmullRom(a, b, c, d) {\n drawCurve(t => evaluateCatmullRom(a, b, c, d, t), {\n segments: 25,\n width: 4,\n });\n}\n\nfunction normalizedFirstDerivative(curve, curveFirstDerivative) {\n const curveLength = curveLengthApproximation(curve);\n const length = curveLength(1);\n return s => {\n const l = s * length;\n const t = curveLength(l, true);\n return curveFirstDerivative(t);\n };\n}\n\nfunction addCatmullRom(...objects) {\n const points = [...objects];\n\n let t = 0;\n return add([\n pos(0, 0),\n {\n draw() {\n const coords = points.map(p => p.pos);\n const first = coords[0].add(coords[0].sub(coords[1]));\n const last = coords[coords.length - 1].add(\n coords[coords.length - 1].sub(coords[coords.length - 2]),\n );\n let curve;\n let ct;\n const curveCoords = [\n [first, ...coords.slice(0, 3)],\n coords,\n [...coords.slice(1), last],\n ];\n const curveLengths = curveCoords.map(cc =>\n curveLengthApproximation(t => evaluateCatmullRom(...cc, t))(\n 1,\n )\n );\n const length = curveLengths.reduce((sum, l) => sum + l, 0);\n const p0 = curveLengths[0] / length;\n const p1 = curveLengths[1] / length;\n const p2 = curveLengths[2] / length;\n if (t <= p0) {\n curve = curveCoords[0];\n ct = t * (1 / p0);\n }\n else if (t <= p0 + p1) {\n curve = curveCoords[1];\n ct = (t - p0) * (1 / p1);\n }\n else {\n curve = curveCoords[2];\n ct = (t - p0 - p1) * (1 / p2);\n }\n const c = normalizedCurve(t => evaluateCatmullRom(...curve, t));\n const cd = normalizedFirstDerivative(\n t => evaluateCatmullRom(...curve, t),\n t => evaluateCatmullRomFirstDerivative(...curve, t),\n );\n\n drawCatmullRom(first, ...coords.slice(0, 3), {\n segments: 10,\n width: 4,\n });\n drawCatmullRom(...coords, { segments: 10, width: 4 });\n drawCatmullRom(...coords.slice(1), last, {\n segments: 10,\n width: 4,\n });\n\n const cartPos1 = evaluateCatmullRom(...curve, ct);\n const tangent1 = evaluateCatmullRomFirstDerivative(\n ...curve,\n ct,\n );\n pushTransform();\n pushTranslate(cartPos1);\n pushRotate(tangent1.angle(1, 0));\n drawRect({\n width: 16,\n height: 8,\n pos: vec2(-8, -4),\n color: YELLOW,\n outline: { color: BLUE, width: 4 },\n });\n popTransform();\n\n const cartPos2 = c(ct);\n const tangent2 = cd(ct);\n pushTransform();\n pushTranslate(cartPos2);\n pushRotate(tangent2.angle(1, 0));\n drawRect({\n width: 16,\n height: 8,\n pos: vec2(-8, -4),\n color: RED,\n opacity: 0.5,\n outline: { color: BLACK, width: 4 },\n });\n popTransform();\n },\n update() {\n t += dt() / 10;\n t = t % 1;\n },\n },\n ]);\n}\n\n// Interraction\nlet obj = null;\n\nonClick(\"point\", (point) => {\n obj = point;\n});\n\nonMouseMove((pos) => {\n if (obj) {\n obj.moveTo(pos);\n }\n});\n\nonMouseRelease((pos) => {\n obj = null;\n});\n\n// Scene creation\nconst p0 = addPoint(RED, 100, 40);\nconst p1 = addPoint(BLUE, 80, 120);\nconst p2 = addPoint(BLUE, 300, 60);\nconst p3 = addPoint(RED, 250, 200);\n\naddBezier(p0, p1, p2, p3);\n\nadd([\n pos(20, 300),\n text(\"yellow: default spacing\\nred: constant spacing\", { size: 20 }),\n]);\n\nconst c0 = addPoint(RED, 400, 40);\nconst c1 = addPoint(RED, 380, 120);\nconst c2 = addPoint(RED, 500, 60);\nconst c3 = addPoint(RED, 450, 200);\n\naddCatmullRom(c0, c1, c2, c3);\n\nadd([\n pos(400, 300),\n text(\"yellow: default speed\\nred: constant speed\", { size: 20 }),\n]);\n\nadd([\n pos(20, 350),\n text(\n \"curves are non-linear in t. This means that for a given t,\\nthe distance traveled from the start doesn't grow at constant speed.\\nTo fix this, turn the curve into a normalized curve first.\\nUse derivatives to find the direction of the curve at a certain t.\",\n { size: 20 },\n ),\n]);\n", + "index": "14" + }, + { + "name": "dialog", + "code": "// Simple dialogues\n\nkaplay({\n background: [255, 209, 253],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"mark\", \"/sprites/mark.png\");\n\n// Define the dialogue data\nconst dialogs = [\n [\"bean\", \"hi my butterfly\"],\n [\"bean\", \"i love u\"],\n [\"bean\", \"you love me? pretty baby\"],\n [\"bean\", \"mark is a stupid\"],\n [\"bean\", \"he did not know how to take care of you...\"],\n [\"mark\", \"you don't know me ...\"],\n [\"bean\", \"what! mark???\"],\n [\"mark\", \"oh...hi \"],\n];\n\nlet curDialog = 0;\n\n// Text bubble\nconst textbox = add([\n rect(width() - 200, 120, { radius: 32 }),\n anchor(\"center\"),\n pos(center().x, height() - 100),\n outline(4),\n]);\n\n// Text\nconst txt = add([\n text(\"\", { size: 32, width: width() - 230, align: \"center\" }),\n pos(textbox.pos),\n anchor(\"center\"),\n color(0, 0, 0),\n]);\n\n// Character avatar\nconst avatar = add([\n sprite(\"bean\"),\n scale(3),\n anchor(\"center\"),\n pos(center().sub(0, 50)),\n]);\n\nonKeyPress(\"space\", () => {\n // Cycle through the dialogs\n curDialog = (curDialog + 1) % dialogs.length;\n updateDialog();\n});\n\n// Update the on screen sprite & text\nfunction updateDialog() {\n const [char, dialog] = dialogs[curDialog];\n\n // Use a new sprite component to replace the old one\n avatar.use(sprite(char));\n // Update the dialog text\n txt.text = dialog;\n}\n\nupdateDialog();\n", + "index": "15" + }, + { + "name": "doublejump", + "code": "kaplay({\n background: [141, 183, 255],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSound(\"coin\", \"/examples/sounds/score.mp3\");\n\nsetGravity(4000);\n\nconst PLAYER_SPEED = 640;\nconst JUMP_FORCE = 1200;\nconst NUM_PLATFORMS = 5;\n\n// a spinning component for fun\nfunction spin(speed = 1200) {\n let spinning = false;\n return {\n require: [\"rotate\"],\n update() {\n if (!spinning) {\n return;\n }\n this.angle -= speed * dt();\n if (this.angle <= -360) {\n spinning = false;\n this.angle = 0;\n }\n },\n spin() {\n spinning = true;\n },\n };\n}\n\nscene(\"game\", () => {\n const score = add([\n text(\"0\", 24),\n pos(24, 24),\n { value: 0 },\n ]);\n\n const bean = add([\n sprite(\"bean\"),\n area(),\n anchor(\"center\"),\n pos(0, 0),\n body({ jumpForce: JUMP_FORCE }),\n doubleJump(),\n rotate(0),\n spin(),\n ]);\n\n for (let i = 1; i < NUM_PLATFORMS; i++) {\n add([\n sprite(\"grass\"),\n area(),\n pos(rand(0, width()), i * height() / NUM_PLATFORMS),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"platform\",\n {\n speed: rand(120, 320),\n dir: choose([-1, 1]),\n },\n ]);\n }\n\n // go to the first platform\n bean.pos = get(\"platform\")[0].pos.sub(0, 64);\n\n function genCoin(avoid) {\n const plats = get(\"platform\");\n let idx = randi(0, plats.length);\n // avoid the spawning on the same platforms\n if (avoid != null) {\n idx = choose([...plats.keys()].filter((i) => i !== avoid));\n }\n const plat = plats[idx];\n add([\n pos(),\n anchor(\"center\"),\n sprite(\"coin\"),\n area(),\n follow(plat, vec2(0, -60)),\n \"coin\",\n { idx: idx },\n ]);\n }\n\n genCoin(0);\n\n for (let i = 0; i < width() / 64; i++) {\n add([\n pos(i * 64, height()),\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n scale(),\n \"danger\",\n ]);\n }\n\n bean.onCollide(\"danger\", () => {\n go(\"lose\");\n });\n\n bean.onCollide(\"coin\", (c) => {\n destroy(c);\n play(\"coin\");\n score.value += 1;\n score.text = score.value;\n genCoin(c.idx);\n });\n\n // spin on double jump\n bean.onDoubleJump(() => {\n bean.spin();\n });\n\n onUpdate(\"platform\", (p) => {\n p.move(p.dir * p.speed, 0);\n if (p.pos.x < 0 || p.pos.x > width()) {\n p.dir = -p.dir;\n }\n });\n\n onKeyPress(\"space\", () => {\n bean.doubleJump();\n });\n\n function move(x) {\n bean.move(x, 0);\n if (bean.pos.x < 0) {\n bean.pos.x = width();\n }\n else if (bean.pos.x > width()) {\n bean.pos.x = 0;\n }\n }\n\n // both keys will trigger\n onKeyDown(\"left\", () => {\n move(-PLAYER_SPEED);\n });\n\n onKeyDown(\"right\", () => {\n move(PLAYER_SPEED);\n });\n\n onGamepadButtonPress(\"south\", () => bean.doubleJump());\n\n onGamepadStick(\"left\", (v) => {\n move(v.x * PLAYER_SPEED);\n });\n\n let timeLeft = 30;\n\n const timer = add([\n anchor(\"topright\"),\n pos(width() - 24, 24),\n text(timeLeft),\n ]);\n\n onUpdate(() => {\n timeLeft -= dt();\n if (timeLeft <= 0) {\n go(\"win\", score.value);\n }\n timer.text = timeLeft.toFixed(2);\n });\n});\n\nscene(\"win\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 80),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 80),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onGamepadButtonPress(\"south\", () => go(\"game\"));\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n ]);\n onKeyPress(\"space\", () => go(\"game\"));\n onGamepadButtonPress(\"south\", () => go(\"game\"));\n});\n\ngo(\"game\");\n", + "index": "16" + }, + { + "name": "drag", + "code": "// Drag & drop interaction\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Keep track of the current draggin item\nlet curDraggin = null;\n\n// A custom component for handling drag & drop behavior\nfunction drag() {\n // The displacement between object pos and mouse pos\n let offset = vec2(0);\n\n return {\n // Name of the component\n id: \"drag\",\n // This component requires the \"pos\" and \"area\" component to work\n require: [\"pos\", \"area\"],\n pick() {\n // Set the current global dragged object to this\n curDraggin = this;\n offset = mousePos().sub(this.pos);\n this.trigger(\"drag\");\n },\n // \"update\" is a lifecycle method gets called every frame the obj is in scene\n update() {\n if (curDraggin === this) {\n this.pos = mousePos().sub(offset);\n this.trigger(\"dragUpdate\");\n }\n },\n onDrag(action) {\n return this.on(\"drag\", action);\n },\n onDragUpdate(action) {\n return this.on(\"dragUpdate\", action);\n },\n onDragEnd(action) {\n return this.on(\"dragEnd\", action);\n },\n };\n}\n\n// Check if someone is picked\nonMousePress(() => {\n if (curDraggin) {\n return;\n }\n // Loop all \"bean\"s in reverse, so we pick the topmost one\n for (const obj of get(\"drag\").reverse()) {\n // If mouse is pressed and mouse position is inside, we pick\n if (obj.isHovering()) {\n obj.pick();\n break;\n }\n }\n});\n\n// Drop whatever is dragged on mouse release\nonMouseRelease(() => {\n if (curDraggin) {\n curDraggin.trigger(\"dragEnd\");\n curDraggin = null;\n }\n});\n\n// Reset cursor to default at frame start for easier cursor management\nonUpdate(() => setCursor(\"default\"));\n\n// Add dragable objects\nfor (let i = 0; i < 48; i++) {\n const bean = add([\n sprite(\"bean\"),\n pos(rand(width()), rand(height())),\n area({ cursor: \"pointer\" }),\n scale(5),\n anchor(\"center\"),\n // using our custom component here\n drag(),\n i !== 0 ? color(255, 255, 255) : color(255, 0, 255),\n \"bean\",\n ]);\n\n bean.onDrag(() => {\n // Remove the object and re-add it, so it'll be drawn on top\n readd(bean);\n });\n\n bean.onDragUpdate(() => {\n setCursor(\"move\");\n });\n}\n", + "index": "17" + }, + { + "name": "draw", + "code": "// Kaboom as pure rendering lib (no component / game obj etc.)\n\nkaplay();\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nloadShader(\n \"spiral\",\n null,\n `\nuniform float u_time;\nuniform vec2 u_mpos;\nvec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {\n\tvec2 pp = uv - u_mpos;\n\tfloat angle = atan(pp.y, pp.x);\n\tfloat dis = length(pp);\n\tfloat c = sin(dis * 48.0 + u_time * 8.0 + angle);\n\treturn vec4(c, c, c, 1);\n}\n`,\n);\n\nconst t = (n = 1) => time() * n;\nconst w = (a, b, n) => wave(a, b, t(n));\nconst px = 160;\nconst py = 160;\nconst doodles = [];\nconst trail = [];\n\nconst outline = {\n width: 4,\n color: rgb(0, 0, 0),\n join: \"miter\",\n};\n\nfunction drawStuff() {\n const mx = (width() - px * 2) / 2;\n const my = (height() - py * 2) / 1;\n const p = (x, y) => vec2(x, y).scale(mx, my).add(px, py);\n\n drawSprite({\n sprite: \"bean\",\n pos: p(0, 0),\n angle: t(40),\n anchor: \"center\",\n scale: w(1, 1.5, 4),\n color: rgb(w(128, 255, 4), w(128, 255, 8), 255),\n });\n\n drawRect({\n pos: p(1, 0),\n width: w(60, 120, 4),\n height: w(100, 140, 8),\n anchor: \"center\",\n radius: w(0, 32, 4),\n angle: t(80),\n color: rgb(w(128, 255, 4), 255, w(128, 255, 8)),\n outline,\n });\n\n drawEllipse({\n pos: p(2, 0),\n radiusX: w(40, 70, 2),\n radiusY: w(40, 70, 4),\n start: 0,\n end: w(180, 360, 1),\n color: rgb(255, w(128, 255, 8), w(128, 255, 4)),\n // gradient: [ Color.RED, Color.BLUE ],\n outline,\n });\n\n drawPolygon({\n pos: p(0, 1),\n pts: [\n vec2(w(-10, 10, 2), -80),\n vec2(80, w(-10, 10, 4)),\n vec2(w(30, 50, 4), 80),\n vec2(-30, w(50, 70, 2)),\n vec2(w(-50, -70, 4), 0),\n ],\n colors: [\n rgb(w(128, 255, 8), 255, w(128, 255, 4)),\n rgb(255, w(128, 255, 8), w(128, 255, 4)),\n rgb(w(128, 255, 8), w(128, 255, 4), 255),\n rgb(255, 128, w(128, 255, 4)),\n rgb(w(128, 255, 8), w(128, 255, 4), 128),\n ],\n outline,\n });\n\n drawText({\n text: \"yo\",\n pos: p(1, 1),\n anchor: \"center\",\n size: w(80, 120, 2),\n color: rgb(w(128, 255, 4), w(128, 255, 8), w(128, 255, 2)),\n });\n\n drawLines({\n ...outline,\n pts: trail,\n });\n\n doodles.forEach((pts) => {\n drawLines({\n ...outline,\n pts: pts,\n });\n });\n}\n\n// onDraw() is similar to onUpdate(), it runs every frame, but after all update events.\n// All drawXXX() functions need to be called every frame if you want them to persist\nonDraw(() => {\n const maskFunc = Math.floor(time()) % 2 === 0 ? drawSubtracted : drawMasked;\n\n if (isKeyDown(\"space\")) {\n maskFunc(() => {\n drawUVQuad({\n width: width(),\n height: height(),\n shader: \"spiral\",\n uniform: {\n \"u_time\": time(),\n \"u_mpos\": mousePos().scale(1 / width(), 1 / height()),\n },\n });\n }, drawStuff);\n }\n else {\n drawStuff();\n }\n});\n\n// It's a common practice to put all input handling and state updates before rendering.\nonUpdate(() => {\n trail.push(mousePos());\n\n if (trail.length > 16) {\n trail.shift();\n }\n\n if (isMousePressed()) {\n doodles.push([]);\n }\n\n if (isMouseDown() && isMouseMoved()) {\n doodles[doodles.length - 1].push(mousePos());\n }\n});\n", + "index": "18" + }, + { + "name": "easing", + "code": "kaplay();\n\nadd([\n pos(20, 20),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"steps\",\n]);\n\nonClick(\"steps\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => {\n square.color = value;\n },\n easingSteps(5, \"jump-none\"),\n );\n});\n\nadd([\n pos(80, 20),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"stepsmove\",\n]);\n\nonClick(\"stepsmove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingSteps(5, \"jump-none\"),\n );\n});\n\nadd([\n pos(20, 120),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"linear\",\n]);\n\nonClick(\"linear\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => square.color = value,\n easingLinear([vec2(0, 0), vec2(0.5, 0.25), vec2(1, 1)]),\n );\n});\n\nadd([\n pos(80, 120),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"linearmove\",\n]);\n\nonClick(\"linearmove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingLinear([vec2(0, 0), vec2(0.5, 0.25), vec2(1, 1)]),\n );\n});\n\nadd([\n pos(20, 220),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"bezier\",\n]);\n\nonClick(\"bezier\", (square) => {\n square.tween(\n WHITE,\n BLACK,\n 2,\n (value) => square.color = value,\n easingCubicBezier(vec2(.17, .67), vec2(.77, .71)),\n );\n});\n\nadd([\n pos(80, 220),\n rect(50, 50),\n color(WHITE),\n timer(),\n area(),\n \"beziermove\",\n]);\n\nonClick(\"beziermove\", (square) => {\n square.tween(\n 80,\n 400,\n 2,\n (value) => {\n square.pos.x = value;\n },\n easingCubicBezier(vec2(.17, .67), vec2(.77, .71)),\n );\n});\n", + "index": "19" + }, + { + "name": "eatlove", + "code": "kaplay();\n\nconst fruits = [\n \"apple\",\n \"pineapple\",\n \"grape\",\n \"watermelon\",\n];\n\nfor (const fruit of fruits) {\n loadSprite(fruit, `/sprites/${fruit}.png`);\n}\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"heart\", \"/sprites/heart.png\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\n\nscene(\"start\", () => {\n play(\"wooosh\");\n\n add([\n text(\"Eat All\"),\n pos(center().sub(0, 100)),\n scale(2),\n anchor(\"center\"),\n ]);\n\n add([\n sprite(\"heart\"),\n pos(center().add(0, 100)),\n scale(2),\n anchor(\"center\"),\n ]);\n\n wait(1.5, () => go(\"game\"));\n});\n\n// main game scene content\nscene(\"game\", () => {\n const SPEED_MIN = 120;\n const SPEED_MAX = 640;\n\n // add the player game object\n const player = add([\n sprite(\"bean\"),\n pos(40, 20),\n area({ scale: 0.5 }),\n anchor(\"center\"),\n ]);\n\n // make the layer move by mouse\n player.onUpdate(() => {\n player.pos = mousePos();\n });\n\n // game over if player eats a fruit\n player.onCollide(\"fruit\", () => {\n go(\"lose\", score);\n play(\"hit\");\n });\n\n // move the food every frame, destroy it if far outside of screen\n onUpdate(\"food\", (food) => {\n food.move(-food.speed, 0);\n if (food.pos.x < -120) {\n destroy(food);\n }\n });\n\n onUpdate(\"heart\", (heart) => {\n if (heart.pos.x <= 0) {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(heart.pos);\n }\n });\n\n // score counter\n let score = 0;\n\n const scoreLabel = add([\n text(score, 32),\n pos(12, 12),\n ]);\n\n // increment score if player eats a heart\n player.onCollide(\"heart\", (heart) => {\n addKaboom(player.pos);\n score += 1;\n destroy(heart);\n scoreLabel.text = score;\n burp();\n shake(12);\n });\n\n // do this every 0.3 seconds\n loop(0.3, () => {\n // spawn from right side of the screen\n const x = width() + 24;\n // spawn from a random y position\n const y = rand(0, height());\n // get a random speed\n const speed = rand(SPEED_MIN, SPEED_MAX);\n // 50% percent chance is heart\n const isHeart = chance(0.5);\n const spriteName = isHeart ? \"heart\" : choose(fruits);\n\n add([\n sprite(spriteName),\n pos(x, y),\n area({ scale: 0.5 }),\n anchor(\"center\"),\n \"food\",\n isHeart ? \"heart\" : \"fruit\",\n { speed: speed },\n ]);\n });\n});\n\n// game over scene\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"start\"));\n onClick(() => go(\"start\"));\n});\n\n// start with the \"game\" scene\ngo(\"start\");\n", + "index": "20" + }, + { + "name": "egg", + "code": "// Egg minigames (yes, like Peppa)\n\nkaplay({\n background: [135, 62, 132],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"egg\", \"/sprites/egg.png\");\nloadSprite(\"egg_crack\", \"/sprites/egg_crack.png\");\n\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n anchor(\"center\"),\n z(50),\n]);\n\nconst counter = add([\n text(\"0\"),\n pos(24, 24),\n z(100),\n { value: 0 },\n]);\n\n// \"shake\" is taken, so..\nfunction rock() {\n let strength = 0;\n let time = 0;\n return {\n id: \"rock\",\n require: [\"rotate\"],\n update() {\n if (strength === 0) {\n return;\n }\n this.angle = Math.sin(time * 10) * strength;\n time += dt();\n strength -= dt() * 30;\n if (strength <= 0) {\n strength = 0;\n time = 0;\n }\n },\n rock(n = 15) {\n strength = n;\n },\n };\n}\n\nonKeyPress(\"space\", () => {\n add([\n sprite(\"egg\"),\n pos(player.pos.add(0, 24)),\n rotate(0),\n anchor(\"bot\"),\n rock(),\n \"egg\",\n { stage: 0 },\n ]);\n\n player.moveTo(rand(0, width()), rand(0, height()));\n});\n\n// HATCH\nonKeyPress(\"enter\", () => {\n get(\"egg\", { recursive: true }).forEach((e) => {\n if (e.stage === 0) {\n e.stage = 1;\n e.rock();\n e.use(sprite(\"egg_crack\"));\n }\n else if (e.stage === 1) {\n e.stage = 2;\n e.use(sprite(\"bean\"));\n addKaboom(e.pos.sub(0, e.height / 2));\n counter.value += 1;\n counter.text = counter.value;\n }\n });\n});\n", + "index": "21" + }, + { + "name": "fadeIn", + "code": "kaplay();\n\nloadBean();\n\n// spawn a bean that takes a second to fade in\nconst bean = add([\n sprite(\"bean\"),\n pos(120, 80),\n opacity(1), // opacity() component gives it opacity which is required for fadeIn\n]);\n\nbean.fadeIn(1); // makes it fade in\n\n// spawn another bean that takes 5 seconds to fade in halfway\n// SPOOKY!\nlet spookyBean = add([\n sprite(\"bean\"),\n pos(240, 80),\n opacity(0.5), // opacity() component gives it opacity which is required for fadeIn (set to 0.5 so it will be half transparent)\n]);\n\nspookyBean.fadeIn(5); // makes it fade in (set to 5 so that it takes 5 seconds to fade in)\n", + "index": "22" + }, + { + "name": "fakeMouse", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"cursor\", \"/sprites/cursor_default.png\");\n\n// set the layers\nlayers([\n \"game\",\n \"ui\",\n], \"game\");\n\nconst MOUSE_VEL = 200;\n\nconst cursor = add([\n sprite(\"cursor\"),\n fakeMouse(),\n pos(),\n layer(\"ui\"),\n]);\n\n// Mouse press and release\ncursor.onKeyPress(\"f\", () => {\n cursor.press();\n});\n\ncursor.onKeyRelease(\"f\", () => {\n cursor.release();\n});\n\n// Mouse movement\ncursor.onKeyDown(\"left\", () => {\n cursor.move(-MOUSE_VEL, 0);\n});\n\ncursor.onKeyDown(\"right\", () => {\n cursor.move(MOUSE_VEL, 0);\n});\n\ncursor.onKeyDown(\"up\", () => {\n cursor.move(0, -MOUSE_VEL);\n});\n\ncursor.onKeyDown(\"down\", () => {\n cursor.move(0, MOUSE_VEL);\n});\n\n// Example with hovering and click\nconst bean = add([\n sprite(\"bean\"),\n area(),\n color(BLUE),\n]);\n\nbean.onClick(() => {\n debug.log(\"ohhi\");\n});\n\nbean.onHover(() => {\n bean.color = RED;\n});\n\nbean.onHoverEnd(() => {\n bean.color = BLUE;\n});\n", + "index": "23" + }, + { + "name": "fall", + "code": "// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\n\nsetGravity(2400);\n\naddLevel([\n // Design the level layout with symbols\n \" \",\n \" \",\n \" \",\n \" \",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100),\n // Define what each symbol means (in components)\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n ],\n },\n});\n\nloop(0.2, () => {\n const coin = add([\n pos(rand(100, 400), 0),\n sprite(\"coin\"),\n area(),\n body(),\n \"coin\",\n ]);\n wait(3, () => coin.destroy());\n});\n\ndebug.paused = true;\n\nonKeyPressRepeat(\"space\", () => {\n debug.stepFrame();\n});\n", + "index": "24" + }, + { + "name": "fixedUpdate", + "code": "kaplay();\n\nlet fixedCount = 0;\nlet count = 0;\n\nonFixedUpdate(() => {\n fixedCount++;\n});\n\nonUpdate(() => {\n count++;\n debug.log(\n `${fixedDt()} ${Math.floor(fixedCount / time())} ${dt()} ${\n Math.floor(count / time())\n }`,\n );\n});\n", + "index": "25" + }, + { + "name": "flamebar", + "code": "// Mario-like flamebar\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"pineapple\", \"/sprites/pineapple.png\");\n\n// Define player movement speed\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(80, 40),\n area(),\n]);\n\n// Player movement\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\n// Function to add a flamebar\nfunction addFlamebar(position = vec2(0), angle = 0, num = 6) {\n // Create a parent game object for position and rotation\n const flameHead = add([\n pos(position),\n rotate(angle),\n ]);\n\n // Add each section of flame as children\n for (let i = 0; i < num; i++) {\n flameHead.add([\n sprite(\"pineapple\"),\n pos(0, i * 48),\n area(),\n anchor(\"center\"),\n \"flame\",\n ]);\n }\n\n // The flame head's rotation will affect all its children\n flameHead.onUpdate(() => {\n flameHead.angle += dt() * 60;\n });\n\n return flameHead;\n}\n\naddFlamebar(vec2(200, 300), -60);\naddFlamebar(vec2(480, 100), 180);\naddFlamebar(vec2(400, 480), 0);\n\n// Game over if player touches a flame\nplayer.onCollide(\"flame\", () => {\n addKaboom(player.pos);\n player.destroy();\n});\n", + "index": "26" + }, + { + "name": "flappy", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\n\n// define gravity\nsetGravity(3200);\n\nscene(\"game\", () => {\n const PIPE_OPEN = 240;\n const PIPE_MIN = 60;\n const JUMP_FORCE = 800;\n const SPEED = 320;\n const CEILING = -60;\n\n // a game object consists of a list of components and tags\n const bean = add([\n // sprite() means it's drawn with a sprite of name \"bean\" (defined above in 'loadSprite')\n sprite(\"bean\"),\n // give it a position\n pos(width() / 4, 0),\n // give it a collider\n area(),\n // body component enables it to fall and jump in a gravity world\n body(),\n ]);\n\n // check for fall death\n bean.onUpdate(() => {\n if (bean.pos.y >= height() || bean.pos.y <= CEILING) {\n // switch to \"lose\" scene\n go(\"lose\", score);\n }\n });\n\n // jump\n onKeyPress(\"space\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n onGamepadButtonPress(\"south\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n // mobile\n onClick(() => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n function spawnPipe() {\n // calculate pipe positions\n const h1 = rand(PIPE_MIN, height() - PIPE_MIN - PIPE_OPEN);\n const h2 = height() - h1 - PIPE_OPEN;\n\n add([\n pos(width(), 0),\n rect(64, h1),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n ]);\n\n add([\n pos(width(), h1 + PIPE_OPEN),\n rect(64, h2),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n // raw obj just assigns every field to the game obj\n { passed: false },\n ]);\n }\n\n // callback when bean onCollide with objects with tag \"pipe\"\n bean.onCollide(\"pipe\", () => {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(bean.pos);\n });\n\n // per frame event for all objects with tag 'pipe'\n onUpdate(\"pipe\", (p) => {\n // check if bean passed the pipe\n if (p.pos.x + p.width <= bean.pos.x && p.passed === false) {\n addScore();\n p.passed = true;\n }\n });\n\n // spawn a pipe every 1 sec\n loop(1, () => {\n spawnPipe();\n });\n\n let score = 0;\n\n // display score\n const scoreLabel = add([\n text(score),\n anchor(\"center\"),\n pos(width() / 2, 80),\n fixed(),\n z(100),\n ]);\n\n function addScore() {\n score++;\n scoreLabel.text = score;\n play(\"score\");\n }\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", + "index": "27" + }, + { + "name": "gamepad", + "code": "kaplay();\nsetGravity(2400);\nsetBackground(0, 0, 0);\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nscene(\"nogamepad\", () => {\n add([\n text(\"Gamepad not found.\\nConnect a gamepad and press a button!\", {\n width: width() - 80,\n align: \"center\",\n }),\n pos(center()),\n anchor(\"center\"),\n ]);\n onGamepadConnect(() => {\n go(\"game\");\n });\n});\n\nscene(\"game\", () => {\n const player = add([\n pos(center()),\n anchor(\"center\"),\n sprite(\"bean\"),\n area(),\n body(),\n ]);\n\n // platform\n add([\n pos(0, height()),\n anchor(\"botleft\"),\n rect(width(), 140),\n area(),\n body({ isStatic: true }),\n ]);\n\n onGamepadButtonPress((b) => {\n debug.log(b);\n });\n\n onGamepadButtonPress([\"south\", \"west\"], () => {\n player.jump();\n });\n\n onGamepadStick(\"left\", (v) => {\n player.move(v.x * 400, 0);\n });\n\n onGamepadDisconnect(() => {\n go(\"nogamepad\");\n });\n});\n\nif (getGamepads().length > 0) {\n go(\"game\");\n}\nelse {\n go(\"nogamepad\");\n}\n", + "index": "28" + }, + { + "name": "ghosthunting", + "code": "kaplay({\n width: 1024,\n height: 768,\n letterbox: true,\n});\n\nloadSprite(\"bean\", \"./sprites/bean.png\");\nloadSprite(\"gun\", \"./sprites/gun.png\");\nloadSprite(\"ghosty\", \"./sprites/ghosty.png\");\nloadSprite(\"hexagon\", \"./examples/sprites/particle_hexagon_filled.png\");\nloadSprite(\"star\", \"./examples/sprites/particle_star_filled.png\");\n\nconst nav = new NavMesh();\n// Hallway\nnav.addPolygon([vec2(20, 20), vec2(1004, 20), vec2(620, 120), vec2(20, 120)]);\n// Living room\nnav.addPolygon([\n vec2(620, 120),\n vec2(1004, 20),\n vec2(1004, 440),\n vec2(620, 140),\n]);\nnav.addPolygon([vec2(20, 140), vec2(620, 140), vec2(1004, 440), vec2(20, 440)]);\n// Kitchen\nnav.addPolygon([vec2(20, 460), vec2(320, 460), vec2(320, 748), vec2(20, 748)]);\nnav.addPolygon([\n vec2(320, 440),\n vec2(420, 440),\n vec2(420, 748),\n vec2(320, 748),\n]);\nnav.addPolygon([\n vec2(420, 460),\n vec2(620, 460),\n vec2(620, 748),\n vec2(420, 748),\n]);\n// Storage room\nnav.addPolygon([\n vec2(640, 460),\n vec2(720, 460),\n vec2(720, 748),\n vec2(640, 748),\n]);\nnav.addPolygon([\n vec2(720, 440),\n vec2(820, 440),\n vec2(820, 748),\n vec2(720, 748),\n]);\nnav.addPolygon([\n vec2(820, 460),\n vec2(1004, 460),\n vec2(1004, 748),\n vec2(820, 748),\n]);\n\n// Border\nadd([\n pos(0, 0),\n rect(20, height()),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(0, 0),\n rect(width(), 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(width() - 20, 0),\n rect(20, height()),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(0, height() - 20),\n rect(width(), 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Hallway\nadd([\n pos(20, 20),\n rect(600, 100),\n color(rgb(128, 64, 64)),\n \"floor\",\n]);\nadd([\n pos(20, 120),\n rect(600, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Living room\nadd([\n pos(20, 140),\n rect(600, 300),\n color(rgb(64, 64, 128)),\n \"floor\",\n]);\nadd([\n pos(620, 20),\n rect(384, 420),\n color(rgb(64, 64, 128)),\n \"floor\",\n]);\nadd([\n pos(20, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(420, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\nadd([\n pos(820, 440),\n rect(300, 20),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Kitchen\nadd([\n pos(320, 440),\n rect(100, 20),\n color(rgb(128, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(20, 460),\n rect(600, 288),\n color(rgb(128, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(620, 460),\n rect(20, 288),\n area(),\n body({ isStatic: true }),\n color(rgb(128, 128, 128)),\n \"wall\",\n]);\n// Storage\nadd([\n pos(720, 440),\n rect(100, 20),\n color(rgb(64, 128, 64)),\n \"floor\",\n]);\nadd([\n pos(640, 460),\n rect(364, 288),\n color(rgb(64, 128, 64)),\n \"floor\",\n]);\n\nconst player = add([\n pos(50, 50),\n sprite(\"bean\"),\n anchor(vec2(0, 0)),\n area(),\n body(),\n \"player\",\n]);\n\nconst gun = player.add([\n sprite(\"gun\"),\n anchor(vec2(-2, 0)),\n rotate(0),\n \"player\",\n]);\n\nfunction addEnemy(p) {\n const enemy = add([\n {\n add() {\n this.onHurt(() => {\n this.opacity = this.hp() / 100;\n });\n this.onDeath(() => {\n const rect = this.localArea();\n rect.pos = rect.pos.sub(rect.width / 2, rect.height / 2);\n const dissipate = add([\n pos(this.pos),\n particles({\n max: 20,\n speed: [50, 100],\n angle: [0, 360],\n angularVelocity: [45, 90],\n lifeTime: [1.0, 1.5],\n colors: [rgb(128, 128, 255), WHITE],\n opacities: [0.1, 1.0, 0.0],\n texture: getSprite(\"star\").data.tex,\n quads: [getSprite(\"star\").data.frames[0]],\n }, {\n lifetime: 1.5,\n shape: rect,\n rate: 0,\n direction: -90,\n spread: 0,\n }),\n ]);\n dissipate.emit(20);\n dissipate.onEnd(() => {\n destroy(dissipate);\n });\n destroy(this);\n });\n this.onObjectsSpotted(objects => {\n const playerSeen = objects.some(o => o.is(\"player\"));\n if (playerSeen) {\n enemy.action = \"pursuit\";\n enemy.waypoints = null;\n }\n });\n this.onPatrolFinished(() => {\n enemy.action = \"observe\";\n });\n },\n },\n pos(p),\n sprite(\"ghosty\"),\n opacity(1),\n anchor(vec2(0, 0)),\n area(),\n body(),\n // Health provides properties and methods to keep track of the enemies health\n health(100),\n // Sentry makes it easy to check for visibility of the player\n sentry({ includes: \"player\" }, {\n lineOfSight: true,\n raycastExclude: [\"enemy\"],\n }),\n // Patrol can make the enemy follow a computed path\n patrol({ speed: 100 }),\n // Navigator can compute a path given a graph\n navigation({\n graph: nav,\n navigationOpt: {\n type: \"edges\",\n },\n }),\n \"enemy\",\n { action: \"observing\", waypoint: null },\n ]);\n return enemy;\n}\n\naddEnemy(vec2(width() * 3 / 4, height() / 2));\naddEnemy(vec2(width() * 1 / 4, height() / 2));\naddEnemy(vec2(width() * 1 / 4, height() * 2 / 3));\naddEnemy(vec2(width() * 0.8, height() * 2 / 3));\n\nlet path;\nonUpdate(\"enemy\", enemy => {\n switch (enemy.action) {\n case \"observe\": {\n break;\n }\n case \"pursuit\": {\n if (enemy.hasLineOfSight(player)) {\n // We can see the player, just go straight to their location\n enemy.moveTo(player.pos, 100);\n }\n else {\n // We can't see the player, but we know where they are, plot a path\n path = enemy.navigateTo(player.pos);\n // enemy.waypoint = path[1];\n enemy.waypoints = path;\n enemy.action = \"observe\";\n }\n break;\n }\n }\n});\n\nconst SPEED = 100;\n\nconst dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n \"a\": LEFT,\n \"d\": RIGHT,\n \"w\": UP,\n \"s\": DOWN,\n};\n\nfor (const dir in dirs) {\n onKeyDown(dir, () => {\n player.move(dirs[dir].scale(SPEED));\n });\n}\n\nonMouseMove(() => {\n gun.angle = mousePos().sub(player.pos).angle();\n gun.flipY = Math.abs(gun.angle) > 90;\n});\n\nonMousePress(() => {\n const flash = gun.add([\n pos(\n getSprite(\"gun\").data.width * 1.5,\n Math.abs(gun.angle) > 90 ? 7 : -7,\n ),\n circle(10),\n color(YELLOW),\n opacity(0.5),\n ]);\n flash.fadeOut(0.5).then(() => {\n destroy(flash);\n });\n\n const dir = mousePos().sub(player.pos).unit().scale(1024);\n const hit = raycast(player.pos, dir, [\n \"player\",\n ]);\n if (hit) {\n const splatter = add([\n pos(hit.point),\n particles({\n max: 20,\n speed: [200, 250],\n lifeTime: [0.2, 0.75],\n colors: [WHITE],\n opacities: [1.0, 0.0],\n angle: [0, 360],\n texture: getSprite(\"hexagon\").data.tex,\n quads: [getSprite(\"hexagon\").data.frames[0]],\n }, {\n lifetime: 0.75,\n rate: 0,\n direction: dir.scale(-1).angle(),\n spread: 45,\n }),\n ]);\n splatter.emit(10);\n splatter.onEnd(() => {\n destroy(splatter);\n });\n if (hit.object && hit.object.is(\"enemy\")) {\n hit.object.moveBy(dir.unit().scale(10));\n hit.object.hurt(20);\n }\n }\n});\n", + "index": "29" + }, + { + "name": "gravity", + "code": "// Responding to gravity & jumping\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Set the gravity acceleration (pixels per second)\nsetGravity(1600);\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n pos(center()),\n area(),\n // body() component gives the ability to respond to gravity\n body(),\n]);\n\nonKeyPress(\"space\", () => {\n // .isGrounded() is provided by body()\n if (player.isGrounded()) {\n // .jump() is provided by body()\n player.jump();\n }\n});\n\n// .onGround() is provided by body(). It registers an event that runs whenever player hits the ground.\nplayer.onGround(() => {\n debug.log(\"ouch\");\n});\n\n// Accelerate falling when player holding down arrow key\nonKeyDown(\"down\", () => {\n if (!player.isGrounded()) {\n player.vel.y += dt() * 1200;\n }\n});\n\n// Jump higher if space is held\nonKeyDown(\"space\", () => {\n if (!player.isGrounded() && player.vel.y < 0) {\n player.vel.y -= dt() * 600;\n }\n});\n\n// Add a platform to hold the player\nadd([\n rect(width(), 48),\n outline(4),\n area(),\n pos(0, height() - 48),\n // Give objects a body() component if you don't want other solid objects pass through\n body({ isStatic: true }),\n]);\n\nadd([\n text(\"Press space key\", { width: width() / 2 }),\n pos(12, 12),\n]);\n\n// Check out https://kaplayjs.com/doc/BodyComp for everything body() provides\n", + "index": "30" + }, + { + "name": "hover", + "code": "// Differeces between onHover and onHoverUpdate\n\nkaplay({\n // Use logMax to see more messages on debug.log()\n logMax: 5,\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nadd([\n text(\"onHover()\\nonHoverEnd()\"),\n pos(80, 80),\n]);\n\nadd([\n text(\"onHoverUpdate()\"),\n pos(340, 80),\n]);\n\nconst redBean = add([\n sprite(\"bean\"),\n color(RED),\n pos(130, 180),\n anchor(\"center\"),\n area(),\n]);\n\nconst blueBean = add([\n sprite(\"bean\"),\n color(BLUE),\n pos(380, 180),\n anchor(\"center\"),\n area(),\n]);\n\n// Only runs once when bean is hovered, and when bean is unhovered\nredBean.onHover(() => {\n debug.log(\"red bean hovered\");\n\n redBean.color = GREEN;\n});\nredBean.onHoverEnd(() => {\n debug.log(\"red bean unhovered\");\n\n redBean.color = RED;\n});\n\n// Runs every frame when blue bean is hovered\nblueBean.onHoverUpdate(() => {\n const t = time() * 10;\n blueBean.color = rgb(\n wave(0, 255, t),\n wave(0, 255, t + 2),\n wave(0, 255, t + 4),\n );\n\n debug.log(\"blue bean on hover\");\n});\n", + "index": "31" + }, + { + "name": "inspectExample", + "code": "kaplay();\n\n// # will delete this file when changes get merged/declined i don't intend this to be an actual example\nfunction customComponent() {\n return {\n id: \"compy\",\n customing: true,\n // if it didn't have an inspect function it would appear as \"compy\"\n inspect() {\n return `customing: ${this.customing}`;\n },\n };\n}\n\nloadBean();\n\nlet bean = add([\n sprite(\"bean\"),\n area(),\n opacity(),\n pos(center()),\n scale(4),\n customComponent(),\n]);\n\nbean.onClick(() => {\n bean.customing = !bean.customing;\n});\n\n// # check sprite.ts and the other components in the object\n// now the inspect function says eg: `sprite: ${src}` instead of `${src}`\n", + "index": "32" + }, + { + "name": "kaboom", + "code": "// You can still use kaboom() instead of kaplay()!\nkaboom();\n\naddKaboom(center());\n\nonKeyPress(() => addKaboom(mousePos()));\nonMouseMove(() => addKaboom(mousePos()));\n", + "index": "33" + }, + { + "name": "largeTexture", + "code": "kaplay();\n\nlet cameraPosition = camPos();\nlet cameraScale = 1;\n\n// Loads a random 2500px image\nloadSprite(\"bigyoshi\", \"/examples/sprites/YOSHI.png\");\n\nadd([\n sprite(\"bigyoshi\"),\n]);\n\n// Adds a label\nconst label = make([\n text(\"Click and drag the mouse, scroll the wheel\"),\n]);\n\nadd([\n rect(label.width, label.height),\n color(0, 0, 0),\n]);\n\nadd(label);\n\n// Mouse handling\nonUpdate(() => {\n if (isMouseDown(\"left\") && isMouseMoved()) {\n cameraPosition = cameraPosition.sub(\n mouseDeltaPos().scale(1 / cameraScale),\n );\n camPos(cameraPosition);\n }\n});\n\nonScroll((delta) => {\n cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y));\n camScale(cameraScale);\n});\n", + "index": "34" + }, + { + "name": "layer", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Create a parent node that won't be affected by camera (fixed) and will be drawn on top (z of 100)\nconst ui = add([\n fixed(),\n z(100),\n]);\n\n// This will be on top, because the parent node has z(100)\nui.add([\n sprite(\"bean\"),\n scale(5),\n color(0, 0, 255),\n]);\n\nadd([\n sprite(\"bean\"),\n pos(100, 100),\n scale(5),\n]);\n", + "index": "35" + }, + { + "name": "layers", + "code": "kaplay();\n\nlayers([\"bg\", \"game\", \"ui\"], \"game\");\n\n// bg layer\nadd([\n rect(width(), height()),\n layer(\"bg\"),\n color(rgb(64, 128, 255)),\n // opacity(0.5)\n]).add([text(\"BG\")]);\n\n// game layer explicit\nadd([\n pos(width() / 5, height() / 5),\n rect(width() / 3, height() / 3),\n layer(\"game\"),\n color(rgb(255, 128, 64)),\n]).add([text(\"GAME\")]);\n\n// game layer implicit\nadd([\n pos(3 * width() / 5, 3 * height() / 5),\n rect(width() / 3, height() / 3),\n color(rgb(255, 128, 64)),\n]).add([pos(width() / 3, height() / 3), text(\"GAME\"), anchor(\"botright\")]);\n\n// ui layer\nadd([\n pos(center()),\n rect(width() / 2, height() / 2),\n anchor(\"center\"),\n color(rgb(64, 255, 128)),\n]).add([text(\"UI\"), anchor(\"center\")]);\n", + "index": "36" + }, + { + "name": "level", + "code": "// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst SPEED = 480;\n\nsetGravity(2400);\n\nconst level = addLevel([\n // Design the level layout with symbols\n \"@ ^ $$\",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100, 200),\n // Define what each symbol means (in components)\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\n// Back to the original position if hit a \"danger\" item\nplayer.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n});\n\n// Eat the coin!\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n});\n", + "index": "37" + }, + { + "name": "linecap", + "code": "kaplay();\n\nonDraw(() => {\n // No line cap\n drawLines({\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n cap: \"none\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 200),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n cap: \"none\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 200),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n cap: \"none\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 50),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 200),\n radius: 4,\n color: RED,\n });\n\n // Square line cap\n drawLines({\n pos: vec2(0, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 450),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 450),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 250),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n cap: \"square\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 300),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 450),\n radius: 4,\n color: RED,\n });\n\n // Round line cap\n drawLines({\n pos: vec2(0, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"bevel\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(50, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(50, 700),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(200, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"round\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(250, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(250, 700),\n radius: 4,\n color: RED,\n });\n\n drawLines({\n pos: vec2(400, 500),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n ],\n join: \"miter\",\n cap: \"round\",\n width: 20,\n });\n drawCircle({\n pos: vec2(450, 550),\n radius: 4,\n color: RED,\n });\n drawCircle({\n pos: vec2(450, 700),\n radius: 4,\n color: RED,\n });\n});\n", + "index": "38" + }, + { + "name": "linejoin", + "code": "kaplay();\n\nonDraw(() => {\n // Rectangles\n drawLines({\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"bevel\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(200, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"round\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(400, 0),\n pts: [\n vec2(50, 50),\n vec2(200, 50),\n vec2(200, 200),\n vec2(50, 200),\n vec2(50, 50),\n ],\n join: \"miter\",\n width: 20,\n });\n\n // Parallelograms\n drawLines({\n pos: vec2(0, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"bevel\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(200, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"round\",\n width: 20,\n });\n\n drawLines({\n pos: vec2(400, 200),\n pts: [\n vec2(60, 50),\n vec2(210, 50),\n vec2(170, 200),\n vec2(20, 200),\n vec2(60, 50),\n ],\n join: \"miter\",\n width: 20,\n });\n});\n\nadd([\n pos(0, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 1, \"bevel\"),\n]);\n\nadd([\n pos(200, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 1, \"round\"),\n]);\n\nadd([\n pos(400, 400),\n polygon([vec2(125, 50), vec2(200, 200), vec2(50, 200)]),\n outline(20, RED, 0.5, \"miter\"),\n]);\n", + "index": "39" + }, + { + "name": "loader", + "code": "// Customizing the asset loader\n\nkaplay({\n // Optionally turn off loading screen entirely\n // Unloaded assets simply won't be drawn\n // loadingScreen: false,\n});\n\nlet spr = null;\n\n// Every loadXXX() function returns a Asset where you can customize the error handling (by default it'll stop the game and log on screen), or deal with the raw asset data yourself instead of using a name.\nloadSprite(\"bean\", \"/sprites/bean.png\").onError(() => {\n alert(\"oh no we failed to load bean\");\n}).onLoad((data) => {\n // The promise resolves to the raw sprite data\n spr = data;\n});\n\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// load() adds a Promise under kaboom's management, which affects loadProgress()\n// Here we intentionally stall the loading by 1sec to see the loading screen\nload(\n new Promise((res) => {\n // wait() won't work here because timers are not run during loading so we use setTimeout\n setTimeout(() => {\n res();\n }, 1000);\n }),\n);\n\n// make loader wait for a fetch() call\nload(fetch(\"https://kaboomjs.com/\"));\n\n// You can also use the handle returned by loadXXX() as the resource handle\nconst bugSound = loadSound(\"bug\", \"/examples/sounds/bug.mp3\");\n\nvolume(0.1);\n\nonKeyPress(\"space\", () => play(bugSound));\n\n// Custom loading screen\n// Runs the callback every frame during loading\nonLoading((progress) => {\n // Black background\n drawRect({\n width: width(),\n height: height(),\n color: rgb(0, 0, 0),\n });\n\n // A pie representing current load progress\n drawCircle({\n pos: center(),\n radius: 32,\n end: map(progress, 0, 1, 0, 360),\n });\n\n drawText({\n text: \"loading\" + \".\".repeat(wave(1, 4, time() * 12)),\n font: \"monospace\",\n size: 24,\n anchor: \"center\",\n pos: center().add(0, 70),\n });\n});\n\nonDraw(() => {\n if (spr) {\n drawSprite({\n // You can pass raw sprite data here instead of the name\n sprite: spr,\n });\n }\n});\n", + "index": "40" + }, + { + "name": "maze", + "code": "kaplay({\n scale: 0.5,\n background: [0, 0, 0],\n});\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"steel\", \"sprites/steel.png\");\n\nconst TILE_WIDTH = 64;\nconst TILE_HEIGHT = TILE_WIDTH;\n\nfunction createMazeMap(width, height) {\n const size = width * height;\n function getUnvisitedNeighbours(map, index) {\n const n = [];\n const x = Math.floor(index / width);\n if (x > 1 && map[index - 2] === 2) n.push(index - 2);\n if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);\n if (index >= 2 * width && map[index - 2 * width] === 2) {\n n.push(index - 2 * width);\n }\n if (index < size - 2 * width && map[index + 2 * width] === 2) {\n n.push(index + 2 * width);\n }\n return n;\n }\n const map = new Array(size).fill(1, 0, size);\n map.forEach((_, index) => {\n const x = Math.floor(index / width);\n const y = Math.floor(index % width);\n if ((x & 1) === 1 && (y & 1) === 1) {\n map[index] = 2;\n }\n });\n\n const stack = [];\n const startX = Math.floor(Math.random() * (width - 1)) | 1;\n const startY = Math.floor(Math.random() * (height - 1)) | 1;\n const start = startX + startY * width;\n map[start] = 0;\n stack.push(start);\n while (stack.length) {\n const index = stack.pop();\n const neighbours = getUnvisitedNeighbours(map, index);\n if (neighbours.length > 0) {\n stack.push(index);\n const neighbour =\n neighbours[Math.floor(neighbours.length * Math.random())];\n const between = (index + neighbour) / 2;\n map[neighbour] = 0;\n map[between] = 0;\n stack.push(neighbour);\n }\n }\n return map;\n}\n\nfunction createMazeLevelMap(width, height, options) {\n const symbols = options?.symbols || {};\n const map = createMazeMap(width, height);\n const space = symbols[\" \"] || \" \";\n const fence = symbols[\"#\"] || \"#\";\n const detail = [\n space,\n symbols[\"╸\"] || \"╸\", // 1\n symbols[\"╹\"] || \"╹\", // 2\n symbols[\"┛\"] || \"┛\", // 3\n symbols[\"╺\"] || \"╺\", // 4\n symbols[\"━\"] || \"━\", // 5\n symbols[\"┗\"] || \"┗\", // 6\n symbols[\"┻\"] || \"┻\", // 7\n symbols[\"╻\"] || \"╻\", // 8\n symbols[\"┓\"] || \"┓\", // 9\n symbols[\"┃\"] || \"┃\", // a\n symbols[\"┫\"] || \"┫\", // b\n symbols[\"┏\"] || \"┏\", // c\n symbols[\"┳\"] || \"┳\", // d\n symbols[\"┣\"] || \"┣\", // e\n symbols[\"╋ \"] || \"╋ \", // f\n ];\n const symbolMap = options?.detailed\n ? map.map((s, index) => {\n if (s === 0) return space;\n const x = Math.floor(index % width);\n const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;\n const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;\n const topWall = index >= width && map[index - width] == 1 ? 2 : 0;\n const bottomWall =\n index < height * width - width && map[index + width] == 1\n ? 8\n : 0;\n return detail[leftWall | rightWall | topWall | bottomWall];\n })\n : map.map((s) => {\n return s == 1 ? fence : space;\n });\n const levelMap = [];\n for (let i = 0; i < height; i++) {\n levelMap.push(symbolMap.slice(i * width, i * width + width).join(\"\"));\n }\n return levelMap;\n}\n\nconst level = addLevel(\n createMazeLevelMap(15, 15, {}),\n {\n tileWidth: TILE_WIDTH,\n tileHeight: TILE_HEIGHT,\n tiles: {\n \"#\": () => [\n sprite(\"steel\"),\n tile({ isObstacle: true }),\n ],\n },\n },\n);\n\nconst bean = level.spawn(\n [\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(32, 32),\n tile(),\n agent({ speed: 640, allowDiagonals: true }),\n \"bean\",\n ],\n 1,\n 1,\n);\n\nonClick(() => {\n const pos = mousePos();\n bean.setTarget(vec2(\n Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,\n Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,\n ));\n});\n", + "index": "41" + }, + { + "name": "mazeRaycastedLight", + "code": "kaplay({\n scale: 0.5,\n background: [0, 0, 0],\n});\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"steel\", \"sprites/steel.png\");\n\nconst TILE_WIDTH = 64;\nconst TILE_HEIGHT = TILE_WIDTH;\n\nfunction createMazeMap(width, height) {\n const size = width * height;\n function getUnvisitedNeighbours(map, index) {\n const n = [];\n const x = Math.floor(index / width);\n if (x > 1 && map[index - 2] === 2) n.push(index - 2);\n if (x < width - 2 && map[index + 2] === 2) n.push(index + 2);\n if (index >= 2 * width && map[index - 2 * width] === 2) {\n n.push(index - 2 * width);\n }\n if (index < size - 2 * width && map[index + 2 * width] === 2) {\n n.push(index + 2 * width);\n }\n return n;\n }\n const map = new Array(size).fill(1, 0, size);\n map.forEach((_, index) => {\n const x = Math.floor(index / width);\n const y = Math.floor(index % width);\n if ((x & 1) === 1 && (y & 1) === 1) {\n map[index] = 2;\n }\n });\n\n const stack = [];\n const startX = Math.floor(Math.random() * (width - 1)) | 1;\n const startY = Math.floor(Math.random() * (height - 1)) | 1;\n const start = startX + startY * width;\n map[start] = 0;\n stack.push(start);\n while (stack.length) {\n const index = stack.pop();\n const neighbours = getUnvisitedNeighbours(map, index);\n if (neighbours.length > 0) {\n stack.push(index);\n const neighbour =\n neighbours[Math.floor(neighbours.length * Math.random())];\n const between = (index + neighbour) / 2;\n map[neighbour] = 0;\n map[between] = 0;\n stack.push(neighbour);\n }\n }\n return map;\n}\n\nfunction createMazeLevelMap(width, height, options) {\n const symbols = options?.symbols || {};\n const map = createMazeMap(width, height);\n const space = symbols[\" \"] || \" \";\n const fence = symbols[\"#\"] || \"#\";\n const detail = [\n space,\n symbols[\"╸\"] || \"╸\", // 1\n symbols[\"╹\"] || \"╹\", // 2\n symbols[\"┛\"] || \"┛\", // 3\n symbols[\"╺\"] || \"╺\", // 4\n symbols[\"━\"] || \"━\", // 5\n symbols[\"┗\"] || \"┗\", // 6\n symbols[\"┻\"] || \"┻\", // 7\n symbols[\"╻\"] || \"╻\", // 8\n symbols[\"┓\"] || \"┓\", // 9\n symbols[\"┃\"] || \"┃\", // a\n symbols[\"┫\"] || \"┫\", // b\n symbols[\"┏\"] || \"┏\", // c\n symbols[\"┳\"] || \"┳\", // d\n symbols[\"┣\"] || \"┣\", // e\n symbols[\"╋ \"] || \"╋ \", // f\n ];\n const symbolMap = options?.detailed\n ? map.map((s, index) => {\n if (s === 0) return space;\n const x = Math.floor(index % width);\n const leftWall = x > 0 && map[index - 1] == 1 ? 1 : 0;\n const rightWall = x < width - 1 && map[index + 1] == 1 ? 4 : 0;\n const topWall = index >= width && map[index - width] == 1 ? 2 : 0;\n const bottomWall =\n index < height * width - width && map[index + width] == 1\n ? 8\n : 0;\n return detail[leftWall | rightWall | topWall | bottomWall];\n })\n : map.map((s) => {\n return s == 1 ? fence : space;\n });\n const levelMap = [];\n for (let i = 0; i < height; i++) {\n levelMap.push(symbolMap.slice(i * width, i * width + width).join(\"\"));\n }\n return levelMap;\n}\n\nconst level = addLevel(\n createMazeLevelMap(15, 15, {}),\n {\n pos: vec2(100, 100),\n tileWidth: TILE_WIDTH,\n tileHeight: TILE_HEIGHT,\n tiles: {\n \"#\": () => [\n sprite(\"steel\"),\n tile({ isObstacle: true }),\n ],\n },\n },\n);\n\nconst bean = level.spawn(\n [\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(32, 32),\n tile(),\n agent({ speed: 640, allowDiagonals: true }),\n \"bean\",\n ],\n 1,\n 1,\n);\n\nonClick(() => {\n const pos = level.fromScreen(mousePos());\n bean.setTarget(vec2(\n Math.floor(pos.x / TILE_WIDTH) * TILE_WIDTH + TILE_WIDTH / 2,\n Math.floor(pos.y / TILE_HEIGHT) * TILE_HEIGHT + TILE_HEIGHT / 2,\n ));\n});\n\nonUpdate(() => {\n const pts = [bean.pos];\n // This is overkill, since you theoretically only need to shoot rays to grid positions\n for (let i = 0; i < 360; i += 1) {\n const hit = level.raycast(bean.pos, Vec2.fromAngle(i));\n pts.push(hit.point);\n }\n pts.push(pts[1]);\n drawPolygon({\n pos: vec2(100, 100),\n pts: pts,\n color: rgb(255, 255, 100),\n });\n});\n", + "index": "42" + }, + { + "name": "movement", + "code": "// Input handling and basic player movement\n\n// Start kaboom\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Define player movement speed (pixels per second)\nconst SPEED = 320;\n\n// Add player game object\nconst player = add([\n sprite(\"bean\"),\n // center() returns the center point vec2(width() / 2, height() / 2)\n pos(center()),\n]);\n\n// onKeyDown() registers an event that runs every frame as long as user is holding a certain key\nonKeyDown(\"left\", () => {\n // .move() is provided by pos() component, move by pixels per second\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\n// onClick() registers an event that runs once when left mouse is clicked\nonClick(() => {\n // .moveTo() is provided by pos() component, changes the position\n player.moveTo(mousePos());\n});\n\nadd([\n // text() component is similar to sprite() but renders text\n text(\"Press arrow keys\", { width: width() / 2 }),\n pos(12, 12),\n]);\n", + "index": "43" + }, + { + "name": "multigamepad", + "code": "kaplay();\nsetGravity(2400);\nsetBackground(0, 0, 0);\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nconst playerColors = [\n rgb(252, 53, 43),\n rgb(0, 255, 0),\n rgb(43, 71, 252),\n rgb(255, 255, 0),\n rgb(255, 0, 255),\n];\n\nlet playerCount = 0;\n\nfunction addPlayer(gamepad) {\n const player = add([\n pos(center()),\n anchor(\"center\"),\n sprite(\"bean\"),\n color(playerColors[playerCount]),\n area(),\n body(),\n doubleJump(),\n ]);\n\n playerCount++;\n\n onUpdate(() => {\n const leftStick = gamepad.getStick(\"left\");\n\n if (gamepad.isPressed(\"south\")) {\n player.doubleJump();\n }\n\n if (leftStick.x !== 0) {\n player.move(leftStick.x * 400, 0);\n }\n });\n}\n\n// platform\nadd([\n pos(0, height()),\n anchor(\"botleft\"),\n rect(width(), 140),\n area(),\n body({ isStatic: true }),\n]);\n\n// add players on every gamepad connect\nonGamepadConnect((gamepad) => {\n addPlayer(gamepad);\n});\n", + "index": "44" + }, + { + "name": "out", + "code": "// detect if obj is out of screen\n\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// custom comp\nfunction handleout() {\n return {\n id: \"handleout\",\n require: [\"pos\"],\n update() {\n const spos = this.screenPos();\n if (\n spos.x < 0\n || spos.x > width()\n || spos.y < 0\n || spos.y > height()\n ) {\n // triggers a custom event when out\n this.trigger(\"out\");\n }\n },\n };\n}\n\nconst SPEED = 640;\n\nfunction shoot() {\n const center = vec2(width() / 2, height() / 2);\n const mpos = mousePos();\n add([\n pos(center),\n sprite(\"bean\"),\n anchor(\"center\"),\n handleout(),\n \"bean\",\n { dir: mpos.sub(center).unit() },\n ]);\n}\n\nonKeyPress(\"space\", shoot);\nonClick(shoot);\n\nonUpdate(\"bean\", (m) => {\n m.move(m.dir.scale(SPEED));\n});\n\n// binds a custom event \"out\" to tag group \"bean\"\non(\"out\", \"bean\", (m) => {\n addKaboom(m.pos);\n destroy(m);\n});\n", + "index": "45" + }, + { + "name": "overlap", + "code": "kaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawRect({\n pos: vec2(-1, -1),\n width: 3,\n height: 3,\n color: this.color,\n });\n },\n getShape() {\n // This would be point if we had a real class for it\n return new Rect(vec2(-1, -1).add(this.pos), 3, 3);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nlet selection;\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.getShape().contains(pos));\n selection = pickList[pickList.length - 1];\n});\n\nonMouseMove((pos, delta) => {\n if (selection) {\n selection.moveBy(delta);\n }\n});\n\nonMouseRelease(() => {\n selection = null;\n});\n\nonDraw(() => {\n if (selection) {\n const rect = selection.getShape().bbox();\n drawRect({\n pos: rect.pos,\n width: rect.width,\n height: rect.height,\n outline: {\n width: 1,\n color: YELLOW,\n },\n fill: false,\n });\n }\n});\n", + "index": "46" + }, + { + "name": "particle", + "code": "// Particle spawning\n\nkaplay();\n\nconst sprites = [\n \"apple\",\n \"heart\",\n \"coin\",\n \"meat\",\n \"lightening\",\n];\n\nsprites.forEach((spr) => {\n loadSprite(spr, `/sprites/${spr}.png`);\n});\n\nsetGravity(800);\n\n// Spawn one particle every 0.1 second\nloop(0.1, () => {\n // TODO: they are resolving collision with each other for some reason\n // Compose particle properties with components\n const item = add([\n pos(mousePos()),\n sprite(choose(sprites)),\n anchor(\"center\"),\n scale(rand(0.5, 1)),\n area({ collisionIgnore: [\"particle\"] }),\n body(),\n lifespan(1, { fade: 0.5 }),\n opacity(1),\n move(choose([LEFT, RIGHT]), rand(60, 240)),\n \"particle\",\n ]);\n\n item.onCollide(\"particle\", (p) => {\n console.log(\"dea\");\n });\n\n item.jump(rand(320, 640));\n});\n", + "index": "47" + }, + { + "name": "pauseMenu", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"wooosh\", \"/examples/sounds/wooosh.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\n\n// define gravity\nsetGravity(3200);\n\nsetBackground(141, 183, 255);\n\nscene(\"game\", () => {\n const game = add([\n timer(),\n ]);\n\n const PIPE_OPEN = 240;\n const PIPE_MIN = 60;\n const JUMP_FORCE = 800;\n const SPEED = 320;\n const CEILING = -60;\n\n // a game object consists of a list of components and tags\n const bean = game.add([\n // sprite() means it's drawn with a sprite of name \"bean\" (defined above in 'loadSprite')\n sprite(\"bean\"),\n // give it a position\n pos(width() / 4, 0),\n // give it a collider\n area(),\n // body component enables it to fall and jump in a gravity world\n body(),\n ]);\n\n // check for fall death\n bean.onUpdate(() => {\n if (bean.pos.y >= height() || bean.pos.y <= CEILING) {\n // switch to \"lose\" scene\n go(\"lose\", score);\n }\n });\n\n // jump\n onKeyPress(\"space\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n onGamepadButtonPress(\"south\", () => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n // mobile\n onClick(() => {\n bean.jump(JUMP_FORCE);\n play(\"wooosh\");\n });\n\n function spawnPipe() {\n // calculate pipe positions\n const h1 = rand(PIPE_MIN, height() - PIPE_MIN - PIPE_OPEN);\n const h2 = height() - h1 - PIPE_OPEN;\n\n game.add([\n pos(width(), 0),\n rect(64, h1),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n ]);\n\n game.add([\n pos(width(), h1 + PIPE_OPEN),\n rect(64, h2),\n color(0, 127, 255),\n outline(4),\n area(),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n // give it tags to easier define behaviors see below\n \"pipe\",\n // raw obj just assigns every field to the game obj\n { passed: false },\n ]);\n }\n\n // callback when bean onCollide with objects with tag \"pipe\"\n bean.onCollide(\"pipe\", () => {\n go(\"lose\", score);\n play(\"hit\");\n addKaboom(bean.pos);\n });\n\n // per frame event for all objects with tag 'pipe'\n onUpdate(\"pipe\", (p) => {\n // check if bean passed the pipe\n if (p.pos.x + p.width <= bean.pos.x && p.passed === false) {\n addScore();\n p.passed = true;\n }\n });\n\n // spawn a pipe every 1 sec\n game.loop(1, () => {\n spawnPipe();\n });\n\n let score = 0;\n\n // display score\n const scoreLabel = game.add([\n text(score),\n anchor(\"center\"),\n pos(width() / 2, 80),\n fixed(),\n z(100),\n ]);\n\n function addScore() {\n score++;\n scoreLabel.text = score;\n play(\"score\");\n }\n\n let curTween = null;\n\n onKeyPress(\"p\", () => {\n game.paused = !game.paused;\n if (curTween) curTween.cancel();\n curTween = tween(\n pauseMenu.pos,\n game.paused ? center() : center().add(0, 700),\n 1,\n (p) => pauseMenu.pos = p,\n easings.easeOutElastic,\n );\n if (game.paused) {\n pauseMenu.hidden = false;\n pauseMenu.paused = false;\n }\n else {\n curTween.onEnd(() => {\n pauseMenu.hidden = true;\n pauseMenu.paused = true;\n });\n }\n });\n\n const pauseMenu = add([\n rect(300, 400),\n color(255, 255, 255),\n outline(4),\n anchor(\"center\"),\n pos(center().add(0, 700)),\n ]);\n\n pauseMenu.hidden = true;\n pauseMenu.paused = true;\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 108),\n scale(3),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", + "index": "48" + }, + { + "name": "physics", + "code": "kaplay();\n\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"bag\", \"sprites/bag.png\");\n\nsetGravity(300);\n\nconst trajectoryText = add([\n pos(20, 20),\n text(`0`),\n]);\n\nfunction ballistics(pos, vel, t) {\n return pos.add(vel.scale(t)).add(\n vec2(0, 1).scale(getGravity() * t * t * 0.5),\n );\n}\n\nlet y;\n\nonDraw(() => {\n drawCurve(t => ballistics(vec2(50, 100), vec2(200, -100), t * 2), {\n width: 2,\n color: RED,\n });\n});\n\nonClick(() => {\n const startTime = time();\n let results = [];\n const bean = add([\n sprite(\"bean\"),\n anchor(\"center\"),\n pos(50, 100),\n body(),\n offscreen({ destroy: true }),\n {\n draw() {\n drawLine({\n p1: vec2(-40, 0),\n p2: vec2(40, 0),\n width: 2,\n color: GREEN,\n });\n drawLine({\n p1: vec2(0, -40),\n p2: vec2(0, 40),\n width: 2,\n color: GREEN,\n });\n },\n update() {\n const t = time() - startTime;\n if (t >= 2) return;\n results.push([\n t,\n this.pos.y,\n ballistics(vec2(50, 100), vec2(200, -100), t).y,\n ]);\n },\n destroy() {\n const a = results.map(d =>\n Math.sqrt((d[1] - d[2]) * (d[1] - d[2]))\n ).reduce((s, v) => s + v, 0) / results.length;\n trajectoryText.text = `${a.toFixed(2)}`;\n },\n },\n ]);\n bean.vel = vec2(200, -100);\n});\n\nfunction highestPoint(pos, vel) {\n return pos.y - vel.y * vel.y / (2 * getGravity());\n}\n\nconst heightGoal = highestPoint(vec2(50, 300), vec2(0, -200));\nlet heightResult = 0;\n\nonDraw(() => {\n y = highestPoint(vec2(50, 300), vec2(0, -200));\n drawLine({\n p1: vec2(10, y),\n p2: vec2(90, y),\n width: 2,\n color: RED,\n });\n});\n\nconst heightText = add([\n pos(100, heightGoal),\n text(`0%`),\n]);\n\nonUpdate(() => {\n heightText.text =\n `${((100 * (heightResult - heightGoal) / heightGoal).toFixed(2))}%`;\n});\n\nonClick(() => {\n y = highestPoint(vec2(50, 300), vec2(0, -200));\n const bean = add([\n sprite(\"bag\"),\n anchor(\"center\"),\n pos(50, 300),\n body(),\n offscreen({ destroy: true }),\n {\n draw() {\n drawLine({\n p1: vec2(-40, 0),\n p2: vec2(40, 0),\n width: 2,\n color: GREEN,\n });\n },\n update() {\n if (this.vel.y <= 0) {\n heightResult = this.pos.y;\n }\n },\n },\n ]);\n bean.vel = vec2(0, -200);\n});\n", + "index": "49" + }, + { + "name": "physicsfactory", + "code": "kaplay();\n\nsetGravity(300);\n\n// Conveyor belt moving right\nadd([\n pos(100, 300),\n rect(200, 20),\n area(),\n body({ isStatic: true }),\n surfaceEffector({ speed: 20 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(2, 2),\n vec2(12, 10),\n vec2(2, 18),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Conveyor belt moving left\nadd([\n pos(80, 400),\n rect(250, 20),\n area(),\n body({ isStatic: true }),\n surfaceEffector({ speed: -20 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(12, 2),\n vec2(2, 10),\n vec2(12, 18),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Windtunnel moving up\nadd([\n pos(20, 150),\n rect(50, 300),\n area(),\n areaEffector({ forceAngle: -90, forceMagnitude: 150 }),\n {\n draw() {\n drawPolygon({\n pts: [\n vec2(25, 2),\n vec2(48, 12),\n vec2(2, 12),\n ],\n color: RED,\n });\n },\n },\n]);\n\n// Magnet\nadd([\n pos(85, 50),\n rect(90, 90),\n anchor(\"center\"),\n area(),\n pointEffector({ forceMagnitude: 300 }),\n {\n draw() {\n drawCircle({\n pos: vec2(0, 0),\n radius: 5,\n color: RED,\n });\n },\n },\n]);\n\n// Continouous boxes\nloop(5, () => {\n add([\n pos(100, 100),\n rect(20, 20),\n color(RED),\n area(),\n body(),\n offscreen({ destroy: true, distance: 10 }),\n ]);\n});\n\n// A box\nadd([\n pos(500, 100),\n rect(20, 20),\n color(RED),\n area(),\n body({ mass: 10 }),\n // offscreen({ destroy: true }),\n]);\n\n// Water\nadd([\n pos(400, 200),\n rect(200, 100),\n color(BLUE),\n opacity(0.5),\n area(),\n buoyancyEffector({ surfaceLevel: 200, density: 6 }),\n]);\n", + "index": "50" + }, + { + "name": "platformer", + "code": "kaplay({\n background: [141, 183, 255],\n});\n\n// load assets\nloadSprite(\"bigyoshi\", \"/examples/sprites/YOSHI.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"bag\", \"/sprites/bag.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\nloadSprite(\"prize\", \"/sprites/jumpy.png\");\nloadSprite(\"apple\", \"/sprites/apple.png\");\nloadSprite(\"portal\", \"/sprites/portal.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSound(\"coin\", \"/examples/sounds/score.mp3\");\nloadSound(\"powerup\", \"/examples/sounds/powerup.mp3\");\nloadSound(\"blip\", \"/examples/sounds/blip.mp3\");\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"portal\", \"/examples/sounds/portal.mp3\");\n\nsetGravity(3200);\n\n// custom component controlling enemy patrol movement\nfunction patrol(speed = 60, dir = 1) {\n return {\n id: \"patrol\",\n require: [\"pos\", \"area\"],\n add() {\n this.on(\"collide\", (obj, col) => {\n if (col.isLeft() || col.isRight()) {\n dir = -dir;\n }\n });\n },\n update() {\n this.move(speed * dir, 0);\n },\n };\n}\n\n// custom component that makes stuff grow big\nfunction big() {\n let timer = 0;\n let isBig = false;\n let destScale = 1;\n return {\n // component id / name\n id: \"big\",\n // it requires the scale component\n require: [\"scale\"],\n // this runs every frame\n update() {\n if (isBig) {\n timer -= dt();\n if (timer <= 0) {\n this.smallify();\n }\n }\n this.scale = this.scale.lerp(vec2(destScale), dt() * 6);\n },\n // custom methods\n isBig() {\n return isBig;\n },\n smallify() {\n destScale = 1;\n timer = 0;\n isBig = false;\n },\n biggify(time) {\n destScale = 2;\n timer = time;\n isBig = true;\n },\n };\n}\n\n// define some constants\nconst JUMP_FORCE = 1320;\nconst MOVE_SPEED = 480;\nconst FALL_DEATH = 2400;\n\nconst LEVELS = [\n [\n \" 0 \",\n \" -- \",\n \" $$ \",\n \" % === \",\n \" \",\n \" ^^ > = @\",\n \"============\",\n ],\n [\n \" $\",\n \" $\",\n \" $\",\n \" $\",\n \" $\",\n \" $$ = $\",\n \" % ==== = $\",\n \" = $\",\n \" = \",\n \" ^^ = > = @\",\n \"===========================\",\n ],\n [\n \" $ $ $ $ $\",\n \" $ $ $ $ $\",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" ^^^^>^^^^>^^^^>^^^^>^^^^^@\",\n \"===========================\",\n ],\n];\n\n// define what each symbol means in the level graph\nconst levelConf = {\n tileWidth: 64,\n tileHeight: 64,\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"platform\",\n ],\n \"-\": () => [\n sprite(\"steel\"),\n area(),\n body({ isStatic: true }),\n offscreen({ hide: true }),\n anchor(\"bot\"),\n ],\n \"0\": () => [\n sprite(\"bag\"),\n area(),\n body({ isStatic: true }),\n offscreen({ hide: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n pos(0, -9),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"coin\",\n ],\n \"%\": () => [\n sprite(\"prize\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"prize\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n offscreen({ hide: true }),\n \"danger\",\n ],\n \"#\": () => [\n sprite(\"apple\"),\n area(),\n anchor(\"bot\"),\n body(),\n offscreen({ hide: true }),\n \"apple\",\n ],\n \">\": () => [\n sprite(\"ghosty\"),\n area(),\n anchor(\"bot\"),\n body(),\n patrol(),\n offscreen({ hide: true }),\n \"enemy\",\n ],\n \"@\": () => [\n sprite(\"portal\"),\n area({ scale: 0.5 }),\n anchor(\"bot\"),\n pos(0, -12),\n offscreen({ hide: true }),\n \"portal\",\n ],\n },\n};\n\nscene(\"game\", ({ levelId, coins } = { levelId: 0, coins: 0 }) => {\n // add level to scene\n const level = addLevel(LEVELS[levelId ?? 0], levelConf);\n\n // define player object\n const player = add([\n sprite(\"bean\"),\n pos(0, 0),\n area(),\n scale(1),\n // makes it fall to gravity and jumpable\n body(),\n // the custom component we defined above\n big(),\n anchor(\"bot\"),\n ]);\n\n // action() runs every frame\n player.onUpdate(() => {\n // center camera to player\n camPos(player.pos);\n // check fall death\n if (player.pos.y >= FALL_DEATH) {\n go(\"lose\");\n }\n });\n\n player.onBeforePhysicsResolve((collision) => {\n if (collision.target.is([\"platform\", \"soft\"]) && player.isJumping()) {\n collision.preventResolution();\n }\n });\n\n player.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.pos);\n });\n\n // if player onCollide with any obj with \"danger\" tag, lose\n player.onCollide(\"danger\", () => {\n go(\"lose\");\n play(\"hit\");\n });\n\n player.onCollide(\"portal\", () => {\n play(\"portal\");\n if (levelId + 1 < LEVELS.length) {\n go(\"game\", {\n levelId: levelId + 1,\n coins: coins,\n });\n }\n else {\n go(\"win\");\n }\n });\n\n player.onGround((l) => {\n if (l.is(\"enemy\")) {\n player.jump(JUMP_FORCE * 1.5);\n destroy(l);\n addKaboom(player.pos);\n play(\"powerup\");\n }\n });\n\n player.onCollide(\"enemy\", (e, col) => {\n // if it's not from the top, die\n if (!col.isBottom()) {\n go(\"lose\");\n play(\"hit\");\n }\n });\n\n let hasApple = false;\n\n // grow an apple if player's head bumps into an obj with \"prize\" tag\n player.onHeadbutt((obj) => {\n if (obj.is(\"prize\") && !hasApple) {\n const apple = level.spawn(\"#\", obj.tilePos.sub(0, 1));\n apple.jump();\n hasApple = true;\n play(\"blip\");\n }\n });\n\n // player grows big onCollide with an \"apple\" obj\n player.onCollide(\"apple\", (a) => {\n destroy(a);\n // as we defined in the big() component\n player.biggify(3);\n hasApple = false;\n play(\"powerup\");\n });\n\n let coinPitch = 0;\n\n onUpdate(() => {\n if (coinPitch > 0) {\n coinPitch = Math.max(0, coinPitch - dt() * 100);\n }\n });\n\n player.onCollide(\"coin\", (c) => {\n destroy(c);\n play(\"coin\", {\n detune: coinPitch,\n });\n coinPitch += 100;\n coins += 1;\n coinsLabel.text = coins;\n });\n\n const coinsLabel = add([\n text(coins),\n pos(24, 24),\n fixed(),\n ]);\n\n function jump() {\n // these 2 functions are provided by body() component\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n }\n\n // jump with space\n onKeyPress(\"space\", jump);\n\n onKeyDown(\"left\", () => {\n player.move(-MOVE_SPEED, 0);\n });\n\n onKeyDown(\"right\", () => {\n player.move(MOVE_SPEED, 0);\n });\n\n onKeyPress(\"down\", () => {\n player.weight = 3;\n });\n\n onKeyRelease(\"down\", () => {\n player.weight = 1;\n });\n\n onGamepadButtonPress(\"south\", jump);\n\n onGamepadStick(\"left\", (v) => {\n player.move(v.x * MOVE_SPEED, 0);\n });\n\n onKeyPress(\"f\", () => {\n setFullscreen(!isFullscreen());\n });\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n ]);\n onKeyPress(() => go(\"game\"));\n});\n\nscene(\"win\", () => {\n add([\n text(\"You Win\"),\n ]);\n onKeyPress(() => go(\"game\"));\n});\n\ngo(\"game\");\n", + "index": "51" + }, + { + "name": "polygon", + "code": "kaplay();\n\nsetBackground(0, 0, 0);\n\nadd([\n text(\"Drag corners of the polygon\"),\n pos(20, 20),\n]);\n\n// Make a weird shape\nconst poly = add([\n polygon([\n vec2(0, 0),\n vec2(100, 0),\n vec2(100, 200),\n vec2(200, 200),\n vec2(200, 300),\n vec2(100, 300),\n vec2(100, 200),\n vec2(0, 200),\n ], {\n colors: [\n rgb(128, 255, 128),\n rgb(255, 128, 128),\n rgb(128, 128, 255),\n rgb(255, 128, 128),\n rgb(128, 128, 128),\n rgb(128, 255, 128),\n rgb(255, 128, 128),\n rgb(128, 255, 128),\n ],\n triangulate: true,\n }),\n pos(150, 150),\n area(),\n color(),\n]);\n\nlet dragging = null;\nlet hovering = null;\n\npoly.onDraw(() => {\n const triangles = triangulate(poly.pts);\n for (const triangle of triangles) {\n drawTriangle({\n p1: triangle[0],\n p2: triangle[1],\n p3: triangle[2],\n fill: false,\n outline: { color: BLACK },\n });\n }\n if (hovering !== null) {\n drawCircle({\n pos: poly.pts[hovering],\n radius: 16,\n });\n }\n});\n\nonUpdate(() => {\n if (isConvex(poly.pts)) {\n poly.color = WHITE;\n }\n else {\n poly.color = rgb(192, 192, 192);\n }\n});\n\nonMousePress(() => {\n dragging = hovering;\n});\n\nonMouseRelease(() => {\n dragging = null;\n});\n\nonMouseMove(() => {\n hovering = null;\n const mp = mousePos().sub(poly.pos);\n for (let i = 0; i < poly.pts.length; i++) {\n if (mp.dist(poly.pts[i]) < 16) {\n hovering = i;\n break;\n }\n }\n if (dragging !== null) {\n poly.pts[dragging] = mousePos().sub(poly.pos);\n }\n});\n\npoly.onHover(() => {\n poly.color = rgb(200, 200, 255);\n});\n\npoly.onHoverEnd(() => {\n poly.color = rgb(255, 255, 255);\n});\n", + "index": "52" + }, + { + "name": "polygonuv", + "code": "// Adding game objects to screen\n\n// Start a kaboom game\nkaplay();\n\n// Load a sprite asset from \"sprites/bean.png\", with the name \"bean\"\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\n// A \"Game Object\" is the basic unit of entity in kaboom\n// Game objects are composed from components\n// Each component gives a game object certain capabilities\n\n// add() assembles a game object from a list of components and add to game, returns the reference of the game object\nconst player = add([\n sprite(\"bean\"), // sprite() component makes it render as a sprite\n pos(120, 80), // pos() component gives it position, also enables movement\n rotate(0), // rotate() component gives it rotation\n anchor(\"center\"), // anchor() component defines the pivot point (defaults to \"topleft\")\n]);\n\n// .onUpdate() is a method on all game objects, it registers an event that runs every frame\nplayer.onUpdate(() => {\n // .angle is a property provided by rotate() component, here we're incrementing the angle by 120 degrees per second, dt() is the time elapsed since last frame in seconds\n player.angle += 120 * dt();\n});\n\n// Make sure all sprites have been loaded\nonLoad(() => {\n // Get the texture and uv for ghosty\n const data = getSprite(\"ghosty\").data;\n const tex = data.tex;\n const quad = data.frames[0];\n // Add multiple game objects\n for (let i = 0; i < 3; i++) {\n // generate a random point on screen\n // width() and height() gives the game dimension\n const x = rand(0, width());\n const y = rand(0, height());\n\n add([\n pos(x, y),\n {\n q: quad.clone(),\n pts: [\n vec2(-32, -32),\n vec2(32, -32),\n vec2(32, 32),\n vec2(-32, 32),\n ],\n // Draw the polygon\n draw() {\n const q = this.q;\n drawPolygon({\n pts: pts,\n uv: [\n vec2(q.x, q.y),\n vec2(q.x + q.w, q.y),\n vec2(q.x + q.w, q.y + q.h),\n vec2(q.x, q.y + q.h),\n ],\n tex: tex,\n });\n },\n // Update the vertices each frame\n update() {\n pts = [\n vec2(-32, -32),\n vec2(32, -32),\n vec2(32, 32),\n vec2(-32, 32),\n ].map((p, index) =>\n p.add(\n 5 * Math.cos((time() + index * 0.25) * Math.PI),\n 5 * Math.sin((time() + index * 0.25) * Math.PI),\n )\n );\n },\n },\n ]);\n }\n});\n", + "index": "53" + }, + { + "name": "pong", + "code": "kaplay({\n background: [255, 255, 128],\n});\n\n// add paddles\nadd([\n pos(40, 0),\n rect(20, 80),\n outline(4),\n anchor(\"center\"),\n area(),\n \"paddle\",\n]);\n\nadd([\n pos(width() - 40, 0),\n rect(20, 80),\n outline(4),\n anchor(\"center\"),\n area(),\n \"paddle\",\n]);\n\n// move paddles with mouse\nonUpdate(\"paddle\", (p) => {\n p.pos.y = mousePos().y;\n});\n\n// score counter\nlet score = 0;\n\nadd([\n text(score),\n pos(center()),\n anchor(\"center\"),\n z(50),\n {\n update() {\n this.text = score;\n },\n },\n]);\n\n// ball\nlet speed = 480;\n\nconst ball = add([\n pos(center()),\n circle(16),\n outline(4),\n area({ shape: new Rect(vec2(-16), 32, 32) }),\n { vel: Vec2.fromAngle(rand(-20, 20)) },\n]);\n\n// move ball, bounce it when touche horizontal edges, respawn when touch vertical edges\nball.onUpdate(() => {\n ball.move(ball.vel.scale(speed));\n if (ball.pos.x < 0 || ball.pos.x > width()) {\n score = 0;\n ball.pos = center();\n ball.vel = Vec2.fromAngle(rand(-20, 20));\n speed = 320;\n }\n if (ball.pos.y < 0 || ball.pos.y > height()) {\n ball.vel.y = -ball.vel.y;\n }\n});\n\n// bounce when touch paddle\nball.onCollide(\"paddle\", (p) => {\n speed += 60;\n ball.vel = Vec2.fromAngle(ball.pos.angle(p.pos));\n score++;\n});\n", + "index": "54" + }, + { + "name": "postEffect", + "code": "// Build levels with addLevel()\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\n\nconst effects = {\n crt: () => ({\n \"u_flatness\": 3,\n }),\n vhs: () => ({\n \"u_intensity\": 12,\n }),\n pixelate: () => ({\n \"u_resolution\": vec2(width(), height()),\n \"u_size\": wave(2, 16, time() * 2),\n }),\n invert: () => ({\n \"u_invert\": 1,\n }),\n light: () => ({\n \"u_radius\": 64,\n \"u_blur\": 64,\n \"u_resolution\": vec2(width(), height()),\n \"u_mouse\": mousePos(),\n }),\n};\n\nfor (const effect in effects) {\n loadShaderURL(effect, null, `/examples/shaders/${effect}.frag`);\n}\n\nlet curEffect = 0;\nconst SPEED = 480;\n\nsetGravity(2400);\n\nconst level = addLevel([\n // Design the level layout with symbols\n \"@ ^ $$\",\n \"=======\",\n], {\n // The size of each grid\n tileWidth: 64,\n tileHeight: 64,\n // The position of the top left block\n pos: vec2(100, 200),\n // Define what each symbol means (in components)\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n },\n});\n\n// Get the player object from tag\nconst player = level.get(\"player\")[0];\n\n// Movements\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n});\n\n// Back to the original position if hit a \"danger\" item\nplayer.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n});\n\n// Eat the coin!\nplayer.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n});\n\nonKeyPress(\"up\", () => {\n const list = Object.keys(effects);\n curEffect = curEffect === 0 ? list.length - 1 : curEffect - 1;\n label.text = list[curEffect];\n});\n\nonKeyPress(\"down\", () => {\n const list = Object.keys(effects);\n curEffect = (curEffect + 1) % list.length;\n label.text = list[curEffect];\n});\n\nconst label = add([\n pos(8, 8),\n text(Object.keys(effects)[curEffect]),\n]);\n\nadd([\n pos(8, height() - 8),\n text(\"Press up / down to switch effects\"),\n anchor(\"botleft\"),\n]);\n\nonUpdate(() => {\n const effect = Object.keys(effects)[curEffect];\n usePostEffect(effect, effects[effect]());\n});\n", + "index": "55" + }, + { + "name": "query", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\n\nconst bean = add([\n pos(50, 50),\n sprite(\"bean\"),\n color(WHITE),\n \"bean\",\n]);\n\nadd([\n pos(200, 50),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n]);\n\nadd([\n pos(400, 50),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n]);\n\nadd([\n pos(100, 250),\n sprite(\"ghosty\"),\n color(WHITE),\n \"ghosty\",\n named(\"Candy&Carmel\"),\n]);\n\nfunction makeButton(p, t, cb) {\n const button = add([\n pos(p),\n rect(150, 40, { radius: 5 }),\n anchor(\"center\"),\n color(WHITE),\n area(),\n \"button\",\n ]);\n button.add([\n text(t),\n color(BLACK),\n anchor(\"center\"),\n area(),\n ]);\n button.onClick(() => {\n get(\"button\").forEach(o => o.color = WHITE);\n button.color = GREEN;\n cb();\n });\n}\n\nmakeButton(vec2(200, 400), \"bean\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ include: \"bean\" }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(360, 400), \"ghosty\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ include: \"ghosty\" }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(200, 450), \"near\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n bean.query({\n distance: 150,\n distanceOp: \"near\",\n hierarchy: \"siblings\",\n exclude: \"button\",\n }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(360, 450), \"far\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n bean.query({\n distance: 150,\n distanceOp: \"far\",\n hierarchy: \"siblings\",\n exclude: \"button\",\n }).forEach(o => o.color = RED);\n});\n\nmakeButton(vec2(520, 400), \"name\", () => {\n get(\"sprite\").forEach(o => o.color = WHITE);\n query({ name: \"Candy&Carmel\" }).forEach(o => o.color = RED);\n});\n", + "index": "56" + }, + { + "name": "raycastLevelTest", + "code": "kaplay();\n\nconst level = addLevel([\n \"a\",\n], {\n tileHeight: 100,\n tileWidth: 100,\n tiles: {\n a: () => [\n rect(32, 32),\n area(),\n color(RED),\n ],\n },\n});\ntry {\n level.raycast(vec2(50, 50), vec2(-50, -50));\n} catch (e) {\n debug.error(e.stack);\n throw e;\n}\n", + "index": "57" + }, + { + "name": "raycastObject", + "code": "kaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n area(),\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n area(),\n]);\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonDraw(\"selected\", (s) => {\n const bbox = s.worldArea().bbox();\n drawRect({\n pos: bbox.pos.sub(s.pos),\n width: bbox.width,\n height: bbox.height,\n outline: {\n color: YELLOW,\n width: 1,\n },\n fill: false,\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"area\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.hasPoint(pos));\n const selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n\nfunction laser() {\n return {\n draw() {\n drawTriangle({\n p1: vec2(-16, -16),\n p2: vec2(16, 0),\n p3: vec2(-16, 16),\n pos: vec2(0, 0),\n color: this.color,\n });\n if (this.showRing || this.is(\"turn\")) {\n drawCircle({\n pos: vec2(0, 0),\n radius: 28,\n outline: {\n color: RED,\n width: 4,\n },\n fill: false,\n });\n }\n pushTransform();\n pushRotate(-this.angle);\n const MAX_TRACE_DEPTH = 3;\n const MAX_DISTANCE = 400;\n let origin = this.pos;\n let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);\n let traceDepth = 0;\n while (traceDepth < MAX_TRACE_DEPTH) {\n const hit = raycast(origin, direction, [\"laser\"]);\n if (!hit) {\n drawLine({\n p1: origin.sub(this.pos),\n p2: origin.add(direction).sub(this.pos),\n width: 1,\n color: this.color,\n });\n break;\n }\n const pos = hit.point.sub(this.pos);\n // Draw hit point\n drawCircle({\n pos: pos,\n radius: 4,\n color: this.color,\n });\n // Draw hit normal\n drawLine({\n p1: pos,\n p2: pos.add(hit.normal.scale(20)),\n width: 1,\n color: BLUE,\n });\n // Draw hit distance\n drawLine({\n p1: origin.sub(this.pos),\n p2: pos,\n width: 1,\n color: this.color,\n });\n // Offset the point slightly, otherwise it might be too close to the surface\n // and give internal reflections\n origin = hit.point.add(hit.normal.scale(0.001));\n // Reflect vector\n direction = direction.reflect(hit.normal);\n traceDepth++;\n }\n popTransform();\n },\n showRing: false,\n };\n}\n\nconst ray = add([\n pos(150, 270),\n rotate(-45),\n anchor(\"center\"),\n rect(64, 64),\n area(),\n laser(0),\n color(RED),\n opacity(0.0),\n \"laser\",\n]);\n\nget(\"laser\").forEach(laser => {\n laser.onHover(() => {\n laser.showRing = true;\n });\n laser.onHoverEnd(() => {\n laser.showRing = false;\n });\n laser.onClick(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n if (laser.pos.sub(mousePos()).slen() > 28 * 28) {\n laser.use(\"turn\");\n }\n else {\n laser.use(\"selected\");\n }\n });\n});\n", + "index": "58" + }, + { + "name": "raycastShape", + "code": "kaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(this.pos, this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(this.pos, this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(180, 40),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts.map((pt) => pt.add(this.pos)));\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawRect({\n pos: vec2(-1, -1),\n width: 3,\n height: 3,\n color: this.color,\n });\n },\n getShape() {\n // This would be point if we had a real class for it\n return new Rect(vec2(-1, -1).add(this.pos), 3, 3);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(this.pos, 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nfunction rayCastShapes(origin, direction) {\n let minHit;\n const shapes = get(\"shape\");\n shapes.forEach(s => {\n const shape = s.getShape();\n const hit = shape.raycast(origin, direction);\n if (hit) {\n if (minHit) {\n if (hit.fraction < minHit.fraction) {\n minHit = hit;\n }\n }\n else {\n minHit = hit;\n }\n }\n });\n return minHit;\n}\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && s1.getShape().collides(s2.getShape())\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) => shape.getShape().contains(pos));\n const selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n\nfunction laser() {\n return {\n draw() {\n drawTriangle({\n p1: vec2(-16, -16),\n p2: vec2(16, 0),\n p3: vec2(-16, 16),\n pos: vec2(0, 0),\n color: this.color,\n });\n if (this.showRing || this.is(\"turn\")) {\n drawCircle({\n pos: vec2(0, 0),\n radius: 28,\n outline: {\n color: RED,\n width: 4,\n },\n fill: false,\n });\n }\n pushTransform();\n pushRotate(-this.angle);\n const MAX_TRACE_DEPTH = 3;\n const MAX_DISTANCE = 400;\n let origin = this.pos;\n let direction = Vec2.fromAngle(this.angle).scale(MAX_DISTANCE);\n let traceDepth = 0;\n while (traceDepth < MAX_TRACE_DEPTH) {\n const hit = rayCastShapes(origin, direction);\n if (!hit) {\n drawLine({\n p1: origin.sub(this.pos),\n p2: origin.add(direction).sub(this.pos),\n width: 1,\n color: this.color,\n });\n break;\n }\n const pos = hit.point.sub(this.pos);\n // Draw hit point\n drawCircle({\n pos: pos,\n radius: 4,\n color: this.color,\n });\n // Draw hit normal\n drawLine({\n p1: pos,\n p2: pos.add(hit.normal.scale(20)),\n width: 1,\n color: BLUE,\n });\n // Draw hit distance\n drawLine({\n p1: origin.sub(this.pos),\n p2: pos,\n width: 1,\n color: this.color,\n });\n // Offset the point slightly, otherwise it might be too close to the surface\n // and give internal reflections\n origin = hit.point.add(hit.normal.scale(0.001));\n // Reflect vector\n direction = direction.reflect(hit.normal);\n traceDepth++;\n }\n popTransform();\n },\n showRing: false,\n };\n}\n\nconst ray = add([\n pos(150, 270),\n rotate(-45),\n anchor(\"center\"),\n rect(64, 64),\n area(),\n laser(0),\n color(RED),\n opacity(0.0),\n \"laser\",\n]);\n\nget(\"laser\").forEach(laser => {\n laser.onHover(() => {\n laser.showRing = true;\n });\n laser.onHoverEnd(() => {\n laser.showRing = false;\n });\n laser.onClick(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n if (laser.pos.sub(mousePos()).slen() > 28 * 28) {\n laser.use(\"turn\");\n }\n else {\n laser.use(\"selected\");\n }\n });\n});\n", + "index": "59" + }, + { + "name": "raycaster3d", + "code": "// Start kaboom\nkaplay();\n\n// load assets\nlet bean;\nlet objSlices = [];\nlet wall;\nlet slices = [];\nloadSprite(\"bean\", \"sprites/bean.png\");\nloadSprite(\"wall\", \"sprites/brick_wall.png\");\n\nonLoad(() => {\n bean = getSprite(\"bean\").data;\n for (let i = 0; i < bean.width; i++) {\n objSlices.push(\n bean.frames[0].scale(\n new Quad(i / bean.width, 0, 1 / bean.width, 1),\n ),\n );\n }\n\n wall = getSprite(\"wall\").data;\n for (let i = 0; i < wall.width; i++) {\n slices.push(\n wall.frames[0].scale(\n new Quad(i / wall.width, 0, 1 / wall.width, 1),\n ),\n );\n }\n});\n\nfunction rayCastGrid(origin, direction, gridPosHit, maxDistance = 64) {\n const pos = origin;\n const len = direction.len();\n const dir = direction.scale(1 / len);\n let t = 0;\n let gridPos = vec2(Math.floor(origin.x), Math.floor(origin.y));\n const step = vec2(dir.x > 0 ? 1 : -1, dir.y > 0 ? 1 : -1);\n const tDelta = vec2(Math.abs(1 / dir.x), Math.abs(1 / dir.y));\n let dist = vec2(\n (step.x > 0) ? (gridPos.x + 1 - origin.x) : (origin.x - gridPos.x),\n (step.y > 0) ? (gridPos.y + 1 - origin.y) : (origin.y - gridPos.y),\n );\n let tMax = vec2(\n (tDelta.x < Infinity) ? tDelta.x * dist.x : Infinity,\n (tDelta.y < Infinity) ? tDelta.y * dist.y : Infinity,\n );\n let steppedIndex = -1;\n while (t <= maxDistance) {\n const hit = gridPosHit(gridPos);\n if (hit === true) {\n return {\n point: pos.add(dir.scale(t)),\n normal: vec2(\n steppedIndex === 0 ? -step.x : 0,\n steppedIndex === 1 ? -step.y : 0,\n ),\n t: t / len, // Since dir is normalized, t is len times too large\n gridPos,\n };\n }\n else if (hit) {\n return hit;\n }\n if (tMax.x < tMax.y) {\n gridPos.x += step.x;\n t = tMax.x;\n tMax.x += tDelta.x;\n steppedIndex = 0;\n }\n else {\n gridPos.y += step.y;\n t = tMax.y;\n tMax.y += tDelta.y;\n steppedIndex = 1;\n }\n }\n\n return null;\n}\n\nfunction raycastEdge(origin, direction, line) {\n const a = origin;\n const c = line.p1.add(line.pos);\n const d = line.p2.add(line.pos);\n const ab = direction;\n const cd = d.sub(c);\n let abxcd = ab.cross(cd);\n // If parallel, no intersection\n if (Math.abs(abxcd) < Number.EPSILON) {\n return false;\n }\n const ac = c.sub(a);\n const s = ac.cross(cd) / abxcd;\n // s is the percentage of the position of the intersection on cd\n if (s <= 0 || s >= 1) {\n return false;\n }\n const t = ac.cross(ab) / abxcd;\n // t is the percentage of the position of the intersection on ab\n if (t <= 0 || t >= 1) {\n return false;\n }\n\n const normal = cd.normal().unit();\n if (direction.dot(normal) > 0) {\n normal.x *= -1;\n normal.y *= -1;\n }\n\n return {\n point: a.add(ab.scale(s)),\n normal: normal,\n t: s,\n s: t,\n object: line,\n };\n}\n\nfunction rayCastAsciiGrid(origin, direction, grid) {\n origin = origin.scale(1 / 16);\n direction = direction.scale(1 / 16);\n const objects = [];\n const hit = rayCastGrid(origin, direction, ({ x, y }) => {\n if (y >= 0 && y < grid.length) {\n const row = grid[y];\n if (x >= 0 && x < row.length) {\n if (row[x] === \"&\") {\n const perp = direction.normal().unit();\n const planeP1 = perp.scale(-0.2);\n const planeP2 = perp.scale(0.2);\n const objectHit = raycastEdge(origin, direction, {\n pos: vec2(x + 0.5, y + 0.5),\n p1: planeP1,\n p2: planeP2,\n });\n if (objectHit) {\n objects.push(objectHit);\n }\n }\n return row[x] !== \" \" && row[x] !== \"&\";\n }\n }\n }, direction.len());\n if (hit) {\n hit.point = hit.point.scale(16);\n hit.object = { color: colors[grid[hit.gridPos.y][hit.gridPos.x]] };\n hit.objects = objects;\n }\n return hit;\n}\n\nconst colors = {\n \"#\": RED,\n \"$\": GREEN,\n \"%\": BLUE,\n \"&\": YELLOW,\n};\n\nconst grid = [\n \"##################\",\n \"# #\",\n \"# $$$$$$$ $$$$$$ #\",\n \"# $ $ #\",\n \"# $ %% %%%%%%% $ #\",\n \"# $ % % $ #\",\n \"#&$&%%%%% %%%&$&#\",\n \"# $ % $ #\",\n \"# $ %%%%%%%%%% #\",\n \"# $ $ #\",\n \"# $$$$$$$ $$$$$$ #\",\n \"# & #\",\n \"##################\",\n];\n\nconst camera = add([\n pos(7 * 16, 11 * 16 + 8),\n rotate(0),\n z(-1),\n rect(8, 8),\n anchor(\"center\"),\n area(),\n opacity(0),\n body(),\n {\n draw() {\n pushTransform();\n pushRotate(-this.angle);\n drawCircle({\n pos: vec2(),\n radius: 4,\n color: RED,\n });\n const dir = Vec2.fromAngle(this.angle);\n const perp = dir.normal();\n const planeP1 = this.pos.add(dir.scale(this.focalLength)).add(\n perp.scale(this.fov),\n ).sub(this.pos);\n const planeP2 = this.pos.add(dir.scale(this.focalLength)).sub(\n perp.scale(this.fov),\n ).sub(this.pos);\n drawLine({\n p1: planeP1,\n p2: planeP2,\n width: 1,\n color: RED,\n });\n pushTranslate(this.pos.scale(-1).add(300, 50));\n drawRect({\n width: 240,\n height: 120,\n color: rgb(100, 100, 100),\n });\n drawRect({\n pos: vec2(0, 120),\n width: 240,\n height: 120,\n color: rgb(128, 128, 128),\n });\n for (let x = 0; x <= 120; x++) {\n let direction = lerp(planeP1, planeP2, x / 120).scale(6);\n const hit = rayCastAsciiGrid(this.pos, direction, grid);\n if (hit) {\n const t = hit.t;\n // Distance to attenuate light\n const d = (1 - t)\n * ((hit.normal.x + hit.normal.y) < 0 ? 0.5 : 1);\n // Horizontal texture slice\n let u = Math.abs(hit.normal.x) > Math.abs(hit.normal.y)\n ? hit.point.y\n : hit.point.x;\n u = (u % 16) / 16;\n u = u - Math.floor(u);\n // Height of the wall\n const h = 240 / (t * direction.len() / 16);\n\n drawUVQuad({\n width: 2,\n height: h,\n pos: vec2(x * 2, 120 - h / 2),\n tex: wall.tex,\n quad: slices[Math.round(u * (wall.width - 1))],\n color: BLACK.lerp(WHITE, d),\n });\n\n // If we hit any objects\n if (hit.objects) {\n hit.objects.reverse().forEach(o => {\n const t = o.t;\n // Wall and object height\n const wh = 240 / (t * direction.len() / 16);\n const oh = 140 / (t * direction.len() / 16);\n // Slice to render\n let u = o.s;\n drawUVQuad({\n width: 2,\n height: oh,\n pos: vec2(x * 2, 120 + wh / 2 - oh),\n tex: bean.tex,\n quad:\n objSlices[Math.round(u * (bean.width - 1))],\n color: BLACK.lerp(WHITE, u),\n });\n });\n }\n }\n }\n popTransform();\n },\n focalLength: 40,\n fov: 10,\n },\n]);\n\naddLevel(grid, {\n pos: vec2(0, 0),\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \"#\": () => [\n rect(16, 16),\n color(RED),\n area(),\n body({ isStatic: true }),\n ],\n \"$\": () => [\n rect(16, 16),\n color(GREEN),\n area(),\n body({ isStatic: true }),\n ],\n \"%\": () => [\n rect(16, 16),\n color(BLUE),\n area(),\n body({ isStatic: true }),\n ],\n \"&\": () => [\n pos(4, 4),\n rect(8, 8),\n color(YELLOW),\n ],\n },\n});\n\nonKeyDown(\"up\", () => {\n camera.move(Vec2.fromAngle(camera.angle).scale(40));\n});\n\nonKeyDown(\"down\", () => {\n camera.move(Vec2.fromAngle(camera.angle).scale(-40));\n});\n\nonKeyDown(\"left\", () => {\n camera.angle -= 90 * dt();\n});\n\nonKeyDown(\"right\", () => {\n camera.angle += 90 * dt();\n});\n\nonKeyDown(\"f\", () => {\n camera.focalLength = Math.max(1, camera.focalLength - 10 * dt());\n});\n\nonKeyDown(\"g\", () => {\n camera.focalLength += 10 * dt();\n});\n\nonKeyDown(\"r\", () => {\n camera.fov = Math.max(1, camera.fov - 10 * dt());\n});\n\nonKeyDown(\"t\", () => {\n camera.fov += 10 * dt();\n});\n\nonKeyDown(\"p\", () => {\n debug.paused = !debug.paused;\n});\n\nlet lastPos = vec2();\n\nonTouchStart(pos => {\n lastPos = pos;\n});\n\nonTouchMove(pos => {\n const delta = pos.sub(lastPos);\n if (delta.x < 0) {\n camera.angle -= 90 * dt();\n }\n else if (delta.x > 0) {\n camera.angle += 90 * dt();\n }\n if (delta.y < 0) {\n camera.move(Vec2.fromAngle(camera.angle).scale(40));\n }\n else if (delta.y > 0) {\n camera.move(Vec2.fromAngle(camera.angle).scale(-40));\n }\n lastPos = pos;\n});\n", + "index": "60" + }, + { + "name": "rect", + "code": "// Adding game objects to screen\n\n// Start a kaboom game\nkaplay();\n\nadd([\n rect(100, 100, { radius: 20 }),\n pos(100, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: [10, 20, 30, 40] }),\n pos(250, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: [0, 20, 0, 20] }),\n pos(400, 100),\n rotate(0),\n anchor(\"center\"),\n]);\n\nadd([\n rect(100, 100, { radius: 20 }),\n pos(100, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n\nadd([\n rect(100, 100, { radius: [10, 20, 30, 40] }),\n pos(250, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n\nadd([\n rect(100, 100, { radius: [0, 20, 0, 20] }),\n pos(400, 250),\n rotate(0),\n anchor(\"center\"),\n outline(4, BLACK),\n]);\n", + "index": "61" + }, + { + "name": "rpg", + "code": "// simple rpg style walk and talk\n\nkaplay({\n background: [74, 48, 82],\n});\n\nloadSprite(\"bag\", \"/sprites/bag.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"steel\", \"/sprites/steel.png\");\nloadSprite(\"door\", \"/sprites/door.png\");\nloadSprite(\"key\", \"/sprites/key.png\");\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nscene(\"main\", (levelIdx) => {\n const SPEED = 320;\n\n // character dialog data\n const characters = {\n \"a\": {\n sprite: \"bag\",\n msg: \"Hi Bean! You should get that key!\",\n },\n \"b\": {\n sprite: \"ghosty\",\n msg: \"Who are you? You can see me??\",\n },\n };\n\n // level layouts\n const levels = [\n [\n \"===|====\",\n \"= =\",\n \"= $ =\",\n \"= a =\",\n \"= =\",\n \"= @ =\",\n \"========\",\n ],\n [\n \"--------\",\n \"- -\",\n \"- $ -\",\n \"| -\",\n \"- b -\",\n \"- @ -\",\n \"--------\",\n ],\n ];\n\n const level = addLevel(levels[levelIdx], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(64, 64),\n tiles: {\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n ],\n \"-\": () => [\n sprite(\"steel\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n ],\n \"$\": () => [\n sprite(\"key\"),\n area(),\n anchor(\"center\"),\n \"key\",\n ],\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"center\"),\n \"player\",\n ],\n \"|\": () => [\n sprite(\"door\"),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"door\",\n ],\n },\n // any() is a special function that gets called everytime there's a\n // symbole not defined above and is supposed to return what that symbol\n // means\n wildcardTile(ch) {\n const char = characters[ch];\n if (char) {\n return [\n sprite(char.sprite),\n area(),\n body({ isStatic: true }),\n anchor(\"center\"),\n \"character\",\n { msg: char.msg },\n ];\n }\n },\n });\n\n // get the player game obj by tag\n const player = level.get(\"player\")[0];\n\n function addDialog() {\n const h = 160;\n const pad = 16;\n const bg = add([\n pos(0, height() - h),\n rect(width(), h),\n color(0, 0, 0),\n z(100),\n ]);\n const txt = add([\n text(\"\", {\n width: width(),\n }),\n pos(0 + pad, height() - h + pad),\n z(100),\n ]);\n bg.hidden = true;\n txt.hidden = true;\n return {\n say(t) {\n txt.text = t;\n bg.hidden = false;\n txt.hidden = false;\n },\n dismiss() {\n if (!this.active()) {\n return;\n }\n txt.text = \"\";\n bg.hidden = true;\n txt.hidden = true;\n },\n active() {\n return !bg.hidden;\n },\n destroy() {\n bg.destroy();\n txt.destroy();\n },\n };\n }\n\n let hasKey = false;\n const dialog = addDialog();\n\n player.onCollide(\"key\", (key) => {\n destroy(key);\n hasKey = true;\n });\n\n player.onCollide(\"door\", () => {\n if (hasKey) {\n if (levelIdx + 1 < levels.length) {\n go(\"main\", levelIdx + 1);\n }\n else {\n go(\"win\");\n }\n }\n else {\n dialog.say(\"you got no key!\");\n }\n });\n\n // talk on touch\n player.onCollide(\"character\", (ch) => {\n dialog.say(ch.msg);\n });\n\n const dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n };\n\n for (const dir in dirs) {\n onKeyPress(dir, () => {\n dialog.dismiss();\n });\n onKeyDown(dir, () => {\n player.move(dirs[dir].scale(SPEED));\n });\n }\n});\n\nscene(\"win\", () => {\n add([\n text(\"You Win!\"),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n ]);\n});\n\ngo(\"main\", 0);\n", + "index": "62" + }, + { + "name": "runner", + "code": "const FLOOR_HEIGHT = 48;\nconst JUMP_FORCE = 800;\nconst SPEED = 480;\n\n// initialize context\nkaplay();\n\nsetBackground(141, 183, 255);\n\n// load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nscene(\"game\", () => {\n // define gravity\n setGravity(2400);\n\n // add a game object to screen\n const player = add([\n // list of components\n sprite(\"bean\"),\n pos(80, 40),\n area(),\n body(),\n ]);\n\n // floor\n add([\n rect(width(), FLOOR_HEIGHT),\n outline(4),\n pos(0, height()),\n anchor(\"botleft\"),\n area(),\n body({ isStatic: true }),\n color(132, 101, 236),\n ]);\n\n function jump() {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n }\n }\n\n // jump when user press space\n onKeyPress(\"space\", jump);\n onClick(jump);\n\n function spawnTree() {\n // add tree obj\n add([\n rect(48, rand(32, 96)),\n area(),\n outline(4),\n pos(width(), height() - FLOOR_HEIGHT),\n anchor(\"botleft\"),\n color(238, 143, 203),\n move(LEFT, SPEED),\n offscreen({ destroy: true }),\n \"tree\",\n ]);\n\n // wait a random amount of time to spawn next tree\n wait(rand(0.5, 1.5), spawnTree);\n }\n\n // start spawning trees\n spawnTree();\n\n // lose if player collides with any game obj with tag \"tree\"\n player.onCollide(\"tree\", () => {\n // go to \"lose\" scene and pass the score\n go(\"lose\", score);\n burp();\n addKaboom(player.pos);\n });\n\n // keep track of score\n let score = 0;\n\n const scoreLabel = add([\n text(score),\n pos(24, 24),\n ]);\n\n // increment score every frame\n onUpdate(() => {\n score++;\n scoreLabel.text = score;\n });\n});\n\nscene(\"lose\", (score) => {\n add([\n sprite(\"bean\"),\n pos(width() / 2, height() / 2 - 64),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // display score\n add([\n text(score),\n pos(width() / 2, height() / 2 + 64),\n scale(2),\n anchor(\"center\"),\n ]);\n\n // go back to game with space is pressed\n onKeyPress(\"space\", () => go(\"game\"));\n onClick(() => go(\"game\"));\n});\n\ngo(\"game\");\n", + "index": "63" + }, + { + "name": "scenes", + "code": "// Extend our game with multiple scenes\n\n// Start game\nkaplay();\n\n// Load assets\nloadSprite(\"bean\", \"/sprites/bean.png\");\nloadSprite(\"coin\", \"/sprites/coin.png\");\nloadSprite(\"spike\", \"/sprites/spike.png\");\nloadSprite(\"grass\", \"/sprites/grass.png\");\nloadSprite(\"ghosty\", \"/sprites/ghosty.png\");\nloadSprite(\"portal\", \"/sprites/portal.png\");\nloadSound(\"score\", \"/examples/sounds/score.mp3\");\nloadSound(\"portal\", \"/examples/sounds/portal.mp3\");\n\nsetGravity(2400);\n\nconst SPEED = 480;\n\n// Design 2 levels\nconst LEVELS = [\n [\n \"@ ^ $$ >\",\n \"=========\",\n ],\n [\n \"@ $ >\",\n \"= = =\",\n ],\n];\n\n// Define a scene called \"game\". The callback will be run when we go() to the scene\n// Scenes can accept argument from go()\nscene(\"game\", ({ levelIdx, score }) => {\n // Use the level passed, or first level\n const level = addLevel(LEVELS[levelIdx || 0], {\n tileWidth: 64,\n tileHeight: 64,\n pos: vec2(100, 200),\n tiles: {\n \"@\": () => [\n sprite(\"bean\"),\n area(),\n body(),\n anchor(\"bot\"),\n \"player\",\n ],\n \"=\": () => [\n sprite(\"grass\"),\n area(),\n body({ isStatic: true }),\n anchor(\"bot\"),\n ],\n \"$\": () => [\n sprite(\"coin\"),\n area(),\n anchor(\"bot\"),\n \"coin\",\n ],\n \"^\": () => [\n sprite(\"spike\"),\n area(),\n anchor(\"bot\"),\n \"danger\",\n ],\n \">\": () => [\n sprite(\"portal\"),\n area(),\n anchor(\"bot\"),\n \"portal\",\n ],\n },\n });\n\n // Get the player object from tag\n const player = level.get(\"player\")[0];\n\n // Movements\n onKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump();\n }\n });\n\n onKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n });\n\n onKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n });\n\n player.onCollide(\"danger\", () => {\n player.pos = level.tile2Pos(0, 0);\n // Go to \"lose\" scene when we hit a \"danger\"\n go(\"lose\");\n });\n\n player.onCollide(\"coin\", (coin) => {\n destroy(coin);\n play(\"score\");\n score++;\n scoreLabel.text = score;\n });\n\n // Fall death\n player.onUpdate(() => {\n if (player.pos.y >= 480) {\n go(\"lose\");\n }\n });\n\n // Enter the next level on portal\n player.onCollide(\"portal\", () => {\n play(\"portal\");\n if (levelIdx < LEVELS.length - 1) {\n // If there's a next level, go() to the same scene but load the next level\n go(\"game\", {\n levelIdx: levelIdx + 1,\n score: score,\n });\n }\n else {\n // Otherwise we have reached the end of game, go to \"win\" scene!\n go(\"win\", { score: score });\n }\n });\n\n // Score counter text\n const scoreLabel = add([\n text(score),\n pos(12),\n ]);\n});\n\nscene(\"lose\", () => {\n add([\n text(\"You Lose\"),\n pos(12),\n ]);\n\n // Press any key to go back\n onKeyPress(start);\n});\n\nscene(\"win\", ({ score }) => {\n add([\n text(`You grabbed ${score} coins!!!`, {\n width: width(),\n }),\n pos(12),\n ]);\n\n onKeyPress(start);\n});\n\nfunction start() {\n // Start with the \"game\" scene, with initial parameters\n go(\"game\", {\n levelIdx: 0,\n score: 0,\n });\n}\n\nstart();\n", + "index": "64" + }, + { + "name": "shader", + "code": "// Custom shader\nkaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Load a shader with custom fragment shader code\n// The fragment shader should define a function \"frag\", which returns a color and receives the vertex position, texture coodinate, vertex color, and texture as arguments\n// There's also the def_frag() function which returns the default fragment color\nloadShader(\n \"invert\",\n null,\n `\nuniform float u_time;\n\nvec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) {\n\tvec4 c = def_frag();\n\tfloat t = (sin(u_time * 4.0) + 1.0) / 2.0;\n\treturn mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t);\n}\n`,\n);\n\nadd([\n sprite(\"bean\"),\n pos(80, 40),\n scale(8),\n // Use the shader with shader() component and pass uniforms\n shader(\"invert\", () => ({\n \"u_time\": time(),\n })),\n]);\n", + "index": "65" + }, + { + "name": "shooter", + "code": "// TODO: document\n\nkaplay({\n background: [74, 48, 82],\n});\n\nconst objs = [\n \"apple\",\n \"lightening\",\n \"coin\",\n \"egg\",\n \"key\",\n \"door\",\n \"meat\",\n \"mushroom\",\n];\n\nfor (const obj of objs) {\n loadSprite(obj, `/sprites/${obj}.png`);\n}\n\nloadBean();\nloadSound(\"hit\", \"/examples/sounds/hit.mp3\");\nloadSound(\"shoot\", \"/examples/sounds/shoot.mp3\");\nloadSound(\"explode\", \"/examples/sounds/explode.mp3\");\nloadSound(\"OtherworldlyFoe\", \"/examples/sounds/OtherworldlyFoe.mp3\");\n\nscene(\"battle\", () => {\n const BULLET_SPEED = 1200;\n const TRASH_SPEED = 120;\n const BOSS_SPEED = 48;\n const PLAYER_SPEED = 480;\n const STAR_SPEED = 120;\n const BOSS_HEALTH = 1000;\n const OBJ_HEALTH = 4;\n\n const bossName = choose(objs);\n\n let insaneMode = false;\n\n const music = play(\"OtherworldlyFoe\");\n\n volume(0.5);\n\n function grow(rate) {\n return {\n update() {\n const n = rate * dt();\n this.scale.x += n;\n this.scale.y += n;\n },\n };\n }\n\n function late(t) {\n let timer = 0;\n return {\n add() {\n this.hidden = true;\n },\n update() {\n timer += dt();\n if (timer >= t) {\n this.hidden = false;\n }\n },\n };\n }\n\n add([\n text(\"KILL\", { size: 160 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(1),\n fixed(),\n ]);\n\n add([\n text(\"THE\", { size: 80 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(2),\n late(1),\n fixed(),\n ]);\n\n add([\n text(bossName.toUpperCase(), { size: 120 }),\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n opacity(),\n lifespan(4),\n late(2),\n fixed(),\n ]);\n\n const sky = add([\n rect(width(), height()),\n color(0, 0, 0),\n opacity(0),\n ]);\n\n sky.onUpdate(() => {\n if (insaneMode) {\n const t = time() * 10;\n sky.color.r = wave(127, 255, t);\n sky.color.g = wave(127, 255, t + 1);\n sky.color.b = wave(127, 255, t + 2);\n sky.opacity = 1;\n }\n else {\n sky.color = rgb(0, 0, 0);\n sky.opacity = 0;\n }\n });\n\n // \tadd([\n // \t\tsprite(\"stars\"),\n // \t\tscale(width() / 240, height() / 240),\n // \t\tpos(0, 0),\n // \t\t\"stars\",\n // \t])\n\n // \tadd([\n // \t\tsprite(\"stars\"),\n // \t\tscale(width() / 240, height() / 240),\n // \t\tpos(0, -height()),\n // \t\t\"stars\",\n // \t])\n\n // \tonUpdate(\"stars\", (r) => {\n // \t\tr.move(0, STAR_SPEED * (insaneMode ? 10 : 1))\n // \t\tif (r.pos.y >= height()) {\n // \t\t\tr.pos.y -= height() * 2\n // \t\t}\n // \t})\n\n const player = add([\n sprite(\"bean\"),\n area(),\n pos(width() / 2, height() - 64),\n anchor(\"center\"),\n ]);\n\n onKeyDown(\"left\", () => {\n player.move(-PLAYER_SPEED, 0);\n if (player.pos.x < 0) {\n player.pos.x = width();\n }\n });\n\n onKeyDown(\"right\", () => {\n player.move(PLAYER_SPEED, 0);\n if (player.pos.x > width()) {\n player.pos.x = 0;\n }\n });\n\n onKeyPress(\"up\", () => {\n insaneMode = true;\n music.speed = 2;\n });\n\n onKeyRelease(\"up\", () => {\n insaneMode = false;\n music.speed = 1;\n });\n\n player.onCollide(\"enemy\", (e) => {\n destroy(e);\n destroy(player);\n shake(120);\n play(\"explode\");\n music.detune = -1200;\n addExplode(center(), 12, 120, 30);\n wait(1, () => {\n music.paused = true;\n go(\"battle\");\n });\n });\n\n function addExplode(p, n, rad, size) {\n for (let i = 0; i < n; i++) {\n wait(rand(n * 0.1), () => {\n for (let i = 0; i < 2; i++) {\n add([\n pos(p.add(rand(vec2(-rad), vec2(rad)))),\n rect(4, 4),\n scale(1 * size, 1 * size),\n opacity(),\n lifespan(0.1),\n grow(rand(48, 72) * size),\n anchor(\"center\"),\n ]);\n }\n });\n }\n }\n\n function spawnBullet(p) {\n add([\n rect(12, 48),\n area(),\n pos(p),\n anchor(\"center\"),\n color(127, 127, 255),\n outline(4),\n move(UP, BULLET_SPEED),\n offscreen({ destroy: true }),\n // strings here means a tag\n \"bullet\",\n ]);\n }\n\n onUpdate(\"bullet\", (b) => {\n if (insaneMode) {\n b.color = rand(rgb(0, 0, 0), rgb(255, 255, 255));\n }\n });\n\n onKeyPress(\"space\", () => {\n spawnBullet(player.pos.sub(16, 0));\n spawnBullet(player.pos.add(16, 0));\n play(\"shoot\", {\n volume: 0.3,\n detune: rand(-1200, 1200),\n });\n });\n\n function spawnTrash() {\n const name = choose(objs.filter(n => n != bossName));\n add([\n sprite(name),\n area(),\n pos(rand(0, width()), 0),\n health(OBJ_HEALTH),\n anchor(\"bot\"),\n \"trash\",\n \"enemy\",\n { speed: rand(TRASH_SPEED * 0.5, TRASH_SPEED * 1.5) },\n ]);\n wait(insaneMode ? 0.1 : 0.3, spawnTrash);\n }\n\n const boss = add([\n sprite(bossName),\n area(),\n pos(width() / 2, 40),\n health(BOSS_HEALTH),\n scale(3),\n anchor(\"top\"),\n \"enemy\",\n {\n dir: 1,\n },\n ]);\n\n on(\"death\", \"enemy\", (e) => {\n destroy(e);\n shake(2);\n addKaboom(e.pos);\n });\n\n on(\"hurt\", \"enemy\", (e) => {\n shake(1);\n play(\"hit\", {\n detune: rand(-1200, 1200),\n speed: rand(0.2, 2),\n });\n });\n\n const timer = add([\n text(0),\n pos(12, 32),\n fixed(),\n { time: 0 },\n ]);\n\n timer.onUpdate(() => {\n timer.time += dt();\n timer.text = timer.time.toFixed(2);\n });\n\n onCollide(\"bullet\", \"enemy\", (b, e) => {\n destroy(b);\n e.hurt(insaneMode ? 10 : 1);\n addExplode(b.pos, 1, 24, 1);\n });\n\n onUpdate(\"trash\", (t) => {\n t.move(0, t.speed * (insaneMode ? 5 : 1));\n if (t.pos.y - t.height > height()) {\n destroy(t);\n }\n });\n\n boss.onUpdate((p) => {\n boss.move(BOSS_SPEED * boss.dir * (insaneMode ? 3 : 1), 0);\n if (boss.dir === 1 && boss.pos.x >= width() - 20) {\n boss.dir = -1;\n }\n if (boss.dir === -1 && boss.pos.x <= 20) {\n boss.dir = 1;\n }\n });\n\n boss.onHurt(() => {\n healthbar.set(boss.hp());\n });\n\n boss.onDeath(() => {\n music.stop();\n go(\"win\", {\n time: timer.time,\n boss: bossName,\n });\n });\n\n const healthbar = add([\n rect(width(), 24),\n pos(0, 0),\n color(107, 201, 108),\n fixed(),\n {\n max: BOSS_HEALTH,\n set(hp) {\n this.width = width() * hp / this.max;\n this.flash = true;\n },\n },\n ]);\n\n healthbar.onUpdate(() => {\n if (healthbar.flash) {\n healthbar.color = rgb(255, 255, 255);\n healthbar.flash = false;\n }\n else {\n healthbar.color = rgb(127, 255, 127);\n }\n });\n\n add([\n text(\"UP: insane mode\", { width: width() / 2, size: 32 }),\n anchor(\"botleft\"),\n pos(24, height() - 24),\n ]);\n\n spawnTrash();\n});\n\nscene(\"win\", ({ time, boss }) => {\n const b = burp({\n loop: true,\n });\n\n loop(0.5, () => {\n b.detune = rand(-1200, 1200);\n });\n\n add([\n sprite(boss),\n color(255, 0, 0),\n anchor(\"center\"),\n scale(8),\n pos(width() / 2, height() / 2),\n ]);\n\n add([\n text(time.toFixed(2), 24),\n anchor(\"center\"),\n pos(width() / 2, height() / 2),\n ]);\n});\n\ngo(\"battle\");\n", + "index": "66" + }, + { + "name": "size", + "code": "kaplay({\n // without specifying \"width\" and \"height\", kaboom will size to the container (document.body by default)\n width: 200,\n height: 100,\n // \"stretch\" stretches the defined width and height to fullscreen\n // stretch: true,\n // \"letterbox\" makes stretching keeps aspect ratio (leaves black bars on empty spaces), have no effect without \"stretch\"\n letterbox: true,\n});\n\nloadBean();\n\nadd([\n sprite(\"bean\"),\n]);\n\nonClick(() => addKaboom(mousePos()));\n", + "index": "67" + }, + { + "name": "slice9", + "code": "// 9 slice sprite scaling\n\nkaplay();\n\n// Load a sprite that's made for 9 slice scaling\nloadSprite(\"9slice\", \"/examples/sprites/9slice.png\", {\n // Define the slice by the margins of 4 sides\n slice9: {\n left: 32,\n right: 32,\n top: 32,\n bottom: 32,\n },\n});\n\nconst g = add([\n sprite(\"9slice\"),\n]);\n\nonMouseMove(() => {\n const mpos = mousePos();\n // Scaling the image will keep the aspect ratio of the sliced frames\n g.width = mpos.x;\n g.height = mpos.y;\n});\n", + "index": "68" + }, + { + "name": "sprite", + "code": "// Sprite animation\n\n// Start a kaboom game\nkaplay({\n // Scale the whole game up\n scale: 4,\n // Set the default font\n font: \"monospace\",\n});\n\n// Loading a multi-frame sprite\nloadSprite(\"dino\", \"/examples/sprites/dino.png\", {\n // The image contains 9 frames layed out horizontally, slice it into individual frames\n sliceX: 9,\n // Define animations\n anims: {\n \"idle\": {\n // Starts from frame 0, ends at frame 3\n from: 0,\n to: 3,\n // Frame per second\n speed: 5,\n loop: true,\n },\n \"run\": {\n from: 4,\n to: 7,\n speed: 10,\n loop: true,\n },\n // This animation only has 1 frame\n \"jump\": 8,\n },\n});\n\nconst SPEED = 120;\nconst JUMP_FORCE = 240;\n\nsetGravity(640);\n\n// Add our player character\nconst player = add([\n sprite(\"dino\"),\n pos(center()),\n anchor(\"center\"),\n area(),\n body(),\n]);\n\n// .play is provided by sprite() component, it starts playing the specified animation (the animation information of \"idle\" is defined above in loadSprite)\nplayer.play(\"idle\");\n\n// Add a platform\nadd([\n rect(width(), 24),\n area(),\n outline(1),\n pos(0, height() - 24),\n body({ isStatic: true }),\n]);\n\n// Switch to \"idle\" or \"run\" animation when player hits ground\nplayer.onGround(() => {\n if (!isKeyDown(\"left\") && !isKeyDown(\"right\")) {\n player.play(\"idle\");\n }\n else {\n player.play(\"run\");\n }\n});\n\nplayer.onAnimEnd((anim) => {\n if (anim === \"idle\") {\n // You can also register an event that runs when certain anim ends\n }\n});\n\nonKeyPress(\"space\", () => {\n if (player.isGrounded()) {\n player.jump(JUMP_FORCE);\n player.play(\"jump\");\n }\n});\n\nonKeyDown(\"left\", () => {\n player.move(-SPEED, 0);\n player.flipX = true;\n // .play() will reset to the first frame of the anim, so we want to make sure it only runs when the current animation is not \"run\"\n if (player.isGrounded() && player.curAnim() !== \"run\") {\n player.play(\"run\");\n }\n});\n\nonKeyDown(\"right\", () => {\n player.move(SPEED, 0);\n player.flipX = false;\n if (player.isGrounded() && player.curAnim() !== \"run\") {\n player.play(\"run\");\n }\n});\n[\"left\", \"right\"].forEach((key) => {\n onKeyRelease(key, () => {\n // Only reset to \"idle\" if player is not holding any of these keys\n if (player.isGrounded() && !isKeyDown(\"left\") && !isKeyDown(\"right\")) {\n player.play(\"idle\");\n }\n });\n});\n\nconst getInfo = () =>\n `\nAnim: ${player.curAnim()}\nFrame: ${player.frame}\n`.trim();\n\n// Add some text to show the current animation\nconst label = add([\n text(getInfo(), { size: 12 }),\n color(0, 0, 0),\n pos(4),\n]);\n\nlabel.onUpdate(() => {\n label.text = getInfo();\n});\n\n// Check out https://kaboomjs.com#SpriteComp for everything sprite() provides\n", + "index": "69" + }, + { + "name": "spriteatlas", + "code": "kaplay({\n scale: 4,\n background: [0, 0, 0],\n});\n\n// https://0x72.itch.io/dungeontileset-ii\nloadSpriteAtlas(\"/examples/sprites/dungeon.png\", {\n \"hero\": {\n \"x\": 128,\n \"y\": 196,\n \"width\": 144,\n \"height\": 28,\n \"sliceX\": 9,\n \"anims\": {\n \"idle\": {\n \"from\": 0,\n \"to\": 3,\n \"speed\": 3,\n \"loop\": true,\n },\n \"run\": {\n \"from\": 4,\n \"to\": 7,\n \"speed\": 10,\n \"loop\": true,\n },\n \"hit\": 8,\n },\n },\n \"ogre\": {\n \"x\": 16,\n \"y\": 320,\n \"width\": 256,\n \"height\": 32,\n \"sliceX\": 8,\n \"anims\": {\n \"idle\": {\n \"from\": 0,\n \"to\": 3,\n \"speed\": 3,\n \"loop\": true,\n },\n \"run\": {\n \"from\": 4,\n \"to\": 7,\n \"speed\": 10,\n \"loop\": true,\n },\n },\n },\n \"floor\": {\n \"x\": 16,\n \"y\": 64,\n \"width\": 48,\n \"height\": 48,\n \"sliceX\": 3,\n \"sliceY\": 3,\n },\n \"chest\": {\n \"x\": 304,\n \"y\": 304,\n \"width\": 48,\n \"height\": 16,\n \"sliceX\": 3,\n \"anims\": {\n \"open\": {\n \"from\": 0,\n \"to\": 2,\n \"speed\": 20,\n \"loop\": false,\n },\n \"close\": {\n \"from\": 2,\n \"to\": 0,\n \"speed\": 20,\n \"loop\": false,\n },\n },\n },\n \"sword\": {\n \"x\": 322,\n \"y\": 81,\n \"width\": 12,\n \"height\": 30,\n },\n \"wall\": {\n \"x\": 16,\n \"y\": 16,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_top\": {\n \"x\": 16,\n \"y\": 0,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_left\": {\n \"x\": 16,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_right\": {\n \"x\": 0,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_topleft\": {\n \"x\": 32,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_topright\": {\n \"x\": 48,\n \"y\": 128,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_botleft\": {\n \"x\": 32,\n \"y\": 144,\n \"width\": 16,\n \"height\": 16,\n },\n \"wall_botright\": {\n \"x\": 48,\n \"y\": 144,\n \"width\": 16,\n \"height\": 16,\n },\n});\n\n// Can also load from external JSON url\n// loadSpriteAtlas(\"/sprites/dungeon.png\", \"/sprites/dungeon.json\")\n\n// floor\naddLevel([\n \"xxxxxxxxxx\",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n \" \",\n], {\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \" \": () => [\n sprite(\"floor\", { frame: ~~rand(0, 8) }),\n ],\n },\n});\n\n// objects\nconst map = addLevel([\n \"tttttttttt\",\n \"cwwwwwwwwd\",\n \"l r\",\n \"l r\",\n \"l r\",\n \"l $ r\",\n \"l r\",\n \"l $ r\",\n \"attttttttb\",\n \"wwwwwwwwww\",\n], {\n tileWidth: 16,\n tileHeight: 16,\n tiles: {\n \"$\": () => [\n sprite(\"chest\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n { opened: false },\n \"chest\",\n ],\n \"a\": () => [\n sprite(\"wall_botleft\"),\n area({ shape: new Rect(vec2(0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"b\": () => [\n sprite(\"wall_botright\"),\n area({ shape: new Rect(vec2(12, 0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"c\": () => [\n sprite(\"wall_topleft\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"d\": () => [\n sprite(\"wall_topright\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"w\": () => [\n sprite(\"wall\"),\n area(),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"t\": () => [\n sprite(\"wall_top\"),\n area({ shape: new Rect(vec2(0, 12), 16, 4) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"l\": () => [\n sprite(\"wall_left\"),\n area({ shape: new Rect(vec2(0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n \"r\": () => [\n sprite(\"wall_right\"),\n area({ shape: new Rect(vec2(12, 0), 4, 16) }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n },\n});\n\nconst player = map.spawn(\n [\n sprite(\"hero\", { anim: \"idle\" }),\n area({ shape: new Rect(vec2(0, 6), 12, 12) }),\n body(),\n anchor(\"center\"),\n tile(),\n ],\n 2,\n 2,\n);\n\nconst sword = player.add([\n pos(-4, 9),\n sprite(\"sword\"),\n anchor(\"bot\"),\n rotate(0),\n spin(),\n]);\n\n// TODO: z\nmap.spawn(\n [\n sprite(\"ogre\"),\n anchor(\"bot\"),\n area({ scale: 0.5 }),\n body({ isStatic: true }),\n tile({ isObstacle: true }),\n ],\n 5,\n 4,\n);\n\nfunction spin() {\n let spinning = false;\n return {\n id: \"spin\",\n update() {\n if (spinning) {\n this.angle += 1200 * dt();\n if (this.angle >= 360) {\n this.angle = 0;\n spinning = false;\n }\n }\n },\n spin() {\n spinning = true;\n },\n };\n}\n\nfunction interact() {\n let interacted = false;\n for (const col of player.getCollisions()) {\n const c = col.target;\n if (c.is(\"chest\")) {\n if (c.opened) {\n c.play(\"close\");\n c.opened = false;\n }\n else {\n c.play(\"open\");\n c.opened = true;\n }\n interacted = true;\n }\n }\n if (!interacted) {\n sword.spin();\n }\n}\n\nonKeyPress(\"space\", interact);\n\nconst SPEED = 120;\n\nconst dirs = {\n \"left\": LEFT,\n \"right\": RIGHT,\n \"up\": UP,\n \"down\": DOWN,\n};\n\nplayer.onUpdate(() => {\n camPos(player.pos);\n});\n\nplayer.onPhysicsResolve(() => {\n // Set the viewport center to player.pos\n camPos(player.pos);\n});\n\nonKeyDown(\"right\", () => {\n player.flipX = false;\n sword.flipX = false;\n player.move(SPEED, 0);\n sword.pos = vec2(-4, 9);\n});\n\nonKeyDown(\"left\", () => {\n player.flipX = true;\n sword.flipX = true;\n player.move(-SPEED, 0);\n sword.pos = vec2(4, 9);\n});\n\nonKeyDown(\"up\", () => {\n player.move(0, -SPEED);\n});\n\nonKeyDown(\"down\", () => {\n player.move(0, SPEED);\n});\n\nonGamepadButtonPress(\"south\", interact);\n\nonGamepadStick(\"left\", (v) => {\n if (v.x < 0) {\n player.flipX = true;\n sword.flipX = true;\n sword.pos = vec2(4, 9);\n }\n else if (v.x > 0) {\n player.flipX = false;\n sword.flipX = false;\n sword.pos = vec2(-4, 9);\n }\n player.move(v.scale(SPEED));\n if (v.isZero()) {\n if (player.curAnim() !== \"idle\") player.play(\"idle\");\n }\n else {\n if (player.curAnim() !== \"run\") player.play(\"run\");\n }\n});\n[\"left\", \"right\", \"up\", \"down\"].forEach((key) => {\n onKeyPress(key, () => {\n player.play(\"run\");\n });\n onKeyRelease(key, () => {\n if (\n !isKeyDown(\"left\")\n && !isKeyDown(\"right\")\n && !isKeyDown(\"up\")\n && !isKeyDown(\"down\")\n ) {\n player.play(\"idle\");\n }\n });\n});\n", + "index": "70" + }, + { + "name": "text", + "code": "kaplay({\n background: [212, 110, 179],\n});\n\n// Load a custom font from a .ttf file\nloadFont(\"FlowerSketches\", \"/examples/fonts/FlowerSketches.ttf\");\n\n// Load a custom font with options\nloadFont(\"apl386\", \"/examples/fonts/apl386.ttf\", {\n outline: 4,\n filter: \"linear\",\n});\n\n// Load custom bitmap font, specifying the width and height of each character in the image\nloadBitmapFont(\"unscii\", \"/examples/fonts/unscii_8x8.png\", 8, 8);\nloadBitmapFont(\"4x4\", \"/examples/fonts/4x4.png\", 4, 4);\n\n// List of built-in fonts (\"o\" at the end means the outlined version)\nconst builtinFonts = [\n \"monospace\",\n];\n\n// Make a list of fonts that we cycle through\nconst fonts = [\n ...builtinFonts,\n \"4x4\",\n \"unscii\",\n \"FlowerSketches\",\n \"apl386\",\n \"Sans-Serif\",\n];\n\n// Keep track which is the current font\nlet curFont = 0;\nlet curSize = 48;\nconst pad = 24;\n\n// Add a game object with text() component + options\nconst input = add([\n pos(pad),\n // Render text with the text() component\n text(\"Type! And try arrow keys!\", {\n // What font to use\n font: fonts[curFont],\n // It'll wrap to next line if the text width exceeds the width option specified here\n width: width() - pad * 2,\n // The height of character\n size: curSize,\n // Text alignment (\"left\", \"center\", \"right\", default \"left\")\n align: \"left\",\n lineSpacing: 8,\n letterSpacing: 4,\n // Transform each character for special effects\n transform: (idx, ch) => ({\n color: hsl2rgb((time() * 0.2 + idx * 0.1) % 1, 0.7, 0.8),\n pos: vec2(0, wave(-4, 4, time() * 4 + idx * 0.5)),\n scale: wave(1, 1.2, time() * 3 + idx),\n angle: wave(-9, 9, time() * 3 + idx),\n }),\n }),\n]);\n\n// Like onKeyPressRepeat() but more suitable for text input.\nonCharInput((ch) => {\n input.text += ch;\n});\n\n// Like onKeyPress() but will retrigger when key is being held (which is similar to text input behavior)\n// Insert new line when user presses enter\nonKeyPressRepeat(\"enter\", () => {\n input.text += \"\\n\";\n});\n\n// Delete last character\nonKeyPressRepeat(\"backspace\", () => {\n input.text = input.text.substring(0, input.text.length - 1);\n});\n\n// Go to previous font\nonKeyPress(\"left\", () => {\n if (--curFont < 0) curFont = fonts.length - 1;\n input.font = fonts[curFont];\n});\n\n// Go to next font\nonKeyPress(\"right\", () => {\n curFont = (curFont + 1) % fonts.length;\n input.font = fonts[curFont];\n});\n\nconst SIZE_SPEED = 32;\nconst SIZE_MIN = 12;\nconst SIZE_MAX = 120;\n\n// Increase text size\nonKeyDown(\"up\", () => {\n curSize = Math.min(curSize + dt() * SIZE_SPEED, SIZE_MAX);\n input.textSize = curSize;\n});\n\n// Decrease text size\nonKeyDown(\"down\", () => {\n curSize = Math.max(curSize - dt() * SIZE_SPEED, SIZE_MIN);\n input.textSize = curSize;\n});\n\n// Use this syntax and style option to style chunks of text, with CharTransformFunc.\nadd([\n text(\n \"[green]oh hi[/green] here's some [wavy][rainbow]styled[/rainbow][/wavy] text\",\n {\n width: width(),\n styles: {\n \"green\": {\n color: rgb(128, 128, 255),\n },\n \"wavy\": (idx, ch) => ({\n pos: vec2(0, wave(-4, 4, time() * 6 + idx * 0.5)),\n }),\n \"rainbow\": (idx, ch) => ({\n color: hsl2rgb((time() * 0.2 + idx * 0.1) % 1, 0.7, 0.8),\n }),\n },\n },\n ),\n pos(pad, height() - pad),\n anchor(\"botleft\"),\n // scale(0.5),\n]);\n", + "index": "71" + }, + { + "name": "textInput", + "code": "// Start kaboom\nkaplay();\n\nsetBackground(BLACK);\n\n// Add the game object that asks a question\nadd([\n anchor(\"top\"),\n pos(width() / 2, 0),\n text(\"Whats your favorite food :D\"),\n]);\n\n// Add the node that you write in\nconst food = add([\n text(\"\"),\n textInput(true, 10), // make it have focus and only be 20 chars max\n pos(width() / 2, height() / 2),\n anchor(\"center\"),\n]);\n\n// add the response\nadd([\n text(\"\"),\n anchor(\"bot\"),\n pos(width() / 2, height()),\n {\n update() {\n this.text =\n `wow i didnt know you love ${food.text} so much, but i like it too :D`;\n },\n },\n]);\n", + "index": "72" + }, + { + "name": "tiled", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nadd([\n pos(150, 150),\n sprite(\"bean\", {\n tiled: true,\n width: 200,\n height: 200,\n }),\n anchor(\"center\"),\n]);\n\n// debug.inspect = true\n", + "index": "73" + }, + { + "name": "timer", + "code": "kaplay();\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\n// Execute something after every 0.5 seconds.\nloop(0.5, () => {\n const bean = add([\n sprite(\"bean\"),\n pos(rand(vec2(0), vec2(width(), height()))),\n ]);\n\n // Execute something after 3 seconds.\n wait(3, () => {\n destroy(bean);\n });\n});\n", + "index": "74" + }, + { + "name": "transformShape", + "code": "kaplay();\n\nadd([\n pos(80, 80),\n circle(40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(vec2(), this.radius);\n },\n },\n]);\n\nadd([\n pos(180, 210),\n circle(20),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Circle(vec2(), this.radius);\n },\n },\n]);\n\nadd([\n pos(40, 180),\n rect(20, 40),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(), this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(140, 130),\n rect(60, 50),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(), this.width, this.height);\n },\n },\n]);\n\nadd([\n pos(190, 40),\n rotate(45),\n polygon([vec2(-60, 60), vec2(0, 0), vec2(60, 60)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts);\n },\n },\n]);\n\nadd([\n pos(280, 130),\n polygon([vec2(-20, 20), vec2(0, 0), vec2(20, 20)]),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Polygon(this.pts);\n },\n },\n]);\n\nadd([\n pos(280, 80),\n color(BLUE),\n \"shape\",\n {\n draw() {\n drawLine({\n p1: vec2(30, 0),\n p2: vec2(0, 30),\n width: 4,\n color: this.color,\n });\n },\n getShape() {\n return new Line(\n vec2(30, 0).add(this.pos),\n vec2(0, 30).add(this.pos),\n );\n },\n },\n]);\n\nadd([\n pos(260, 80),\n color(BLUE),\n rotate(45),\n rect(30, 60),\n \"shape\",\n {\n getShape() {\n return new Rect(vec2(0, 0), 30, 60);\n },\n },\n]);\n\nadd([\n pos(280, 200),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(vec2(), 80, 30);\n },\n draw() {\n drawEllipse({\n radiusX: 80,\n radiusY: 30,\n color: this.color,\n });\n },\n },\n]);\n\nadd([\n pos(340, 120),\n color(BLUE),\n \"shape\",\n {\n getShape() {\n return new Ellipse(vec2(), 40, 15, 45);\n },\n draw() {\n pushRotate(45);\n drawEllipse({\n radiusX: 40,\n radiusY: 15,\n color: this.color,\n });\n popTransform();\n },\n },\n]);\n\nfunction getGlobalShape(s) {\n const t = s.transform;\n return s.getShape().transform(t);\n}\n\nonUpdate(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s1 => {\n if (\n shapes.some(s2 =>\n s1 !== s2 && getGlobalShape(s1).collides(getGlobalShape(s2))\n )\n ) {\n s1.color = RED;\n }\n else {\n s1.color = BLUE;\n }\n });\n});\n\nonDraw(() => {\n const shapes = get(\"shape\");\n shapes.forEach(s => {\n const shape = getGlobalShape(s);\n // console.log(tshape)\n switch (shape.constructor.name) {\n case \"Ellipse\":\n pushTransform();\n pushTranslate(shape.center);\n pushRotate(shape.angle);\n drawEllipse({\n pos: vec2(),\n radiusX: shape.radiusX,\n radiusY: shape.radiusY,\n fill: false,\n outline: {\n width: 4,\n color: rgb(255, 255, 0),\n },\n });\n popTransform();\n break;\n case \"Polygon\":\n drawPolygon({\n pts: shape.pts,\n fill: false,\n outline: {\n width: 4,\n color: rgb(255, 255, 0),\n },\n });\n break;\n }\n });\n});\n\nonMousePress(() => {\n const shapes = get(\"shape\");\n const pos = mousePos();\n const pickList = shapes.filter((shape) =>\n getGlobalShape(shape).contains(pos)\n );\n selection = pickList[pickList.length - 1];\n if (selection) {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n selection.use(\"selected\");\n }\n});\n\nonMouseMove((pos, delta) => {\n get(\"selected\").forEach(sel => {\n sel.moveBy(delta);\n });\n get(\"turn\").forEach(laser => {\n const oldVec = mousePos().sub(delta).sub(laser.pos);\n const newVec = mousePos().sub(laser.pos);\n laser.angle += oldVec.angleBetween(newVec);\n });\n});\n\nonMouseRelease(() => {\n get(\"selected\").forEach(s => s.unuse(\"selected\"));\n get(\"turn\").forEach(s => s.unuse(\"turn\"));\n});\n", + "index": "75" + }, + { + "name": "tween", + "code": "// Tweeeeeening!\n\nkaplay({\n background: [141, 183, 255],\n});\n\nloadSprite(\"bean\", \"/sprites/bean.png\");\n\nconst duration = 1;\nconst easeTypes = Object.keys(easings);\nlet curEaseType = 0;\n\nconst bean = add([\n sprite(\"bean\"),\n scale(2),\n pos(center()),\n rotate(0),\n anchor(\"center\"),\n]);\n\nconst label = add([\n text(easeTypes[curEaseType], { size: 64 }),\n pos(24, 24),\n]);\n\nadd([\n text(\"Click anywhere & use arrow keys\", { width: width() }),\n anchor(\"botleft\"),\n pos(24, height() - 24),\n]);\n\nonKeyPress([\"left\", \"a\"], () => {\n curEaseType = curEaseType === 0 ? easeTypes.length - 1 : curEaseType - 1;\n label.text = easeTypes[curEaseType];\n});\n\nonKeyPress([\"right\", \"d\"], () => {\n curEaseType = (curEaseType + 1) % easeTypes.length;\n label.text = easeTypes[curEaseType];\n});\n\nlet curTween = null;\n\nonMousePress(() => {\n const easeType = easeTypes[curEaseType];\n // stop previous lerp, or there will be jittering\n if (curTween) curTween.cancel();\n // start the tween\n curTween = tween(\n // start value (accepts number, Vec2 and Color)\n bean.pos,\n // destination value\n mousePos(),\n // duration (in seconds)\n duration,\n // how value should be updated\n (val) => bean.pos = val,\n // interpolation function (defaults to easings.linear)\n easings[easeType],\n );\n});\n", + "index": "76" + } +] \ No newline at end of file diff --git a/src/util/hotkeys.ts b/src/util/hotkeys.ts new file mode 100644 index 0000000..ebb8889 --- /dev/null +++ b/src/util/hotkeys.ts @@ -0,0 +1,9 @@ +import { useEditor } from "../hooks/useEditor"; + +document.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.key === "s") { + event.preventDefault(); + event.stopPropagation(); + useEditor.getState().run(); + } +}); diff --git a/vite.config.ts b/vite.config.ts index c6615ce..6c31aab 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,6 +1,8 @@ import react from "@vitejs/plugin-react"; +import path from "path"; import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; +import { generateExamples } from "./scripts/examples.js"; // https://vitejs.dev/config/ export default defineConfig({ @@ -18,5 +20,26 @@ export default defineConfig({ }, ], }), + { + name: "kaplay", + buildStart() { + const examplesPath = process.env.EXAMPLES_PATH; + + if (examplesPath) { + generateExamples( + path.join(import.meta.dirname, examplesPath), + ); + } else generateExamples(); + }, + watchChange() { + const examplesPath = process.env.EXAMPLES_PATH; + + if (examplesPath) { + generateExamples( + path.join(import.meta.dirname, examplesPath), + ); + } else generateExamples(); + }, + }, ], });