├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feat.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── publish.yml │ ├── sync-playground.yml │ └── test.yml ├── .gitignore ├── .vscode ├── settings.json └── snippets.code-snippets ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── dprint.json ├── eslint.config.mjs ├── examples ├── ai.js ├── animation.js ├── audio.js ├── basicEventsObject.js ├── basicsCompRender.js ├── basicsComponents.js ├── basicsEvents.js ├── basicsGlobals.js ├── basicsObject.js ├── basicsStart.js ├── binding.js ├── blend.js ├── burp.js ├── button.js ├── camera.js ├── children.js ├── clip.js ├── collision.js ├── collisionshapes.js ├── component.js ├── concert.js ├── confetti.js ├── curves.js ├── customCompDebug.js ├── doublejump.js ├── drag.js ├── draw.js ├── drawon.js ├── drawoncanvas.js ├── eatlove.js ├── egg.js ├── examples.json ├── fadeIn.js ├── fakeMouse.js ├── flamebar.js ├── flappy.js ├── fonts │ ├── 04b03.ttf │ ├── 04b03_6x8.png │ ├── 4x4.png │ ├── FlowerSketches.ttf │ ├── Romantique.ttf │ ├── apl386.ttf │ ├── cga_8x8.png │ ├── happy_28x36.png │ ├── proggy_7x13.png │ ├── sink_6x8.png │ ├── unscii_8x8.png │ └── zpix.ttf ├── frames.js ├── friction.js ├── gamepad.js ├── gamepadMulti.js ├── ghosthunting.js ├── gravity.js ├── health.js ├── hover.js ├── kaboom.js ├── kaplayLogoAnim.js ├── layer.js ├── layers.js ├── lerp.js ├── level.js ├── levelRaycast.js ├── levelcomp.js ├── lifespan.js ├── linecap.js ├── linejoin.js ├── livequery.js ├── loadingScreen.js ├── maze.js ├── mazeRaycastedLight.js ├── movement.js ├── objectInit.js ├── onLoadError.js ├── out.js ├── overlap.js ├── particle.js ├── particleTrail.js ├── patrol.js ├── pauseMenu.js ├── picture.js ├── platformEffector.js ├── platformer.js ├── polygon.js ├── pong.js ├── postEffect.js ├── query.js ├── raycastObject.js ├── raycastShape.js ├── raycaster3d.js ├── restitution.js ├── rpg.js ├── runner.js ├── scaletest.js ├── scenes.js ├── shader.js ├── shaders │ ├── blink.frag │ ├── crt.frag │ ├── invert.frag │ ├── light.frag │ ├── pixelate.frag │ └── vhs.frag ├── shapeRect.js ├── shooter.js ├── size.js ├── slice9.js ├── sokoban.js ├── sounds │ ├── OtherworldlyFoe.mp3 │ ├── bean_voice.wav │ ├── bell.mp3 │ ├── blip.mp3 │ ├── bug.mp3 │ ├── burp.mp3 │ ├── computer.mp3 │ ├── danger.mp3 │ ├── dune.mp3 │ ├── error.mp3 │ ├── explode.mp3 │ ├── hit.mp3 │ ├── kaboom2000.mp3 │ ├── knock.ogg │ ├── mark_voice.wav │ ├── mystic.mp3 │ ├── notice.mp3 │ ├── off.mp3 │ ├── portal.mp3 │ ├── powerup.mp3 │ ├── robot.mp3 │ ├── score.mp3 │ ├── shoot.mp3 │ ├── signal.mp3 │ ├── spring.mp3 │ ├── weak.mp3 │ └── wooosh.mp3 ├── sprite.js ├── spriteatlas.js ├── sprites │ ├── 9slice.png │ ├── YOSHI.png │ ├── apple.png │ ├── bag.png │ ├── bean.png │ ├── bobo.png │ ├── boom.png │ ├── brick_wall.png │ ├── btfly.png │ ├── cloud.png │ ├── coin.png │ ├── cursor_default.png │ ├── cursor_pointer.png │ ├── dino.png │ ├── door.png │ ├── dungeon-dino.png │ ├── dungeon.json │ ├── dungeon.png │ ├── egg.png │ ├── egg_crack.png │ ├── ghostiny.png │ ├── ghosty.png │ ├── gigagantrum.png │ ├── grab.png │ ├── grape.png │ ├── grass.png │ ├── gun.png │ ├── heart.png │ ├── jumpy.png │ ├── k.png │ ├── ka.png │ ├── kaboom.png │ ├── kaplay-dino.png │ ├── key.png │ ├── lightening.png │ ├── mark.png │ ├── meat.png │ ├── moon.png │ ├── mushroom.png │ ├── note.png │ ├── particle_circle_filled.png │ ├── particle_circle_outline.png │ ├── particle_hexagon_filled.png │ ├── particle_star_filled.png │ ├── pineapple.png │ ├── portal.png │ ├── spike.png │ ├── spritemerge_chest.png │ ├── spritemerge_corpus.png │ ├── steel.png │ ├── sun.png │ ├── sword.png │ ├── tga.png │ ├── watermelon.png │ ├── you.json │ ├── you.png │ └── zombean.png ├── text.js ├── textInput.js ├── tiled.js ├── timer.js ├── tsconfig.json ├── tween.js ├── tweenEasings.js ├── tweenEasingsCustom.js ├── video.js ├── videos │ ├── 3d.mp4 │ └── dance.mp4 └── vn.js ├── help.json ├── kaplay.webp ├── package.json ├── pnpm-lock.yaml ├── scripts ├── build.js ├── buildFast.js ├── constants.js ├── dev.js ├── dev │ ├── dev.js │ ├── serve.js │ └── views │ │ ├── examplesList.ejs │ │ └── game.ejs ├── generateIndex.js ├── git-split.sh ├── lib │ ├── autoinput.js │ ├── build.js │ ├── globaldts.js │ └── util.js └── test.js ├── src ├── .env.d.ts ├── app │ ├── app.ts │ ├── appEvents.ts │ ├── data.ts │ └── inputBindings.ts ├── assets │ ├── aseprite.ts │ ├── asset.ts │ ├── bitmapFont.ts │ ├── font.ts │ ├── shader.ts │ ├── sound.ts │ ├── sprite.ts │ ├── spriteAtlas.ts │ └── utils.ts ├── audio │ ├── audio.ts │ ├── burp.ts │ ├── play.ts │ ├── playMusic.ts │ └── volume.ts ├── constants │ ├── colorMap.ts │ ├── general.ts │ └── math.ts ├── core │ ├── context.ts │ ├── engine.ts │ ├── engineLoop.ts │ ├── errors.ts │ ├── fontCache.ts │ ├── frameRendering.ts │ ├── plug.ts │ └── quit.ts ├── data │ ├── assets │ │ ├── bean.png │ │ ├── boom.png │ │ ├── burp.mp3 │ │ ├── happy.png │ │ ├── index.d.ts │ │ └── ka.png │ └── gamepad.json ├── debug │ ├── debug.ts │ └── record.ts ├── ecs │ ├── components │ │ ├── draw │ │ │ ├── blend.ts │ │ │ ├── circle.ts │ │ │ ├── color.ts │ │ │ ├── drawon.ts │ │ │ ├── ellipse.ts │ │ │ ├── fadeIn.ts │ │ │ ├── mask.ts │ │ │ ├── opacity.ts │ │ │ ├── outline.ts │ │ │ ├── particles.ts │ │ │ ├── picture.ts │ │ │ ├── polygon.ts │ │ │ ├── raycast.ts │ │ │ ├── rect.ts │ │ │ ├── shader.ts │ │ │ ├── sprite.ts │ │ │ ├── text.ts │ │ │ ├── uvquad.ts │ │ │ └── video.ts │ │ ├── level │ │ │ ├── agent.ts │ │ │ ├── level.ts │ │ │ ├── pathfinder.ts │ │ │ ├── patrol.ts │ │ │ ├── sentry.ts │ │ │ └── tile.ts │ │ ├── misc │ │ │ ├── animate.ts │ │ │ ├── boom.ts │ │ │ ├── fakeMouse.ts │ │ │ ├── health.ts │ │ │ ├── lifespan.ts │ │ │ ├── named.ts │ │ │ ├── state.ts │ │ │ ├── stay.ts │ │ │ ├── textInput.ts │ │ │ └── timer.ts │ │ ├── physics │ │ │ ├── area.ts │ │ │ ├── body.ts │ │ │ ├── doubleJump.ts │ │ │ └── effectors.ts │ │ └── transform │ │ │ ├── anchor.ts │ │ │ ├── fixed.ts │ │ │ ├── follow.ts │ │ │ ├── layer.ts │ │ │ ├── move.ts │ │ │ ├── offscreen.ts │ │ │ ├── pos.ts │ │ │ ├── rotate.ts │ │ │ ├── scale.ts │ │ │ └── z.ts │ ├── entity │ │ ├── GameObjRaw.ts │ │ ├── make.ts │ │ ├── premade │ │ │ ├── addKaboom.ts │ │ │ └── addLevel.ts │ │ └── utils.ts │ └── systems │ │ ├── createCollisionSystem.ts │ │ └── systems.ts ├── events │ ├── eventMap.ts │ ├── events.ts │ └── globalEvents.ts ├── game │ ├── camera.ts │ ├── game.ts │ ├── gameLoop.ts │ ├── gravity.ts │ ├── layers.ts │ ├── scenes.ts │ └── utils.ts ├── gfx │ ├── FrameBuffer.ts │ ├── TexPacker.ts │ ├── anchor.ts │ ├── bg.ts │ ├── canvas.ts │ ├── canvasBuffer.ts │ ├── draw │ │ ├── drawBezier.ts │ │ ├── drawCanvas.ts │ │ ├── drawCircle.ts │ │ ├── drawCurve.ts │ │ ├── drawDebug.ts │ │ ├── drawEllipse.ts │ │ ├── drawFormattedText.ts │ │ ├── drawFrame.ts │ │ ├── drawInspectText.ts │ │ ├── drawLine.ts │ │ ├── drawLoadingScreen.ts │ │ ├── drawMasked.ts │ │ ├── drawPicture.ts │ │ ├── drawPolygon.ts │ │ ├── drawRaw.ts │ │ ├── drawRect.ts │ │ ├── drawSprite.ts │ │ ├── drawStenciled.ts │ │ ├── drawSubstracted.ts │ │ ├── drawText.ts │ │ ├── drawTexture.ts │ │ ├── drawTriangle.ts │ │ ├── drawUVQuad.ts │ │ └── drawUnscaled.ts │ ├── formatText.ts │ ├── gfx.ts │ ├── gfxApp.ts │ ├── stack.ts │ └── viewport.ts ├── kaplay.ts ├── math │ ├── Mat4.ts │ ├── Vec2.ts │ ├── clamp.ts │ ├── color.ts │ ├── easings.ts │ ├── gjk.ts │ ├── lerp.ts │ ├── lerpNumber.ts │ ├── math.ts │ ├── minkowski.ts │ ├── navigation.ts │ ├── navigationgrid.ts │ ├── navigationmesh.ts │ ├── sat.ts │ ├── spatial │ │ ├── hashgrid.ts │ │ └── sweepandprune.ts │ ├── various.ts │ └── vec3.ts ├── shared.ts ├── types.ts └── utils │ ├── asserts.ts │ ├── benchmark.ts │ ├── binaryheap.ts │ ├── dataURL.ts │ ├── deepEq.ts │ ├── log.ts │ ├── numbers.ts │ ├── overload.ts │ ├── runes.ts │ ├── sets.ts │ └── types.ts ├── tests ├── .env.d.ts ├── auto │ ├── color.spec.ts │ ├── kaplay.spec.ts │ ├── missingComps.spec.ts │ ├── plugin.spec.ts │ └── vec2.spec.ts ├── playtests │ ├── bench.js │ ├── colorCSS.js │ ├── compsMissing.js │ ├── compsMissingDynamic.js │ ├── debugFramePhysics.js │ ├── debugTimeScale.js │ ├── fastLoop.js │ ├── fixedUpdate.js │ ├── hover.js │ ├── jsconfig.json │ ├── largeTexture.js │ ├── mfbday.js │ ├── mouse.js │ ├── mouseLetterbox.js │ ├── mouseLetterbox2.js │ ├── mouseScale.js │ ├── mouseScaleLetterbox.js │ ├── mouseScaleLetterbox2.js │ ├── mouseScaleStretch.js │ ├── mouseStretch.js │ ├── parenttest.js │ ├── paused.js │ ├── physics.js │ ├── physicsfactory.js │ ├── polygonuv.js │ ├── prettyDebug.js │ ├── raycastLevelTest.js │ ├── slice.js │ ├── test536.js │ ├── textBig.js │ ├── textPaused.js │ ├── textShader.js │ ├── textStyleChangeFont.js │ ├── textStyleOverride.js │ ├── textUnparse.js │ ├── textWeirdTags.js │ ├── textWrap.js │ ├── timeLeft.js │ ├── transformShape.js │ ├── twokaplays.js │ └── wait1.js ├── tsconfig.json └── types │ ├── add.test-d.ts │ ├── plugins.test-d.ts │ └── state.test-d.ts ├── tsconfig.build.json ├── tsconfig.json └── vitest.config.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [kaplayjs] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: kaplay 5 | open_collective: kaplay 6 | ko_fi: kaplay 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve. 3 | labels: ["bug"] 4 | title: "bug: " 5 | type: Bug 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Bug Description 10 | description: A clear and concise description of what the bug is. 11 | placeholder: The objects are not objecting 😔... 12 | validations: 13 | required: true 14 | 15 | - type: input 16 | attributes: 17 | label: Version 18 | description: Running Version 19 | placeholder: What version are you running? ex. 3001.0.10/master 20 | validations: 21 | required: false 22 | 23 | - type: input 24 | attributes: 25 | label: Playground Link 26 | description: Link to the playground where the bug can be reproduced. 27 | placeholder: https://play.kaplayjs.com/... 28 | validations: 29 | required: false 30 | 31 | - type: textarea 32 | attributes: 33 | label: Extra information 34 | description: Is there a method to reliably reproduce it? Did you expect something else to happen? Please add screenshots/evidence if possible. 35 | placeholder: So, i was kaplaying normally, then all of sudden... 36 | validations: 37 | required: false 38 | 39 | - type: checkboxes 40 | attributes: 41 | label: Summary 42 | options: 43 | - label: Fixed in v4000 44 | - label: Fixed in v3001 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | contact_links: 2 | - name: Help 3 | url: https://github.com/kaplayjs/kaplay/discussions/new?category=q-a 4 | about: How to use certain kaplay feature, or how to achieve something with kaplay 5 | - name: Compiling/Contributing 6 | url: https://github.com/kaplayjs/kaplay/discussions/new?category=q-a 7 | about: How to compile kaplay/how to contribute to it 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feat.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for kaplayy. 3 | labels: ["enhancement"] 4 | title: "feat: your feature request here" 5 | type: "Feature" 6 | body: 7 | - type: textarea 8 | attributes: 9 | label: Is your feature request related to a problem? Please describe. 10 | description: A clear and concise description of what the problem is. Ex. I'm always frustated when.... 11 | placeholder: I HATE having to make 3D manually so we should add 3D... 12 | validations: 13 | required: true 14 | 15 | - type: textarea 16 | attributes: 17 | label: Any more information? 18 | description: Have you found alternatives? How should we make the API for you feature request? Type it here. 19 | placeholder: So we could add something like addCube() then... 20 | validations: 21 | required: false -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Please describe what issue(s) this PR fixes. 7 | 8 | ## Summary 9 | 10 | - [ ] Changeloged 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for more information: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | # https://containers.dev/guide/dependabot 6 | 7 | version: 2 8 | updates: 9 | - package-ecosystem: "devcontainers" 10 | directory: "/" 11 | schedule: 12 | interval: weekly 13 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | push: 5 | tags: 6 | - '4000*' 7 | workflow_dispatch: 8 | inputs: 9 | version: 10 | description: "Version to publish" 11 | required: true 12 | default: "4000.0.0" 13 | 14 | jobs: 15 | publish: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Install Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | registry-url: "https://registry.npmjs.org" 24 | node-version: 20 25 | - uses: pnpm/action-setup@v4 26 | name: Install pnpm 27 | with: 28 | run_install: false 29 | - name: Install dependencies 30 | run: pnpm install 31 | - name: Publish to NPM 32 | run: npm publish --tag next 33 | env: 34 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | -------------------------------------------------------------------------------- /.github/workflows/sync-playground.yml: -------------------------------------------------------------------------------- 1 | name: Bump kaplayjs/kaplayground submodule 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | sync: 11 | permissions: write-all 12 | name: "Dispatch Event" 13 | runs-on: ubuntu-latest 14 | if: github.repository_owner == 'kaplayjs' 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Check if there are changes in examples/ folder between HEAD and last commit 21 | id: check_changes 22 | run: | 23 | CHANGES=$(git diff --name-only HEAD^ HEAD -- examples/ | paste -sd "," -) 24 | echo "changes=$CHANGES" >> $GITHUB_OUTPUT 25 | - name: Dispatch Event 26 | uses: actions/github-script@v6 27 | if: steps.check_changes.outputs.changes != '' 28 | with: 29 | github-token: ${{ secrets.PAT_TOKEN }} 30 | script: | 31 | await github.rest.actions.createWorkflowDispatch({ 32 | owner: 'kaplayjs', 33 | repo: 'kaplayground', 34 | workflow_id: 'sync-submodules.yml', 35 | ref: 'master' 36 | }) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | node_modules/ 4 | .idea/ 5 | desktop/ 6 | .env 7 | src-tauri/target/ 8 | .replit 9 | replit.nix 10 | /.obsidian 11 | .pnpm-store 12 | tsconfig.vitest-temp.json 13 | src/index.ts -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifierEnding": "minimal", 4 | "exportall.config.folderListener": [ 5 | "/src/app", 6 | "/src/assets", 7 | "/src/components", 8 | "/src/components/draw", 9 | "/src/components/level", 10 | "/src/components/misc", 11 | "/src/components/physics", 12 | "/src/components/transform", 13 | "/src/ecs/components" 14 | ], 15 | "typescript.preferences.importModuleSpecifier": "shortest", 16 | "cSpell.words": [ 17 | "KAPLAY" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/snippets.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | "Add experimental message": { 3 | "scope": "javascript,typescript", 4 | // kpd = KAPLAY Development 5 | "prefix": "kpd-experimental", 6 | "body": [ 7 | "@experimental This feature is in experimental phase, it will be fully released in v3001.1.0", 8 | ], 9 | "description": "Add @experimental tag in JSDoc" 10 | }, 11 | "Add documentation template": { 12 | "scope": "typescript", 13 | "prefix": "kpd-doc", 14 | "body": [ 15 | "/**", 16 | "* Attach and render a circle to a Game Object.", 17 | "*", 18 | "* @param radius - The radius of the circle.", 19 | "* @param opt - Options for the circle component. See {@link CircleCompOpt `CircleCompOpt`}.", 20 | "*", 21 | "* @example", 22 | "* ```js", 23 | "* ```", 24 | "*", 25 | "* @returns The circle comp.", 26 | "* @since v3000.0", 27 | "* @group Components", 28 | "*/" 29 | ], 30 | "description": "Add the documentation template in a doc entry." 31 | } 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 KAPLAY Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "node_modules/@kaplayjs/dprint-config/dprint.json" 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import jsdoc from "eslint-plugin-jsdoc"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default tseslint.config( 7 | tseslint.configs.base, 8 | { 9 | plugins: { 10 | jsdoc, 11 | }, 12 | rules: { 13 | "jsdoc/require-hyphen-before-param-description": "error", 14 | "jsdoc/check-alignment": "warn", 15 | "jsdoc/check-tag-names": ["error", { 16 | "definedTags": ["group", "experimental"], 17 | }], 18 | "jsdoc/sort-tags": ["error", { 19 | tagSequence: [{ 20 | tags: [ 21 | "deprecated", 22 | ], 23 | }, { 24 | tags: [ 25 | "param", 26 | ], 27 | }, { 28 | tags: [ 29 | "template", 30 | ], 31 | }, { 32 | tags: [ 33 | "example", 34 | ], 35 | }, { 36 | tags: [ 37 | "default", 38 | "readonly", 39 | "static", 40 | "returns", 41 | "since", 42 | "group", 43 | "experimental", 44 | ], 45 | }], 46 | }], 47 | }, 48 | files: ["./src/**/*.ts"], 49 | }, 50 | ); 51 | -------------------------------------------------------------------------------- /examples/basicEventsObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Object Events 3 | * @description Handling events on objects 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 40 10 | */ 11 | 12 | // Before getting more into components, will see more about Game Objects [💡] 13 | 14 | /* 💡 Game Object Raw 💡 15 | It's the serie of methods and properties that are available on all game objects, 16 | like exists(), destroy(), 17 | */ 18 | 19 | /* 💡 Game Object Events 💡 20 | Game objects have their own events based on it's lifecycle and state. 21 | */ 22 | 23 | kaplay(); 24 | 25 | loadSprite("bean", "/sprites/bean.png"); 26 | 27 | const obj = add([ 28 | sprite("bean"), 29 | anchor("center"), 30 | pos(100, 100), 31 | ]); 32 | 33 | // obj.onUpdate() is called every frame while the object exists. 34 | obj.onUpdate(() => { 35 | debug.log("hi!"); 36 | }); 37 | 38 | // obj.onDestroy() is called when the object is destroyed. 39 | obj.onDestroy(() => { 40 | debug.log("bye cruel world!"); 41 | }); 42 | 43 | // We can destroy the object using obj.destroy() 44 | obj.onKeyPress("space", () => { 45 | addKaboom(obj.pos); // addKaboom() is a function that creates an explosion at the given position. 46 | obj.destroy(); 47 | }); 48 | 49 | // Notice we will use global onKeyPress() to handle the event. 50 | // This is because we want this running even if the object is destroyed. 51 | onKeyPress("enter", () => { 52 | // We can also if the object exists using obj.exists() 53 | if (obj.exists()) { 54 | console.log("The object exists!"); 55 | } 56 | else { 57 | console.log("The object doesn't exist!"); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /examples/basicsCompRender.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Render Components 3 | * @description Learn about components that render something 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 60 10 | */ 11 | 12 | // All basic rendering related components. This include rendering shapes, 13 | // sprites and text and also modifying their properties, with color(), outline() 14 | // and opacity(). 15 | 16 | kaplay({ 17 | font: "happy", 18 | background: ["#873e84"], 19 | }); 20 | 21 | loadSprite("bean", "/sprites/bean.png"); 22 | // load a bitmap font 23 | loadBitmapFont("happy", "/fonts/happy_28x36.png", 28, 36); 24 | 25 | // Basic Rendering Components 26 | 27 | // sprite() is a component that renders a sprite. 28 | add([ 29 | sprite("bean"), 30 | pos(10, 10), 31 | ]); 32 | 33 | // rect() is a component that renders a rectangle. 34 | add([ 35 | rect(60, 60), 36 | pos(110, 10), 37 | // add an outline to the shape. 38 | outline(4), 39 | // set the color 40 | color(109, 128, 250), // r g b 41 | ]); 42 | 43 | // circle() is a component that renders a circle. 44 | add([ 45 | circle(30), 46 | pos(210, 10), 47 | outline(4), 48 | // set the pivot point, as default is center for circle() 49 | anchor("topleft"), 50 | // set the opacity 51 | opacity(0.5), 52 | ]); 53 | 54 | // text() is a component that renders a text. 55 | add([ 56 | text("hi!"), 57 | pos(310, 10), 58 | ]); 59 | 60 | // polygon() is a component that renders a polygon. 61 | add([ 62 | polygon([vec2(5, 0), vec2(50, 5), vec2(50, 50), vec2(0, 20)]), 63 | pos(410, 10), 64 | outline(4), 65 | color("#d46eb3"), // hex colors! 66 | ]); 67 | -------------------------------------------------------------------------------- /examples/basicsGlobals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file The KAPLAY Context 3 | * @description What's KAPLAY Context? What are globals? 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 10 10 | */ 11 | 12 | // Learn about globals [💡] 13 | 14 | /* 💡 Context in Globals 💡 15 | The KAPLAY Context functions/variables are available in the global namespace by 16 | default, making them easy to use and great for learning. 17 | */ 18 | 19 | // You can use the context without adding them to the global namespace: 20 | 21 | // 1. Capture the context in a variable 22 | const k = kaplay({ 23 | global: false, // 2. Disable the export to the global namespace 24 | }); 25 | 26 | k.debug.log("Hello from the context!"); 27 | 28 | // This is a safe and better practice for larger projects. 29 | // Read more about this at: https://kaplayjs.com/guides/optimization/#avoid-global-namespace 30 | -------------------------------------------------------------------------------- /examples/basicsObject.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Add game objects 3 | * @description How to create game objects 4 | * @difficulty 0 5 | * @tags basics, gobj 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 20 10 | * 11 | * RIP: add.js, 5year being the first example. 12 | */ 13 | 14 | // Adding game objects to screen [💡, 🥊] 15 | 16 | /* 💡 Game Objects 💡 17 | A Game Object is the base of all entities in KAPLAY. It's composed by components. 18 | */ 19 | 20 | /* 💡 Components 💡 21 | A component is a function that adds behaviour, methods and properties to a 22 | Game Object. 23 | */ 24 | 25 | kaplay(); 26 | 27 | // We load sprites with loadSprite(), it receives the asset name and the path to 28 | // the asset. 29 | loadSprite("bean", "/sprites/bean.png"); // Bean, the frog! 30 | 31 | // You can add a game object to the screen using the add() function, which takes 32 | // an array of components. We can store the result object in a variable. 33 | const bean = add([ 34 | sprite("bean"), // sprite() is a rendering component that draws a sprite on the screen. 35 | // pos(120, 80), // pos() is a component that sets the position of the object on the screen. 36 | ]); 37 | 38 | debug.log(bean.sprite); // Prints the sprite name 39 | 40 | /* 🥊 Challenge 🥊 41 | Try uncommenting the pos() component, it receives and x and y coordinates 42 | and sets the position of the object on the screen. 43 | 44 | Then try logging the position coordinates of the object using 45 | 46 | debug.log(bean.pos); 47 | */ 48 | -------------------------------------------------------------------------------- /examples/basicsStart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Create your first game 3 | * @description All you need to know to get started 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | * @group basics 9 | * @groupOrder 0 10 | */ 11 | 12 | // Get started with KAPLAY [💡] 13 | 14 | /* 💡 KAPLAY Context 💡 15 | The kaplay() function is the entry point to your game. It sets up the game and 16 | exports all the functions and variables you need to use in your game, like 17 | add(), loadSprite(), debug.log(), pos(), and many more. This is called the context. 18 | */ 19 | 20 | // We initialize the context 21 | kaplay({ 22 | // We can optionally pass options to it! 23 | background: "#5ba675", // Set the background color 24 | }); 25 | 26 | // Now you have access to the context functions: 27 | debug.log("Hello from game!"); 28 | -------------------------------------------------------------------------------- /examples/burp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Burp 3 | * @description How to use burp, the engine core 4 | * @difficulty 0 5 | * @tags basics, audio 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // Core KAPLAY features [💡] 11 | 12 | /* 💡 Burp 💡 13 | Burp is the engine core, it handles everything. 14 | Is not needed in most cases, unless you don't want your game crashing 15 | or freezing randomly. 16 | */ 17 | 18 | // Start the game in burp mode 19 | kaplay({ 20 | burp: true, 21 | background: "cc425e", 22 | }); 23 | 24 | // "b" triggers burp() on press 25 | add([ 26 | text("Press B to burp"), 27 | anchor("center"), 28 | pos(width() / 2, height() / 2), 29 | ]); 30 | 31 | // burp() on click / tap for our friends on mobile 32 | onClick(() => burp()); 33 | -------------------------------------------------------------------------------- /examples/button.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Buttons 3 | * @description How to create simple UI buttons 4 | * @difficulty 1 5 | * @tags ui 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Simple UI and setup for buttons 11 | 12 | kaplay({ 13 | background: [135, 62, 132], 14 | }); 15 | 16 | // reset cursor to default on frame start for easier cursor management 17 | onUpdate(() => setCursor("default")); 18 | 19 | // Function that adds a button to the game with a given text, position and function 20 | function addButton( 21 | txt = "start game", 22 | p = vec2(200, 100), 23 | f = () => debug.log("hello"), 24 | ) { 25 | // add a parent background object 26 | const btn = add([ 27 | rect(240, 80, { radius: 8 }), 28 | pos(p), 29 | area(), 30 | scale(1), 31 | anchor("center"), 32 | outline(4), 33 | color(255, 255, 255), 34 | ]); 35 | 36 | // add a child object that displays the text 37 | btn.add([ 38 | text(txt), 39 | anchor("center"), 40 | color(0, 0, 0), 41 | ]); 42 | 43 | // onHoverUpdate() comes from area() component 44 | // it runs every frame when the object is being hovered 45 | btn.onHoverUpdate(() => { 46 | const t = time() * 10; 47 | btn.color = hsl2rgb((t / 10) % 1, 0.6, 0.7); 48 | btn.scale = vec2(1.2); 49 | setCursor("pointer"); 50 | }); 51 | 52 | // onHoverEnd() comes from area() component 53 | // it runs once when the object stopped being hovered 54 | btn.onHoverEnd(() => { 55 | btn.scale = vec2(1); 56 | btn.color = rgb(); 57 | }); 58 | 59 | // onClick() comes from area() component 60 | // it runs once when the object is clicked 61 | btn.onClick(f); 62 | 63 | return btn; 64 | } 65 | 66 | // Adds the buttons with the function we added 67 | addButton("Start", vec2(200, 100), () => debug.log("oh hi")); 68 | addButton("Quit", vec2(200, 200), () => debug.log("bye")); 69 | -------------------------------------------------------------------------------- /examples/children.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Children 3 | * @description How to create children on game objects. 4 | * @difficulty 1 5 | * @tags basics, gobj 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadSprite("bean", "/sprites/bean.png"); 13 | loadSprite("ghosty", "/sprites/ghosty.png"); 14 | 15 | // Adds the nucleus for the other children to get added to, it just means this is their parent 16 | const nucleus = add([ 17 | sprite("ghosty"), 18 | pos(center()), 19 | anchor("center"), 20 | ]); 21 | 22 | // Add children 23 | for (let i = 12; i < 24; i++) { 24 | nucleus.add([ 25 | sprite("bean"), 26 | rotate(0), 27 | anchor(vec2(i).scale(0.25)), 28 | { 29 | speed: i * 8, 30 | }, 31 | ]); 32 | } 33 | 34 | // Runs every frame 35 | nucleus.onUpdate(() => { 36 | nucleus.pos = mousePos(); 37 | 38 | // update children 39 | nucleus.children.forEach((child) => { 40 | child.angle += child.speed * dt(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/collisionshapes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Collision Shapes 3 | * @description How to create different collision shapes. 4 | * @difficulty 1 5 | * @tags basics, comps, physics 6 | * @minver 4000.0 7 | * @category basics 8 | */ 9 | 10 | // How kaplay handles collisions with different shapes 11 | kaplay(); 12 | 13 | // Set the gravity acceleration (pixels per second) 14 | setGravity(300); 15 | 16 | // Adds a ground 17 | add([ 18 | pos(0, 400), 19 | rect(width(), 40), 20 | area(), 21 | body({ isStatic: true }), 22 | ]); 23 | 24 | // Continuous shapes 25 | loop(1, () => { 26 | // Adds an object with a random shape 27 | add([ 28 | pos(width() / 2 + rand(-50, 50), 100), 29 | choose([ 30 | rect(20, 20), 31 | circle(10), 32 | ellipse(20, 10), 33 | polygon([vec2(-15, 10), vec2(0, -10), vec2(15, 10)]), 34 | ]), 35 | color(RED), 36 | area(), 37 | body(), 38 | offscreen({ destroy: true, distance: 10 }), 39 | ]); 40 | 41 | // getTreeRoot() gets the root of the game, the object that holds every other object 42 | // This line basically means that if there are more than 20 objects, we destroy the last one 43 | if (getTreeRoot().children.length > 20) { 44 | destroy(getTreeRoot().children[1]); 45 | } 46 | 47 | /* The previous code can also be written as 48 | if (get("*").length > 20) { 49 | destroy(get("*")[1]); 50 | } 51 | */ 52 | }); 53 | -------------------------------------------------------------------------------- /examples/customCompDebug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Custom Components Debug 3 | * @description How to display custom properties in custom components 4 | * @difficulty 0 5 | * @tags comps, debug 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Press F1 to enable debug mode and see how custom properties appear in the 11 | // inspect box 12 | 13 | kaplay({ scale: 2 }); 14 | 15 | loadBean(); 16 | 17 | // Our custom component 18 | function customComp() { 19 | return { 20 | id: "compy", 21 | customing: true, 22 | inspect() { 23 | return `customing: ${this.customing}`; 24 | }, 25 | }; 26 | } 27 | 28 | const bean = add([ 29 | sprite("bean"), 30 | anchor("center"), 31 | pos(center()), 32 | area(), 33 | customComp(), 34 | ]); 35 | 36 | bean.onClick(() => { 37 | bean.customing = !bean.customing; 38 | }); 39 | -------------------------------------------------------------------------------- /examples/drawon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Drawon Component 3 | * @description How to use Frame Buffers 4 | * @difficulty 2 5 | * @tags draw 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | onLoad(() => { 15 | const pic = new Picture(); 16 | 17 | const cached = add([ 18 | drawon(pic, { childrenOnly: true, refreshOnly: true }), 19 | picture(pic), 20 | { 21 | draw() { 22 | console.log("draw parent"); 23 | }, 24 | }, 25 | ]); 26 | 27 | const screen = new Rect(vec2(100, 100), width() - 200, height() - 200); 28 | 29 | function addBean() { 30 | const bean = cached.add([ 31 | pos(screen.random()), 32 | sprite("bean"), 33 | { 34 | draw() { 35 | console.log("draw child"); 36 | }, 37 | }, 38 | ]); 39 | cached.refresh(); 40 | } 41 | 42 | addBean(); 43 | 44 | onKeyPress("space", addBean); 45 | }); 46 | -------------------------------------------------------------------------------- /examples/drawoncanvas.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | loadBean(); 4 | 5 | loadShader( 6 | "invert", 7 | null, 8 | ` 9 | uniform float u_time; 10 | 11 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 12 | vec4 c = def_frag(); 13 | float t = (sin(u_time * 4.0) + 1.0) / 2.0; 14 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t); 15 | } 16 | `, 17 | ); 18 | 19 | onLoad(() => { 20 | const canvas = makeCanvas(width(), height()); 21 | 22 | const cached = add([ 23 | drawon(canvas.fb, { childrenOnly: true, refreshOnly: true }), 24 | { 25 | draw() { 26 | drawCanvas({ 27 | canvas, 28 | shader: "invert", 29 | uniform: { "u_time": time() }, 30 | }); 31 | }, 32 | }, 33 | shader("invert"), 34 | ]); 35 | 36 | const screen = new Rect(vec2(100, 100), width() - 200, height() - 200); 37 | 38 | function addBean() { 39 | const bean = cached.add([ 40 | pos(screen.random()), 41 | sprite("bean"), 42 | ]); 43 | cached.refresh(); 44 | } 45 | 46 | addBean(); 47 | 48 | onKeyPress("space", addBean); 49 | }); 50 | -------------------------------------------------------------------------------- /examples/fadeIn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fade In 3 | * @description How to fade in game objects 4 | * @difficulty 0 5 | * @tags visual, effects 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // How to fade in an object 11 | 12 | kaplay(); 13 | 14 | loadBean(); 15 | 16 | // spawn a bean that takes a second to fade in 17 | const bean = add([ 18 | sprite("bean"), 19 | pos(120, 80), 20 | opacity(1), // opacity() component gives it opacity which is required for fadeIn 21 | ]); 22 | 23 | bean.fadeIn(1); // makes it fade in 24 | 25 | // spawn another bean that takes 5 seconds to fade in halfway 26 | // SPOOKY! 27 | let spookyBean = add([ 28 | sprite("bean"), 29 | pos(240, 80), 30 | opacity(0.5), // opacity() component gives it opacity which is required for fadeIn (set to 0.5 so it will be half transparent) 31 | ]); 32 | 33 | spookyBean.fadeIn(5); // makes it fade in (set to 5 so that it takes 5 seconds to fade in) 34 | -------------------------------------------------------------------------------- /examples/flamebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Flame bar 3 | * @description How to make mario-like flamebars 4 | * @difficulty 1 5 | * @tags visual, effects 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Mario-like flamebar 11 | 12 | // Start kaplay 13 | kaplay(); 14 | 15 | // Load assets 16 | loadSprite("bean", "/sprites/bean.png"); 17 | loadSprite("pineapple", "/sprites/pineapple.png"); 18 | 19 | // Define player movement speed 20 | const SPEED = 320; 21 | 22 | // Add player game object 23 | const player = add([ 24 | sprite("bean"), 25 | pos(80, 40), 26 | area(), 27 | ]); 28 | 29 | // Player movement 30 | onKeyDown("left", () => { 31 | player.move(-SPEED, 0); 32 | }); 33 | 34 | onKeyDown("right", () => { 35 | player.move(SPEED, 0); 36 | }); 37 | 38 | onKeyDown("up", () => { 39 | player.move(0, -SPEED); 40 | }); 41 | 42 | onKeyDown("down", () => { 43 | player.move(0, SPEED); 44 | }); 45 | 46 | // Function to add a flamebar 47 | function addFlamebar(position = vec2(0), angle = 0, num = 6) { 48 | // Create a parent game object for position and rotation 49 | const flameHead = add([ 50 | pos(position), 51 | rotate(angle), 52 | ]); 53 | 54 | // Add each section of flame as children 55 | for (let i = 0; i < num; i++) { 56 | flameHead.add([ 57 | sprite("pineapple"), 58 | pos(0, i * 48), 59 | area(), 60 | anchor("center"), 61 | "flame", 62 | ]); 63 | } 64 | 65 | // The flame head's rotation will affect all its children 66 | flameHead.onUpdate(() => { 67 | flameHead.angle += dt() * 60; 68 | }); 69 | 70 | return flameHead; 71 | } 72 | 73 | addFlamebar(vec2(200, 300), -60); 74 | addFlamebar(vec2(480, 100), 180); 75 | addFlamebar(vec2(400, 480), 0); 76 | 77 | // Game over if player touches a flame 78 | player.onCollide("flame", () => { 79 | addKaboom(player.pos); 80 | player.destroy(); 81 | }); 82 | -------------------------------------------------------------------------------- /examples/fonts/04b03.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/04b03.ttf -------------------------------------------------------------------------------- /examples/fonts/04b03_6x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/04b03_6x8.png -------------------------------------------------------------------------------- /examples/fonts/4x4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/4x4.png -------------------------------------------------------------------------------- /examples/fonts/FlowerSketches.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/FlowerSketches.ttf -------------------------------------------------------------------------------- /examples/fonts/Romantique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/Romantique.ttf -------------------------------------------------------------------------------- /examples/fonts/apl386.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/apl386.ttf -------------------------------------------------------------------------------- /examples/fonts/cga_8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/cga_8x8.png -------------------------------------------------------------------------------- /examples/fonts/happy_28x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/happy_28x36.png -------------------------------------------------------------------------------- /examples/fonts/proggy_7x13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/proggy_7x13.png -------------------------------------------------------------------------------- /examples/fonts/sink_6x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/sink_6x8.png -------------------------------------------------------------------------------- /examples/fonts/unscii_8x8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/unscii_8x8.png -------------------------------------------------------------------------------- /examples/fonts/zpix.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/fonts/zpix.ttf -------------------------------------------------------------------------------- /examples/frames.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Frames 3 | * @description How to define frames in an animation. 4 | * @difficulty 0 5 | * @tags basics, animation 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay({ 12 | scale: 4, 13 | background: [0, 0, 0], 14 | }); 15 | 16 | // https:/ / (0x72).itch.io / dungeontileset - ii; 17 | loadSpriteAtlas("/sprites/dungeon.png", { 18 | wizard: { 19 | x: 128, 20 | y: 140, 21 | width: 144, 22 | height: 28, 23 | sliceX: 9, 24 | anims: { 25 | bouncy: { 26 | frames: [8, 5, 0, 3, 2, 3, 0, 5], 27 | speed: 10, 28 | loop: true, 29 | }, 30 | }, 31 | }, 32 | }); 33 | 34 | add([ 35 | sprite("wizard", { anim: "bouncy" }), 36 | pos(100, 100), 37 | ]); 38 | -------------------------------------------------------------------------------- /examples/friction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Friction 3 | * @description How to apply friction to objects 4 | * @difficulty 0 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay({ scale: 0.5 }); 11 | loadSprite("bean", "/sprites/bean.png"); 12 | loadSprite("grass", "/sprites/grass.png"); 13 | setGravity(3200); 14 | const level = addLevel([ 15 | "@ = ", 16 | "", 17 | "======= ", 18 | " = ", 19 | " =========", 20 | ], { 21 | tileWidth: 64, 22 | tileHeight: 64, 23 | pos: vec2(100, 200), 24 | tiles: { 25 | "@": () => [ 26 | sprite("bean"), 27 | area({ friction: 0.02, restitution: 0 }), 28 | body(), 29 | anchor("bot"), 30 | "player", 31 | ], 32 | "=": () => [ 33 | sprite("grass"), 34 | area({ friction: 0.02, restitution: 0 }), 35 | body({ isStatic: true }), 36 | anchor("bot"), 37 | ], 38 | }, 39 | }); 40 | 41 | const player = level.get("player")[0]; 42 | player.vel.x = 480; 43 | debug.log(player.friction); 44 | -------------------------------------------------------------------------------- /examples/gamepad.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Gamepad 3 | * @description How to manage gamepad input. 4 | * @difficulty 0 5 | * @tags basics, input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay({ 11 | background: [0, 0, 0], 12 | }); 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | setGravity(2400); 16 | 17 | scene("nogamepad", () => { 18 | add([ 19 | text("Gamepad not found.\nConnect a gamepad and press a button!", { 20 | width: width() - 80, 21 | align: "center", 22 | }), 23 | pos(center()), 24 | anchor("center"), 25 | ]); 26 | onGamepadConnect(() => { 27 | go("game"); 28 | }); 29 | }); 30 | 31 | scene("game", () => { 32 | const player = add([ 33 | pos(center()), 34 | anchor("center"), 35 | sprite("bean"), 36 | area(), 37 | body(), 38 | ]); 39 | 40 | // platform 41 | add([ 42 | pos(0, height()), 43 | anchor("botleft"), 44 | rect(width(), 140), 45 | area(), 46 | body({ isStatic: true }), 47 | ]); 48 | 49 | onGamepadButtonPress((b) => { 50 | debug.log(b); 51 | }); 52 | 53 | onGamepadButtonPress(["south", "west"], () => { 54 | player.jump(); 55 | }); 56 | 57 | onGamepadStick("left", (v) => { 58 | player.move(v.x * 400, 0); 59 | }); 60 | 61 | onGamepadDisconnect(() => { 62 | go("nogamepad"); 63 | }); 64 | }); 65 | 66 | if (getGamepads().length > 0) { 67 | go("game"); 68 | } 69 | else { 70 | go("nogamepad"); 71 | } 72 | -------------------------------------------------------------------------------- /examples/gamepadMulti.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Multi-Gamepad 3 | * @description How to manage multiple gamepads at the same. 4 | * @difficulty 1 5 | * @tags input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | setGravity(2400); 12 | setBackground(0, 0, 0); 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | const playerColors = [ 16 | rgb(252, 53, 43), 17 | rgb(0, 255, 0), 18 | rgb(43, 71, 252), 19 | rgb(255, 255, 0), 20 | rgb(255, 0, 255), 21 | ]; 22 | 23 | let playerCount = 0; 24 | 25 | function addPlayer(gamepad) { 26 | const player = add([ 27 | pos(center()), 28 | anchor("center"), 29 | sprite("bean"), 30 | color(playerColors[playerCount]), 31 | area(), 32 | body(), 33 | doubleJump(), 34 | ]); 35 | 36 | playerCount++; 37 | 38 | onUpdate(() => { 39 | const leftStick = gamepad.getStick("left"); 40 | 41 | if (gamepad.isPressed("south")) { 42 | player.doubleJump(); 43 | } 44 | 45 | if (leftStick.x !== 0) { 46 | player.move(leftStick.x * 400, 0); 47 | } 48 | }); 49 | } 50 | 51 | // platform 52 | add([ 53 | pos(0, height()), 54 | anchor("botleft"), 55 | rect(width(), 140), 56 | area(), 57 | body({ isStatic: true }), 58 | ]); 59 | 60 | // add players on every gamepad connect 61 | onGamepadConnect((gamepad) => { 62 | addPlayer(gamepad); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/gravity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Gravity 3 | * @description How to make use of gravity in KAPLAY. 4 | * @difficulty 0 5 | * @tags basics, physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | // Responding to gravity & jumping 11 | 12 | // Start kaplay 13 | kaplay(); 14 | 15 | // Load assets 16 | loadSprite("bean", "/sprites/bean.png"); 17 | 18 | // Set the gravity acceleration (pixels per second) 19 | setGravity(1600); 20 | 21 | // Add player game object 22 | const player = add([ 23 | sprite("bean"), 24 | pos(center()), 25 | area(), 26 | // body() component gives the ability to respond to gravity 27 | body(), 28 | ]); 29 | 30 | onKeyPress("space", () => { 31 | // .isGrounded() is provided by body() 32 | if (player.isGrounded()) { 33 | // .jump() is provided by body() 34 | player.jump(); 35 | } 36 | }); 37 | 38 | // .onGround() is provided by body(). It registers an event that runs whenever player hits the ground. 39 | player.onGround(() => { 40 | debug.log("ouch"); 41 | }); 42 | 43 | // Accelerate falling when player holding down arrow key 44 | onKeyDown("down", () => { 45 | if (!player.isGrounded()) { 46 | player.vel.y += dt() * 1200; 47 | } 48 | }); 49 | 50 | // Jump higher if space is held 51 | onKeyDown("space", () => { 52 | if (!player.isGrounded() && player.vel.y < 0) { 53 | player.vel.y -= dt() * 600; 54 | } 55 | }); 56 | 57 | // Add a platform to hold the player 58 | add([ 59 | rect(width(), 48), 60 | outline(4), 61 | area(), 62 | pos(0, height() - 48), 63 | // Give objects a body() component if you don't want other solid objects pass through 64 | body({ isStatic: true }), 65 | ]); 66 | 67 | add([ 68 | text("Press space key", { width: width() / 2 }), 69 | pos(12, 12), 70 | ]); 71 | 72 | // Check out https://kaplayjs.com/doc/BodyComp for everything body() provides 73 | -------------------------------------------------------------------------------- /examples/hover.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Hover 3 | * @description Understand the different hover methods 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // Differeces between onHover and onHoverUpdate 11 | 12 | kaplay({ 13 | scale: 2, 14 | }); 15 | 16 | loadSprite("bean", "/sprites/bean.png"); 17 | 18 | add([ 19 | text("onHover()\nonHoverEnd()"), 20 | pos(80, 80), 21 | ]); 22 | 23 | add([ 24 | text("onHoverUpdate()"), 25 | pos(340, 80), 26 | ]); 27 | 28 | const redBean = add([ 29 | sprite("bean"), 30 | color(RED), 31 | pos(130, 180), 32 | anchor("center"), 33 | area(), 34 | ]); 35 | 36 | const blueBean = add([ 37 | sprite("bean"), 38 | color(BLUE), 39 | pos(380, 180), 40 | anchor("center"), 41 | area(), 42 | ]); 43 | 44 | // Only runs once when bean is hovered, and when bean is unhovered 45 | redBean.onHover(() => { 46 | debug.log("red bean hovered"); 47 | 48 | redBean.color = GREEN; 49 | }); 50 | redBean.onHoverEnd(() => { 51 | debug.log("red bean unhovered"); 52 | 53 | redBean.color = RED; 54 | }); 55 | 56 | // Runs every frame when blue bean is hovered 57 | blueBean.onHoverUpdate(() => { 58 | const t = time() * 10; 59 | blueBean.color = rgb( 60 | wave(0, 255, t), 61 | wave(0, 255, t + 2), 62 | wave(0, 255, t + 4), 63 | ); 64 | 65 | debug.log("blue bean on hover"); 66 | }); 67 | 68 | let cameraScale = 1; 69 | 70 | onScroll((delta) => { 71 | cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y)); 72 | setCamScale(cameraScale); 73 | }); 74 | -------------------------------------------------------------------------------- /examples/kaboom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Kaboom! 3 | * @description How to KABOOM! 4 | * @difficulty 0 5 | * @tags basics, effects 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | // KAPLAY born as the direct successor of Kaboom.js! 11 | 12 | kaplay(); 13 | 14 | // The addKaboom() effect is a fun way to add explosions to your game. 15 | addKaboom(center()); 16 | 17 | onKeyPress(() => addKaboom(mousePos())); 18 | onMouseMove(() => addKaboom(mousePos())); 19 | -------------------------------------------------------------------------------- /examples/layer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Layer 3 | * @description How to use the z() component for layering 4 | * @difficulty 0 5 | * @tags basics, effects, comps 6 | * @ver 4000.0.0-alpha.18 7 | * @minver 3001.0 8 | * @category basics 9 | */ 10 | 11 | kaplay(); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | // Create a parent node that won't be affected by camera (fixed) and will be drawn on top (z of 100) 16 | const ui = add([ 17 | fixed(), 18 | z(100), 19 | ]); 20 | 21 | // This will be on top, because the parent node has z(100) 22 | ui.add([ 23 | sprite("bean"), 24 | scale(5), 25 | color(0, 0, 255), 26 | ]); 27 | 28 | add([ 29 | sprite("bean"), 30 | pos(100, 100), 31 | scale(5), 32 | ]); 33 | -------------------------------------------------------------------------------- /examples/layers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Layers 3 | * @description How to use layer system 4 | * @difficulty 0 5 | * @tags basics, effects, ui 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | layers(["bg", "game", "ui"], "game"); 13 | 14 | // bg layer 15 | add([ 16 | rect(width(), height()), 17 | layer("bg"), 18 | color(rgb(64, 128, 255)), 19 | // opacity(0.5) 20 | ]).add([text("BG")]); 21 | 22 | // game layer explicit 23 | add([ 24 | pos(width() / 5, height() / 5), 25 | rect(width() / 3, height() / 3), 26 | layer("game"), 27 | color(rgb(255, 128, 64)), 28 | ]).add([text("GAME")]); 29 | 30 | // game layer implicit 31 | add([ 32 | pos(3 * width() / 5, 3 * height() / 5), 33 | rect(width() / 3, height() / 3), 34 | color(rgb(255, 128, 64)), 35 | ]).add([pos(width() / 3, height() / 3), text("GAME"), anchor("botright")]); 36 | 37 | // ui layer 38 | add([ 39 | pos(center()), 40 | rect(width() / 2, height() / 2), 41 | anchor("center"), 42 | color(rgb(64, 255, 128)), 43 | ]).add([text("UI"), anchor("center")]); 44 | -------------------------------------------------------------------------------- /examples/levelRaycast.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Level Raycast 3 | * @description How to use raycasts in a level environment 4 | * @difficulty 1 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay({ 11 | background: [31, 16, 42], 12 | }); 13 | 14 | loadSprite("grass", "/sprites/grass.png"); 15 | 16 | const level = addLevel([ 17 | "===", 18 | "= =", 19 | "===", 20 | ], { 21 | tileWidth: 64, 22 | tileHeight: 64, 23 | pos: vec2(256, 128), 24 | tiles: { 25 | "=": () => [ 26 | sprite("grass"), 27 | area(), 28 | ], 29 | }, 30 | }); 31 | level.use(rotate(45)); 32 | 33 | onLoad(() => { 34 | level.spawn([ 35 | pos( 36 | level.tileWidth() * 1.5, 37 | level.tileHeight() * 1.5, 38 | ), 39 | circle(6), 40 | color("#ea6262"), 41 | { 42 | add() { 43 | const rayHit = level.raycast( 44 | this.pos, 45 | Vec2.fromAngle(0).scale(100), 46 | ); 47 | 48 | debug.log( 49 | `${rayHit != null} ${ 50 | rayHit && rayHit.object ? rayHit.object.id : -1 51 | }`, 52 | ); 53 | }, 54 | }, 55 | ]); 56 | }); 57 | 58 | debug.inspect = true; 59 | -------------------------------------------------------------------------------- /examples/lifespan.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Lifespan 3 | * @description How to use the lifespan component. 4 | * @difficulty 0 5 | * @tags basics, comps 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay(); 11 | 12 | const sprites = [ 13 | "apple", 14 | "heart", 15 | "coin", 16 | "meat", 17 | "lightening", 18 | ]; 19 | 20 | sprites.forEach((spr) => { 21 | loadSprite(spr, `/sprites/${spr}.png`); 22 | }); 23 | 24 | setGravity(800); 25 | 26 | // Spawn one object every 0.1 second 27 | loop(0.1, () => { 28 | // Compose object properties with components 29 | const item = add([ 30 | pos(center()), 31 | sprite(choose(sprites)), 32 | anchor("center"), 33 | scale(rand(0.5, 1)), 34 | area({ collisionIgnore: ["fruit"] }), 35 | body(), 36 | // lifespan() comp destroys the object after desired seconds 37 | lifespan(1, { 38 | // it will fade after 0.5 seconds 39 | fade: 0.5, 40 | }), 41 | opacity(1), 42 | move(choose([LEFT, RIGHT]), rand(60, 240)), 43 | "fruit", 44 | ]); 45 | 46 | item.jump(rand(320, 640)); 47 | }); 48 | -------------------------------------------------------------------------------- /examples/livequery.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Live query 3 | * @description How to live update a get() action. 4 | * @difficulty 1 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // How to keep a get() always updated 12 | 13 | kaplay(); 14 | 15 | loadSprite("ghosty", "/sprites/ghosty.png"); 16 | 17 | const q = get("area", { liveUpdate: true }); 18 | 19 | loop(5, () => { 20 | if (q.length < 10) { 21 | const x = rand(0, width()); 22 | const y = rand(0, height()); 23 | 24 | const ghost = add([ 25 | sprite("ghosty"), 26 | pos(x, y), 27 | area(), 28 | timer(), 29 | color(WHITE), 30 | "touchable", 31 | ]); 32 | ghost.wait(5, () => { 33 | ghost.unuse("area"); 34 | ghost.untag("touchable"); 35 | ghost.use(color(RED)); 36 | ghost.wait(5, () => { 37 | ghost.use(area()); 38 | ghost.tag("touchable"); 39 | ghost.use(color(WHITE)); 40 | }); 41 | }); 42 | } 43 | }); 44 | 45 | onClick("touchable", (ghost) => { 46 | ghost.destroy(); 47 | }); 48 | 49 | loop(1, () => { 50 | debug.log(`There are ${q.length} touchable ghosts`); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/movement.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Movement 3 | * @description How to make basic movement. 4 | * @difficulty 0 5 | * @tags basics, input 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | // Input handling and basic player movement 10 | 11 | // Start kaplay 12 | kaplay(); 13 | 14 | // Load assets 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | // Define player movement speed (pixels per second) 18 | const SPEED = 320; 19 | 20 | // Add player game object 21 | const player = add([ 22 | sprite("bean"), 23 | // center() returns the center point vec2(width() / 2, height() / 2) 24 | pos(center()), 25 | ]); 26 | 27 | // onKeyDown() registers an event that runs every frame as long as user is holding a certain key 28 | onKeyDown("left", () => { 29 | // .move() is provided by pos() component, move by pixels per second 30 | player.move(-SPEED, 0); 31 | }); 32 | 33 | onKeyDown("right", () => { 34 | player.move(SPEED, 0); 35 | }); 36 | 37 | onKeyDown("up", () => { 38 | player.move(0, -SPEED); 39 | }); 40 | 41 | onKeyDown("down", () => { 42 | player.move(0, SPEED); 43 | }); 44 | 45 | // onClick() registers an event that runs once when left mouse is clicked 46 | onClick(() => { 47 | // .moveTo() is provided by pos() component, changes the position 48 | player.moveTo(mousePos()); 49 | }); 50 | 51 | add([ 52 | // text() component is similar to sprite() but renders text 53 | text("Press arrow keys", { width: width() / 2 }), 54 | pos(12, 12), 55 | ]); 56 | -------------------------------------------------------------------------------- /examples/objectInit.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/objectInit.js -------------------------------------------------------------------------------- /examples/onLoadError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Load Error 3 | * @description How to handle errors on load. 4 | * @difficulty 1 5 | * @tags loading 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | // this will not load (uncomment) 13 | // loadSprite("bobo", "notavalidURL"); 14 | 15 | // process the load error 16 | // you decide whether to ignore it, or throw an error and halt the game 17 | onLoadError((name, asset) => { 18 | // ignore it: 19 | debug.error(`${name} failed to load: ${asset.error}`); 20 | // throw an error: 21 | throw asset.error; 22 | }); 23 | -------------------------------------------------------------------------------- /examples/out.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Out of Screen 3 | * @description How to handle objects that are out of screen. 4 | * @difficulty 1 5 | * @tags comps 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // detect if obj is out of screen 12 | 13 | kaplay(); 14 | 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | // custom comp 18 | function handleout() { 19 | return { 20 | id: "handleout", 21 | require: ["pos"], 22 | update() { 23 | const spos = this.screenPos(); 24 | if ( 25 | spos.x < 0 26 | || spos.x > width() 27 | || spos.y < 0 28 | || spos.y > height() 29 | ) { 30 | // triggers a custom event when out 31 | this.trigger("out"); 32 | } 33 | }, 34 | }; 35 | } 36 | 37 | const SPEED = 640; 38 | 39 | function shoot() { 40 | const center = vec2(width() / 2, height() / 2); 41 | const mpos = mousePos(); 42 | add([ 43 | pos(center), 44 | sprite("bean"), 45 | anchor("center"), 46 | handleout(), 47 | "bean", 48 | { dir: mpos.sub(center).unit() }, 49 | ]); 50 | } 51 | 52 | onKeyPress("space", shoot); 53 | onClick(shoot); 54 | 55 | onUpdate("bean", (m) => { 56 | m.move(m.dir.scale(SPEED)); 57 | }); 58 | 59 | // binds a custom event "out" to tag group "bean" 60 | on("out", "bean", (m) => { 61 | addKaboom(m.pos); 62 | destroy(m); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/particle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Particle 3 | * @description How to use particles() 4 | * @difficulty 1 5 | * @tags effects 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group particles 9 | * @groupOrder 0 10 | */ 11 | 12 | // Creating particles using Particle Component 13 | 14 | kaplay(); 15 | 16 | loadSprite("star", "./sprites/particle_star_filled.png"); 17 | 18 | onLoad(() => { 19 | go("game"); 20 | }); 21 | 22 | function woah() { 23 | const parts = add([ 24 | pos(center()), 25 | particles({ 26 | max: 20, 27 | speed: [50, 100], 28 | angle: [0, 360], 29 | angularVelocity: [45, 90], 30 | lifeTime: [1.0, 1.5], 31 | colors: [rgb(128, 128, 255), WHITE], 32 | opacities: [0.1, 1.0, 0.0], 33 | scales: [1, 2, 1], 34 | texture: getSprite("star").data.tex, 35 | quads: [getSprite("star").data.frames[0]], 36 | }, { 37 | lifetime: 1.5, 38 | rate: 0, 39 | direction: -90, 40 | spread: 40, 41 | }), 42 | ]); 43 | 44 | parts.emit(20); 45 | } 46 | 47 | scene("game", () => { 48 | onKeyPress("space", () => { 49 | woah(); 50 | }); 51 | 52 | onMousePress(() => { 53 | woah(); 54 | }); 55 | 56 | add([ 57 | text("press space for particles"), 58 | ]); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/particleTrail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Particle Trail 3 | * @description How to do a mouse-following trail with particles() 4 | * @difficulty 1 5 | * @tags effects 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group particles 9 | * @groupOrder 1 10 | */ 11 | 12 | kaplay(); 13 | 14 | loadSprite("hexagon", "./sprites/particle_hexagon_filled.png"); 15 | 16 | onLoad(() => { 17 | const trail = add([ 18 | pos(), 19 | particles({ 20 | max: 20, 21 | speed: [200, 250], 22 | lifeTime: [0.2, 0.75], 23 | colors: [WHITE], 24 | opacities: [1.0, 0.0], 25 | angle: [0, 360], 26 | texture: getSprite("hexagon").data.tex, 27 | quads: [getSprite("hexagon").data.frames[0]], 28 | }, { 29 | rate: 5, 30 | direction: -90, 31 | spread: 2, 32 | }), 33 | ]); 34 | 35 | onMouseMove((pos, delta) => { 36 | trail.emitter.position = pos; 37 | trail.emit(1); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /examples/patrol.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Patrol 3 | * @description How to patrol a sprite. 4 | * @difficulty 1 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | const bean = add([ 15 | sprite("bean"), 16 | pos(40, 30), 17 | patrol({ 18 | waypoints: [ 19 | vec2(100, 100), 20 | vec2(120, 170), 21 | vec2(50, 50), 22 | vec2(300, 100), 23 | ], 24 | }), 25 | ]); 26 | 27 | bean.onPatrolFinished(gb => { 28 | debug.log(`Bean reached the end of the patrol at ${gb.pos.x}, ${gb.pos.y}`); 29 | }); 30 | -------------------------------------------------------------------------------- /examples/picture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Picture 3 | * @description How to store static drawing data 4 | * @difficulty 0 5 | * @tags effects, optimization, draw 6 | * @minver 4000.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Optimized drawing using Picture API [💡] 12 | 13 | /* 💡 Picture API 💡 14 | The Picture API is a way to store static drawing data. It allows you to 15 | draw a lot of sprites in a single draw call. This is useful for 16 | optimizing performance and reducing draw calls. 17 | */ 18 | 19 | kaplay(); 20 | 21 | loadSprite("bean", "sprites/bean.png"); 22 | 23 | onLoad(() => { 24 | // We create the picture 25 | beginPicture(new Picture()); 26 | 27 | for (let i = 0; i < 16; i++) { 28 | for (let j = 0; j < 16; j++) { 29 | // We draw a sprite at the given position, so we 30 | // "store" the drawing data in the picture 31 | drawSprite({ 32 | pos: vec2(64 + i * 32, 64 + j * 32), 33 | sprite: "bean", 34 | }); 35 | } 36 | } 37 | 38 | // We end the picture 39 | const picture = endPicture(); 40 | 41 | // Now all we have to do is draw the picture, this picture is cached 42 | // by default, so it's the most optimized way to draw a lot of sprites, 43 | // maps, etc. 44 | onDraw(() => { 45 | drawPicture(picture, { 46 | pos: vec2(400, 0), 47 | angle: 45, 48 | scale: vec2(0.5), 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/pong.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Pong 3 | * @description How to make pong in KAPLAY. 4 | * @difficulty 1 5 | * @tags basics, game 6 | * @minver 3001.0 7 | * @category games 8 | */ 9 | 10 | kaplay({ 11 | background: [255, 255, 128], 12 | }); 13 | 14 | // add paddles 15 | add([ 16 | pos(40, 0), 17 | rect(20, 80), 18 | outline(4), 19 | anchor("center"), 20 | area(), 21 | "paddle", 22 | ]); 23 | 24 | add([ 25 | pos(width() - 40, 0), 26 | rect(20, 80), 27 | outline(4), 28 | anchor("center"), 29 | area(), 30 | "paddle", 31 | ]); 32 | 33 | // move paddles with mouse 34 | onUpdate("paddle", (p) => { 35 | p.pos.y = mousePos().y; 36 | }); 37 | 38 | // score counter 39 | let score = 0; 40 | 41 | add([ 42 | text(score.toString()), 43 | pos(center()), 44 | anchor("center"), 45 | z(50), 46 | { 47 | update() { 48 | this.text = score.toString(); 49 | }, 50 | }, 51 | ]); 52 | 53 | // ball 54 | let speed = 480; 55 | 56 | const ball = add([ 57 | pos(center()), 58 | circle(16), 59 | outline(4), 60 | area({ shape: new Rect(vec2(-16), 32, 32) }), 61 | { vel: Vec2.fromAngle(rand(-20, 20)) }, 62 | ]); 63 | 64 | // move ball, bounce it when touche horizontal edges, respawn when touch vertical edges 65 | ball.onUpdate(() => { 66 | ball.move(ball.vel.scale(speed)); 67 | if (ball.pos.x < 0 || ball.pos.x > width()) { 68 | score = 0; 69 | ball.pos = center(); 70 | ball.vel = Vec2.fromAngle(rand(-20, 20)); 71 | speed = 320; 72 | } 73 | if (ball.pos.y < 0 || ball.pos.y > height()) { 74 | ball.vel.y = -ball.vel.y; 75 | } 76 | }); 77 | 78 | // bounce when touch paddle 79 | ball.onCollide("paddle", (p) => { 80 | speed += 60; 81 | ball.vel = Vec2.fromAngle(ball.pos.angle(p.pos)); 82 | score++; 83 | }); 84 | -------------------------------------------------------------------------------- /examples/restitution.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Restitution 3 | * @description How to make objects bounce 4 | * @difficulty 0 5 | * @tags physics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay({ scale: 0.5 }); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | loadSprite("grass", "/sprites/grass.png"); 15 | 16 | setGravity(3200); 17 | 18 | const level = addLevel([ 19 | "@ = ", 20 | "", 21 | "======= ", 22 | " = ", 23 | " =========", 24 | ], { 25 | tileWidth: 64, 26 | tileHeight: 64, 27 | pos: vec2(100, 200), 28 | tiles: { 29 | "@": () => [ 30 | sprite("bean"), 31 | area({ friction: 0, restitution: 1 }), 32 | body(), 33 | anchor("bot"), 34 | "player", 35 | ], 36 | "=": () => [ 37 | sprite("grass"), 38 | area({ friction: 0, restitution: 1 }), 39 | body({ isStatic: true }), 40 | anchor("bot"), 41 | ], 42 | }, 43 | }); 44 | 45 | const player = level.get("player")[0]; 46 | player.vel.x = 480; 47 | debug.log(player.friction); 48 | -------------------------------------------------------------------------------- /examples/scaletest.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | letterbox: true, 3 | width: 640, 4 | height: 360, 5 | }); 6 | 7 | // load assets 8 | loadSprite("bean", "/sprites/bean.png"); 9 | 10 | onDraw(() => { 11 | drawSprite({ 12 | pos: vec2(40, 10), 13 | color: RED, 14 | sprite: "bean", 15 | scale: vec2(1), 16 | }); 17 | }); 18 | 19 | add([pos(vec2(10, 10)), sprite("bean")]); 20 | -------------------------------------------------------------------------------- /examples/shader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Shaders 3 | * @description How to use shaders 4 | * @difficulty 1 5 | * @tags basics, effects 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | // Custom shader 12 | kaplay(); 13 | 14 | loadSprite("bean", "/sprites/bean.png"); 15 | 16 | // Load a shader with custom fragment shader code 17 | // 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 18 | // There's also the def_frag() function which returns the default fragment color 19 | loadShader( 20 | "invert", 21 | null, 22 | ` 23 | uniform float u_time; 24 | 25 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 26 | vec4 c = def_frag(); 27 | float t = (sin(u_time * 4.0) + 1.0) / 2.0; 28 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), t); 29 | } 30 | `, 31 | ); 32 | 33 | add([ 34 | sprite("bean"), 35 | pos(80, 40), 36 | scale(8), 37 | // Use the shader with shader() component and pass uniforms 38 | shader("invert", () => ({ 39 | "u_time": time(), 40 | })), 41 | ]); 42 | -------------------------------------------------------------------------------- /examples/shaders/blink.frag: -------------------------------------------------------------------------------- 1 | uniform float u_time; 2 | uniform vec3 u_fore; 3 | uniform vec3 u_back; 4 | 5 | float map(float x, float a, float b, float c, float d) { 6 | return c + (d - c) * (x - a) / (b - a); 7 | } 8 | 9 | #define pi 3.141592653589 10 | 11 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 12 | vec4 pix = texture2D(tex, uv); 13 | float mixvalue = smoothstep(.3, .7, map(sin(u_time * pi), -1., 1., 0., 1.)); 14 | if (pix.a > 0.) mixvalue = 1. - mixvalue; 15 | return mix(vec4(u_back / 255., 1.), vec4(u_fore / 255., 1.), mixvalue); 16 | } 17 | -------------------------------------------------------------------------------- /examples/shaders/crt.frag: -------------------------------------------------------------------------------- 1 | uniform float u_flatness; 2 | uniform float u_scanline_height; 3 | uniform float u_screen_height; 4 | 5 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 6 | vec2 center = vec2(0.5, 0.5); 7 | vec2 off_center = uv - center; 8 | off_center *= 1.0 + pow(abs(off_center.yx), vec2(u_flatness)); 9 | vec2 uv2 = center + off_center; 10 | if (uv2.x > 1.0 || uv2.x < 0.0 || uv2.y > 1.0 || uv2.y < 0.0) { 11 | return vec4(0.0, 0.0, 0.0, 1.0); 12 | } else { 13 | vec4 c = vec4(texture2D(tex, uv2).rgb, 1.0); 14 | float fv = fract(uv2.y * 120.0); 15 | fv = min(1.0, 0.8 + 0.5 * min(fv, 1.0 - fv)); 16 | c.rgb *= fv; 17 | return c; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/shaders/invert.frag: -------------------------------------------------------------------------------- 1 | uniform float u_invert; 2 | 3 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 4 | vec4 c = def_frag(); 5 | return mix(c, vec4(1.0 - c.r, 1.0 - c.g, 1.0 - c.b, c.a), u_invert); 6 | } 7 | -------------------------------------------------------------------------------- /examples/shaders/light.frag: -------------------------------------------------------------------------------- 1 | uniform float u_radius; 2 | uniform float u_blur; 3 | uniform vec2 u_resolution; 4 | uniform vec2 u_mouse; 5 | 6 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 7 | if (u_radius <= 0.0) return def_frag(); 8 | vec2 center = u_mouse / u_resolution * vec2(1, -1) + vec2(0, 1); 9 | float dist = distance(uv * u_resolution, center * u_resolution); 10 | float alpha = smoothstep(max((dist - u_radius) / u_blur, 0.0), 0.0, 1.0); 11 | return mix(vec4(0, 0, 0, 1), def_frag(), 1.0 - alpha); 12 | } 13 | -------------------------------------------------------------------------------- /examples/shaders/pixelate.frag: -------------------------------------------------------------------------------- 1 | uniform float u_size; 2 | uniform vec2 u_resolution; 3 | 4 | // TODO: this is causing some extra pixels to appear at screen edge 5 | vec4 frag(vec2 pos, vec2 uv, vec4 color, sampler2D tex) { 6 | if (u_size <= 0.0) return def_frag(); 7 | vec2 nsize = vec2(u_size / u_resolution.x, u_size / u_resolution.y); 8 | float x = floor(uv.x / nsize.x + 0.5); 9 | float y = floor(uv.y / nsize.y + 0.5); 10 | vec4 c = texture2D(tex, vec2(x, y) * nsize); 11 | return c * color; 12 | } 13 | -------------------------------------------------------------------------------- /examples/shapeRect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Rect 3 | * @description The different options for the rect() component. 4 | * @difficulty 0 5 | * @tags basics, draw 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | kaplay(); 12 | 13 | add([ 14 | rect(100, 100, { radius: 20 }), 15 | pos(100, 100), 16 | rotate(0), 17 | anchor("center"), 18 | ]); 19 | 20 | add([ 21 | rect(100, 100, { radius: [10, 20, 30, 40] }), 22 | pos(250, 100), 23 | rotate(0), 24 | anchor("center"), 25 | ]); 26 | 27 | add([ 28 | rect(100, 100, { radius: [0, 20, 0, 20] }), 29 | pos(400, 100), 30 | rotate(0), 31 | anchor("center"), 32 | ]); 33 | 34 | add([ 35 | rect(100, 100, { radius: 20 }), 36 | pos(100, 250), 37 | rotate(0), 38 | anchor("center"), 39 | outline(4, BLACK), 40 | ]); 41 | 42 | add([ 43 | rect(100, 100, { radius: [10, 20, 30, 40] }), 44 | pos(250, 250), 45 | rotate(0), 46 | anchor("center"), 47 | outline(4, BLACK), 48 | ]); 49 | 50 | add([ 51 | rect(100, 100, { radius: [0, 20, 0, 20] }), 52 | pos(400, 250), 53 | rotate(0), 54 | anchor("center"), 55 | outline(4, BLACK), 56 | ]); 57 | -------------------------------------------------------------------------------- /examples/size.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Keep Aspect Ratio 3 | * @description How to keep aspect ratio using letterbox 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category basics 8 | */ 9 | 10 | kaplay({ 11 | // without specifying "width" and "height", kaplay will size to the container (document.body by default) 12 | width: 200, 13 | height: 100, 14 | // "letterbox" makes stretching keeps aspect ratio (leaves black bars on empty spaces), have no effect without "stretch" 15 | letterbox: true, 16 | }); 17 | 18 | loadBean(); 19 | 20 | add([ 21 | sprite("bean"), 22 | ]); 23 | 24 | onClick(() => addKaboom(mousePos())); 25 | -------------------------------------------------------------------------------- /examples/slice9.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Slice-9 3 | * @description How to make use of slice-9 sprites. 4 | * @difficulty 1 5 | * @tags animation, draw 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // 9 slice sprite scaling 12 | 13 | kaplay(); 14 | 15 | // Load a sprite that's made for 9 slice scaling 16 | loadSprite("9slice", "/sprites/9slice.png", { 17 | // Define the slice by the margins of 4 sides 18 | slice9: { 19 | left: 32, 20 | right: 32, 21 | top: 32, 22 | bottom: 32, 23 | }, 24 | }); 25 | 26 | const g = add([ 27 | pos(center()), 28 | sprite("9slice"), 29 | anchor("center"), 30 | ]); 31 | 32 | onMouseMove(() => { 33 | const size = mousePos().sub(center()); 34 | // Scaling the image will keep the aspect ratio of the sliced frames 35 | g.width = Math.abs(size.x) * 2; 36 | g.height = Math.abs(size.y) * 2; 37 | }); 38 | -------------------------------------------------------------------------------- /examples/sokoban.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Sokoban 3 | * @description How to make a sokoban-like game in KAPLAY. 4 | * @difficulty 1 5 | * @tags game 6 | * @minver 3001.0 7 | * @category games 8 | */ 9 | 10 | kaplay({ 11 | background: [45, 33, 51], 12 | }); 13 | 14 | loadSprite("bean", "/sprites/bean.png"); 15 | loadSprite("grass", "/sprites/grass.png"); 16 | loadSprite("steel", "/sprites/steel.png"); 17 | 18 | const level = addLevel([ 19 | ".......", 20 | ".p d.", 21 | ". b b .", 22 | ". .", 23 | ".......", 24 | ], { 25 | tileWidth: 64, 26 | tileHeight: 64, 27 | tiles: { 28 | p: () => [sprite("bean"), "player"], 29 | b: () => [sprite("grass"), "box"], 30 | ".": () => [sprite("steel"), "wall"], 31 | }, 32 | }); 33 | 34 | const player = level.get("player")[0]; 35 | 36 | const hasTag = (objs, tag) => objs.findIndex(obj => obj.is(tag)) !== -1; 37 | 38 | const moveObj = (obj, dir) => { 39 | if (dir.x == 1) obj.moveRight(); 40 | if (dir.x == -1) obj.moveLeft(); 41 | if (dir.y == 1) obj.moveDown(); 42 | if (dir.y == -1) obj.moveUp(); 43 | }; 44 | 45 | const move = (dir) => { 46 | const moveTo = player.tilePos.add(dir); 47 | const occupant = level.getAt(moveTo); 48 | 49 | if (hasTag(occupant, "wall")) { 50 | return; 51 | } 52 | 53 | if (hasTag(occupant, "box")) { 54 | const boxMoveTo = occupant[0].tilePos.add(dir); 55 | const boxOccupant = level.getAt(boxMoveTo); 56 | 57 | if (boxOccupant.length !== 0) { 58 | return; 59 | } 60 | 61 | moveObj(occupant[0], dir); 62 | } 63 | 64 | moveObj(player, dir); 65 | }; 66 | 67 | onKeyPress("d", () => { 68 | move(vec2(1, 0)); 69 | }); 70 | 71 | onKeyPress("a", () => { 72 | move(vec2(-1, 0)); 73 | }); 74 | 75 | onKeyPress("w", () => { 76 | move(vec2(0, -1)); 77 | }); 78 | 79 | onKeyPress("s", () => { 80 | move(vec2(0, 1)); 81 | }); 82 | -------------------------------------------------------------------------------- /examples/sounds/OtherworldlyFoe.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/OtherworldlyFoe.mp3 -------------------------------------------------------------------------------- /examples/sounds/bean_voice.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/bean_voice.wav -------------------------------------------------------------------------------- /examples/sounds/bell.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/bell.mp3 -------------------------------------------------------------------------------- /examples/sounds/blip.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/blip.mp3 -------------------------------------------------------------------------------- /examples/sounds/bug.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/bug.mp3 -------------------------------------------------------------------------------- /examples/sounds/burp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/burp.mp3 -------------------------------------------------------------------------------- /examples/sounds/computer.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/computer.mp3 -------------------------------------------------------------------------------- /examples/sounds/danger.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/danger.mp3 -------------------------------------------------------------------------------- /examples/sounds/dune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/dune.mp3 -------------------------------------------------------------------------------- /examples/sounds/error.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/error.mp3 -------------------------------------------------------------------------------- /examples/sounds/explode.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/explode.mp3 -------------------------------------------------------------------------------- /examples/sounds/hit.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/hit.mp3 -------------------------------------------------------------------------------- /examples/sounds/kaboom2000.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/kaboom2000.mp3 -------------------------------------------------------------------------------- /examples/sounds/knock.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/knock.ogg -------------------------------------------------------------------------------- /examples/sounds/mark_voice.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/mark_voice.wav -------------------------------------------------------------------------------- /examples/sounds/mystic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/mystic.mp3 -------------------------------------------------------------------------------- /examples/sounds/notice.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/notice.mp3 -------------------------------------------------------------------------------- /examples/sounds/off.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/off.mp3 -------------------------------------------------------------------------------- /examples/sounds/portal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/portal.mp3 -------------------------------------------------------------------------------- /examples/sounds/powerup.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/powerup.mp3 -------------------------------------------------------------------------------- /examples/sounds/robot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/robot.mp3 -------------------------------------------------------------------------------- /examples/sounds/score.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/score.mp3 -------------------------------------------------------------------------------- /examples/sounds/shoot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/shoot.mp3 -------------------------------------------------------------------------------- /examples/sounds/signal.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/signal.mp3 -------------------------------------------------------------------------------- /examples/sounds/spring.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/spring.mp3 -------------------------------------------------------------------------------- /examples/sounds/weak.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/weak.mp3 -------------------------------------------------------------------------------- /examples/sounds/wooosh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sounds/wooosh.mp3 -------------------------------------------------------------------------------- /examples/sprites/9slice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/9slice.png -------------------------------------------------------------------------------- /examples/sprites/YOSHI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/YOSHI.png -------------------------------------------------------------------------------- /examples/sprites/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/apple.png -------------------------------------------------------------------------------- /examples/sprites/bag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/bag.png -------------------------------------------------------------------------------- /examples/sprites/bean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/bean.png -------------------------------------------------------------------------------- /examples/sprites/bobo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/bobo.png -------------------------------------------------------------------------------- /examples/sprites/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/boom.png -------------------------------------------------------------------------------- /examples/sprites/brick_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/brick_wall.png -------------------------------------------------------------------------------- /examples/sprites/btfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/btfly.png -------------------------------------------------------------------------------- /examples/sprites/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/cloud.png -------------------------------------------------------------------------------- /examples/sprites/coin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/coin.png -------------------------------------------------------------------------------- /examples/sprites/cursor_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/cursor_default.png -------------------------------------------------------------------------------- /examples/sprites/cursor_pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/cursor_pointer.png -------------------------------------------------------------------------------- /examples/sprites/dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/dino.png -------------------------------------------------------------------------------- /examples/sprites/door.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/door.png -------------------------------------------------------------------------------- /examples/sprites/dungeon-dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/dungeon-dino.png -------------------------------------------------------------------------------- /examples/sprites/dungeon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/dungeon.png -------------------------------------------------------------------------------- /examples/sprites/egg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/egg.png -------------------------------------------------------------------------------- /examples/sprites/egg_crack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/egg_crack.png -------------------------------------------------------------------------------- /examples/sprites/ghostiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/ghostiny.png -------------------------------------------------------------------------------- /examples/sprites/ghosty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/ghosty.png -------------------------------------------------------------------------------- /examples/sprites/gigagantrum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/gigagantrum.png -------------------------------------------------------------------------------- /examples/sprites/grab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/grab.png -------------------------------------------------------------------------------- /examples/sprites/grape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/grape.png -------------------------------------------------------------------------------- /examples/sprites/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/grass.png -------------------------------------------------------------------------------- /examples/sprites/gun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/gun.png -------------------------------------------------------------------------------- /examples/sprites/heart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/heart.png -------------------------------------------------------------------------------- /examples/sprites/jumpy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/jumpy.png -------------------------------------------------------------------------------- /examples/sprites/k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/k.png -------------------------------------------------------------------------------- /examples/sprites/ka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/ka.png -------------------------------------------------------------------------------- /examples/sprites/kaboom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/kaboom.png -------------------------------------------------------------------------------- /examples/sprites/kaplay-dino.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/kaplay-dino.png -------------------------------------------------------------------------------- /examples/sprites/key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/key.png -------------------------------------------------------------------------------- /examples/sprites/lightening.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/lightening.png -------------------------------------------------------------------------------- /examples/sprites/mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/mark.png -------------------------------------------------------------------------------- /examples/sprites/meat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/meat.png -------------------------------------------------------------------------------- /examples/sprites/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/moon.png -------------------------------------------------------------------------------- /examples/sprites/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/mushroom.png -------------------------------------------------------------------------------- /examples/sprites/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/note.png -------------------------------------------------------------------------------- /examples/sprites/particle_circle_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/particle_circle_filled.png -------------------------------------------------------------------------------- /examples/sprites/particle_circle_outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/particle_circle_outline.png -------------------------------------------------------------------------------- /examples/sprites/particle_hexagon_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/particle_hexagon_filled.png -------------------------------------------------------------------------------- /examples/sprites/particle_star_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/particle_star_filled.png -------------------------------------------------------------------------------- /examples/sprites/pineapple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/pineapple.png -------------------------------------------------------------------------------- /examples/sprites/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/portal.png -------------------------------------------------------------------------------- /examples/sprites/spike.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/spike.png -------------------------------------------------------------------------------- /examples/sprites/spritemerge_chest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/spritemerge_chest.png -------------------------------------------------------------------------------- /examples/sprites/spritemerge_corpus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/spritemerge_corpus.png -------------------------------------------------------------------------------- /examples/sprites/steel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/steel.png -------------------------------------------------------------------------------- /examples/sprites/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/sun.png -------------------------------------------------------------------------------- /examples/sprites/sword.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/sword.png -------------------------------------------------------------------------------- /examples/sprites/tga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/tga.png -------------------------------------------------------------------------------- /examples/sprites/watermelon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/watermelon.png -------------------------------------------------------------------------------- /examples/sprites/you.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/you.png -------------------------------------------------------------------------------- /examples/sprites/zombean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/sprites/zombean.png -------------------------------------------------------------------------------- /examples/textInput.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text Input 3 | * @description How to take input and display it on text 4 | * @difficulty 0 5 | * @tags basics, ui 6 | * @minver 3001.0 7 | * @category basics 8 | * @test 9 | */ 10 | 11 | // Using textInput() component to catch user text input easily 12 | 13 | kaplay({ font: "happy", background: "#a6555f" }); 14 | 15 | loadHappy(); 16 | 17 | // We will ask something! 18 | add([ 19 | pos(width() / 2, 50), 20 | text("What's your favorite KAPLAY Crew member", { 21 | // Responsive friendly 22 | align: "center", 23 | width: width(), 24 | }), 25 | anchor("top"), 26 | ]); 27 | 28 | // This object will catch user input 29 | const crew = add([ 30 | text(""), 31 | // We pass true so it focus by default. You can also do crew.hasFocus = true; 32 | textInput(true, 20), // <- 20 chars at max 33 | pos(width() / 2, height() / 2), 34 | anchor("center"), 35 | ]); 36 | 37 | // Our response 38 | const response = add([ 39 | text("", { 40 | align: "center", 41 | width: width(), 42 | }), 43 | anchor("bot"), 44 | pos(width() / 2, height() - 50), 45 | ]); 46 | 47 | // Updating the response, depending on input 48 | response.onUpdate(() => { 49 | if (crew.text == "") { 50 | response.text = "..."; 51 | } 52 | else if (crew.text.toLowerCase() === "mark") { 53 | response.text = `Yep. Mark the best`; 54 | } 55 | else { 56 | response.text = `I like ${crew.text}, but Mark is better`; 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /examples/tiled.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tiled 3 | * @description How to use sprites in tiled mode 4 | * @difficulty 1 5 | * @tags basics, game 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Tiled sprites! 12 | 13 | kaplay(); 14 | 15 | loadSprite("bean", "/sprites/bean.png"); 16 | 17 | add([ 18 | pos(150, 150), 19 | sprite("bean", { 20 | tiled: true, 21 | width: 200, 22 | height: 200, 23 | }), 24 | anchor("center"), 25 | ]); 26 | 27 | debug.inspect = true; 28 | -------------------------------------------------------------------------------- /examples/timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Timer 3 | * @description How to make count time in KAPLAY. 4 | * @difficulty 0 5 | * @tags basics 6 | * @minver 3001.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | kaplay(); 12 | 13 | loadSprite("bean", "/sprites/bean.png"); 14 | 15 | // Execute something after every 0.5 seconds. 16 | loop(0.5, () => { 17 | const bean = add([ 18 | sprite("bean"), 19 | pos(rand(vec2(0), vec2(width(), height()))), 20 | ]); 21 | 22 | // Execute something after 3 seconds. 23 | wait(3, () => { 24 | destroy(bean); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "../dist/declaration/global.d.ts" 5 | ], 6 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 7 | "module": "ESNext", /* Specify what module code is generated. */ 8 | "moduleResolution": "Bundler", 9 | "noEmit": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "allowJs": true, 12 | "moduleDetection": "force", 13 | "noImplicitAny": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/tweenEasings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Tween Easings 3 | * @description See all different easings in tween() 4 | * @difficulty 0 5 | * @tags animation 6 | * @minver 3001.0 7 | * @category concepts 8 | * @group tween 9 | * @groupOrder 1 10 | */ 11 | 12 | // See all the tweeen easings 13 | 14 | kaplay({ 15 | background: "#a32858", 16 | font: "happy", 17 | }); 18 | 19 | loadHappy(); 20 | loadSprite("bean", "/sprites/bean.png"); 21 | 22 | const DURATION = 1; 23 | const EASINGS = Object.keys(easings); 24 | let curEasing = 0; 25 | 26 | const bean = add([ 27 | sprite("bean"), 28 | scale(2), 29 | pos(center()), 30 | rotate(0), 31 | anchor("center"), 32 | ]); 33 | 34 | const label = add([ 35 | text(EASINGS[curEasing], { size: 64 }), 36 | pos(24, 24), 37 | ]); 38 | 39 | add([ 40 | text("Click anywhere & use arrow keys", { width: width() }), 41 | anchor("botleft"), 42 | pos(24, height() - 24), 43 | ]); 44 | 45 | onKeyPress(["left", "a"], () => { 46 | curEasing = curEasing === 0 ? EASINGS.length - 1 : curEasing - 1; 47 | label.text = EASINGS[curEasing]; 48 | }); 49 | 50 | onKeyPress(["right", "d"], () => { 51 | curEasing = (curEasing + 1) % EASINGS.length; 52 | label.text = EASINGS[curEasing]; 53 | }); 54 | 55 | let curTween = null; 56 | 57 | onMousePress(() => { 58 | const easeType = EASINGS[curEasing]; 59 | 60 | // Stop the previos tween 61 | if (curTween) curTween.cancel(); 62 | 63 | // start the tween 64 | curTween = tween( 65 | // start value (accepts number, Vec2 and Color) 66 | bean.pos, 67 | // destination value 68 | mousePos(), 69 | // duration (in seconds) 70 | DURATION, 71 | // how value should be updated 72 | (val) => bean.pos = val, 73 | // interpolation function (defaults to easings.linear) 74 | easings[easeType], 75 | ); 76 | }); 77 | -------------------------------------------------------------------------------- /examples/video.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Video 3 | * @description How to play videos 4 | * @difficulty 0 5 | * @tags animation 6 | * @minver 4000.0 7 | * @category concepts 8 | * @test 9 | */ 10 | 11 | // Playing videos (🥊 included) 12 | 13 | kaplay({ scale: 2, background: "#a32858", font: "happy" }); 14 | 15 | loadHappy(); 16 | 17 | const vid = add([ 18 | pos(center()), 19 | // video() fetches the resource, we have to pass URL 20 | video("/videos/dance.mp4", { 21 | width: 320, 22 | height: 200, 23 | }), 24 | anchor("center"), 25 | ]); 26 | 27 | onClick(() => { 28 | vid.play(); 29 | }); 30 | 31 | /* 🥊 Challenge #1 🥊 32 | Videos are cool! Try replacing the video url by this one: 33 | 34 | //videos/3d.mp4 35 | 36 | And see how your mind blows 37 | */ 38 | 39 | // Other visual elements 40 | 41 | add([ 42 | pos(center().x, 50), 43 | text("click to play video"), 44 | anchor("center"), 45 | ]); 46 | -------------------------------------------------------------------------------- /examples/videos/3d.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/videos/3d.mp4 -------------------------------------------------------------------------------- /examples/videos/dance.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/examples/videos/dance.mp4 -------------------------------------------------------------------------------- /help.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/makeman@*/help.schema.json", 3 | "name": "KAPLAY", 4 | "description": "KAPLAY is a JavaScript & TypeScript game library that helps you make games fast and fun!", 5 | "styles": { 6 | "titleFont": "Standard", 7 | "titleColor": { 8 | "r": 171, 9 | "g": 221, 10 | "b": 100 11 | }, 12 | "titleBackground": { 13 | "r": 31, 14 | "g": 16, 15 | "b": 42 16 | } 17 | }, 18 | "targets": { 19 | "dev": { 20 | "description": "Run the development server" 21 | }, 22 | "win:dev": { 23 | "description": "Run the development server\n\tbut with a cmd capable environment variable" 24 | }, 25 | "build": { 26 | "description": "Build the project for production" 27 | }, 28 | "build:fast": { 29 | "description": "Build the project for development\n\tDoes not build .d.ts files" 30 | }, 31 | "check": { 32 | "description": "tsc type check" 33 | }, 34 | "fmt": { 35 | "description": "Format the code using dprint" 36 | }, 37 | "test": { 38 | "description": "Test using puppeteer" 39 | }, 40 | "test:vite": { 41 | "description": "Test using vitest" 42 | }, 43 | "doc-dts": { 44 | "description": "Generate .d.ts bundle" 45 | }, 46 | "desktop": { 47 | "description": "Run the desktop testing system with tauri" 48 | }, 49 | "prepare": { 50 | "description": "Alias for build" 51 | }, 52 | "publish:next": { 53 | "description": "Publish to npm with the next tag" 54 | }, 55 | "help": { 56 | "description": "Show this help message" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kaplay.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/kaplay.webp -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { build } from "./lib/build.js"; 4 | import { genGlobalDTS } from "./lib/globaldts.js"; 5 | 6 | await build(); 7 | await genGlobalDTS(); 8 | -------------------------------------------------------------------------------- /scripts/buildFast.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { build } from "./lib/build.js"; 4 | 5 | await build(true); 6 | -------------------------------------------------------------------------------- /scripts/constants.js: -------------------------------------------------------------------------------- 1 | export const DIST_DIR = "dist"; 2 | export const SRC_DIR = "src"; 3 | export const SRC_PATH = `${SRC_DIR}/kaplay.ts`; 4 | export const SRC_PATH_MINI = `${SRC_DIR}/modules/mini.ts`; 5 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { dev } from "./dev/dev.js"; 4 | 5 | await dev(); 6 | -------------------------------------------------------------------------------- /scripts/dev/dev.js: -------------------------------------------------------------------------------- 1 | // Used in npm dev script 2 | // @ts-check 3 | 4 | import esbuild from "esbuild"; 5 | import { config, fmts } from "../lib/build.js"; 6 | import { serve } from "./serve.js"; 7 | 8 | export async function dev() { 9 | serve(); 10 | 11 | const ctx = await esbuild.context({ 12 | ...config, 13 | ...fmts("kaplay")[0], 14 | sourcemap: true, 15 | minify: false, 16 | keepNames: true, 17 | plugins: [ 18 | { 19 | name: "logger", 20 | setup(b) { 21 | b.onEnd(() => { 22 | console.log(`-> ${fmts("kaplay")[0].outfile}`); 23 | }); 24 | }, 25 | }, 26 | ], 27 | }); 28 | 29 | await ctx.watch(); 30 | } 31 | -------------------------------------------------------------------------------- /scripts/git-split.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -ne 2 ] ; then 4 | echo "Usage: $0 original copy" 5 | exit 1 6 | fi 7 | 8 | if ! git diff --quiet || ! git diff --cached --quiet; then 9 | echo "Error: Unstaged changes present. Please commit or stash them before running this script." 10 | exit 1 11 | fi 12 | 13 | git mv "$1" "$2" 14 | git commit -n -m "Split history $1 to $2 - rename file to target-name" 15 | REV=`git rev-parse HEAD` 16 | git reset --hard HEAD^ 17 | git mv "$1" temp 18 | git commit -n -m "Split history $1 to $2 - rename source-file to temp" 19 | git merge $REV 20 | git commit -a -n -m "Split history $1 to $2 - resolve conflict and keep both files" 21 | git mv temp "$1" 22 | git commit -n -m "Split history $1 to $2 - restore name of source-file" 23 | -------------------------------------------------------------------------------- /scripts/lib/util.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import cp from "child_process"; 4 | import fs from "fs/promises"; 5 | 6 | /** 7 | * Write a file to disk 8 | * 9 | * @param {string} path 10 | * @param {string} content 11 | */ 12 | export async function writeFile(path, content) { 13 | await fs.writeFile(path, content); 14 | console.log(`-> ${path}`); 15 | } 16 | 17 | /** 18 | * Check if the current platform is windows 19 | * 20 | * @type {boolean} 21 | */ 22 | export const isWindows = /^win/.test(process.platform); 23 | 24 | export const c = (n, msg) => `\x1b[${n}m${msg}\x1b[0m`; 25 | 26 | /** 27 | * Wait for a certain amount of time 28 | * 29 | * @param {number} time 30 | * @returns {Promise} 31 | */ 32 | export function wait(time) { 33 | return new Promise((resolve) => setTimeout(() => resolve(), time)); 34 | } 35 | 36 | /** 37 | * Execute a command 38 | * 39 | * @param {string} cmd 40 | * @param {string[]} args 41 | * @param {import("child_process").SpawnOptions} opts 42 | */ 43 | export const exec = async (cmd, args, opts) => 44 | new Promise((resolve, reject) => { 45 | const proc = cp.spawn(isWindows ? cmd + ".cmd" : cmd, args, opts); 46 | proc.on("exit", resolve); 47 | proc.on("error", reject); 48 | }); 49 | 50 | /** 51 | * Check if a file exists 52 | * 53 | * @param {string} path 54 | * @returns {Promise} 55 | */ 56 | export const exists = (path) => 57 | fs.access(path).then(() => true).catch(() => false); 58 | 59 | /** 60 | * Check if a path is a file 61 | * 62 | * @param {string} path 63 | * @returns {Promise} 64 | */ 65 | export const isFile = (path) => 66 | fs.stat(path).then((stat) => stat.isFile()).catch(() => false); 67 | 68 | /** 69 | * Check if a path is a directory 70 | * 71 | * @param {string} path 72 | * @returns {Promise} 73 | */ 74 | export const isDir = (path) => 75 | fs.stat(path).then((stat) => stat.isDirectory()).catch(() => false); 76 | -------------------------------------------------------------------------------- /src/.env.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.png" { 2 | const value: string; 3 | export default value; 4 | } 5 | 6 | declare module "*.mp3" { 7 | const value: Uint8Array; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/data.ts: -------------------------------------------------------------------------------- 1 | // Related to load and save data 2 | 3 | export function getData(key: string, def?: T): T | null { 4 | try { 5 | return JSON.parse(window.localStorage[key]); 6 | } catch { 7 | if (def) { 8 | setData(key, def); 9 | return def; 10 | } 11 | else { 12 | return null; 13 | } 14 | } 15 | } 16 | 17 | export function setData(key: string, data: any) { 18 | window.localStorage[key] = JSON.stringify(data); 19 | } 20 | -------------------------------------------------------------------------------- /src/assets/bitmapFont.ts: -------------------------------------------------------------------------------- 1 | import { ASCII_CHARS } from "../constants/general"; 2 | import { Texture } from "../gfx/gfx"; 3 | import type { Quad } from "../math/math"; 4 | import { _k } from "../shared"; 5 | import type { TexFilter } from "../types"; 6 | import { type Asset, loadImg } from "./asset"; 7 | import { makeFont } from "./font"; 8 | import { fixURL } from "./utils"; 9 | 10 | export interface GfxFont { 11 | tex: Texture; 12 | map: Record; 13 | size: number; 14 | } 15 | 16 | export type BitmapFontData = GfxFont; 17 | 18 | export function getBitmapFont(name: string): Asset | null { 19 | return _k.assets.bitmapFonts.get(name) ?? null; 20 | } 21 | 22 | export interface LoadBitmapFontOpt { 23 | chars?: string; 24 | filter?: TexFilter; 25 | outline?: number; 26 | } 27 | 28 | // TODO: support outline 29 | // TODO: support LoadSpriteSrc 30 | export function loadBitmapFont( 31 | name: string | null, 32 | src: string, 33 | gw: number, 34 | gh: number, 35 | opt: LoadBitmapFontOpt = {}, 36 | ): Asset { 37 | const fontSrc = fixURL(src); 38 | 39 | return _k.assets.bitmapFonts.add( 40 | name, 41 | loadImg(fontSrc) 42 | .then((img) => { 43 | return makeFont( 44 | Texture.fromImage(_k.gfx.ggl, img, opt), 45 | gw, 46 | gh, 47 | opt.chars ?? ASCII_CHARS, 48 | ); 49 | }), 50 | ); 51 | } 52 | 53 | // loading happiness... 54 | export function loadHappy( 55 | fontName: string = "happy", 56 | opt?: LoadBitmapFontOpt, 57 | ) { 58 | if (!_k.game.defaultAssets.happy) { 59 | throw new Error("You can't use loadHappy with kaplay/mini"); 60 | } 61 | 62 | return loadBitmapFont(fontName, _k.game.defaultAssets.happy, 28, 36, opt); 63 | } 64 | -------------------------------------------------------------------------------- /src/assets/utils.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { isDataURL } from "../utils/dataURL"; 3 | 4 | export function fixURL(url: D): D { 5 | if (typeof url !== "string" || isDataURL(url)) return url; 6 | return _k.assets.urlPrefix + url as D; 7 | } 8 | -------------------------------------------------------------------------------- /src/audio/audio.ts: -------------------------------------------------------------------------------- 1 | export type AudioCtx = ReturnType; 2 | 3 | export function createEmptyAudioBuffer(ctx: AudioContext) { 4 | return ctx.createBuffer(1, 1, 44100); 5 | } 6 | 7 | export const initAudio = () => { 8 | const audio = (() => { 9 | const ctx = new ( 10 | window.AudioContext || (window as any).webkitAudioContext 11 | )() as AudioContext; 12 | 13 | const masterNode = ctx.createGain(); 14 | masterNode.connect(ctx.destination); 15 | 16 | return { 17 | ctx, 18 | masterNode, 19 | }; 20 | })(); 21 | 22 | return audio; 23 | }; 24 | -------------------------------------------------------------------------------- /src/audio/burp.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { type AudioPlay, type AudioPlayOpt, play } from "./play"; 3 | 4 | // core KAPLAY logic 5 | export function burp(opt?: AudioPlayOpt): AudioPlay { 6 | if (!_k.game.defaultAssets.burp) { 7 | throw new Error("You can't use burp in kaplay/mini"); 8 | } 9 | 10 | return play(_k.game.defaultAssets.burp, opt); 11 | } 12 | -------------------------------------------------------------------------------- /src/audio/volume.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { deprecateMsg } from "../utils/log"; 3 | export function setVolume(v: number) { 4 | _k.audio.masterNode.gain.value = v; 5 | } 6 | 7 | export function getVolume() { 8 | return _k.audio.masterNode.gain.value; 9 | } 10 | 11 | // get / set master volume 12 | export function volume(v?: number): number { 13 | deprecateMsg("volume", "setVolume / getVolume"); 14 | 15 | if (v !== undefined) { 16 | setVolume(v); 17 | } 18 | return getVolume(); 19 | } 20 | -------------------------------------------------------------------------------- /src/constants/math.ts: -------------------------------------------------------------------------------- 1 | import { Mat4 } from "../math/Mat4"; 2 | import { Vec2 } from "../math/Vec2"; 3 | 4 | export const IDENTITY_MATRIX = new Mat4(); 5 | export const TOP_LEFT = new Vec2(-1, -1); 6 | export const TOP = new Vec2(0, -1); 7 | export const TOP_RIGHT = new Vec2(1, -1); 8 | export const LEFT = new Vec2(-1, 0); 9 | export const CENTER = new Vec2(0, 0); 10 | export const RIGHT = new Vec2(1, 0); 11 | export const BOTTOM_LEFT = new Vec2(-1, 1); 12 | export const BOTTOM = new Vec2(0, 1); 13 | export const BOTTOM_RIGHT = new Vec2(1, 1); 14 | -------------------------------------------------------------------------------- /src/core/fontCache.ts: -------------------------------------------------------------------------------- 1 | import { MAX_TEXT_CACHE_SIZE } from "../constants/general"; 2 | 3 | export const createFontCache = () => { 4 | const fontCacheCanvas = document.createElement("canvas"); 5 | fontCacheCanvas.width = MAX_TEXT_CACHE_SIZE; 6 | fontCacheCanvas.height = MAX_TEXT_CACHE_SIZE; 7 | const fontCacheC2d = fontCacheCanvas.getContext("2d", { 8 | willReadFrequently: true, 9 | }); 10 | 11 | return { 12 | fontCacheCanvas, 13 | fontCacheC2d, 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /src/core/plug.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import type { KAPLAYCtx, KAPLAYPlugin } from "../types"; 3 | 4 | export const plug = >( 5 | plugin: KAPLAYPlugin, 6 | ...args: any 7 | ): KAPLAYCtx & T => { 8 | const funcs = plugin(_k.k); 9 | let funcsObj: T; 10 | if (typeof funcs === "function") { 11 | const plugWithOptions = funcs(...args); 12 | funcsObj = plugWithOptions(_k.k); 13 | } 14 | else { 15 | funcsObj = funcs; 16 | } 17 | 18 | for (const key in funcsObj) { 19 | _k.k[key as keyof typeof _k.k] = funcsObj[key]; 20 | 21 | if (_k.globalOpt.global !== false) { 22 | window[key as any] = funcsObj[key]; 23 | } 24 | } 25 | 26 | return _k.k as unknown as KAPLAYCtx & T; 27 | }; 28 | -------------------------------------------------------------------------------- /src/core/quit.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | 3 | export const quit = () => { 4 | const { game, app, gfx, ggl, gc } = _k; 5 | game.events.onOnce("frameEnd", () => { 6 | app.quit(); 7 | 8 | // clear canvas 9 | gfx.gl.clear( 10 | gfx.gl.COLOR_BUFFER_BIT | gfx.gl.DEPTH_BUFFER_BIT 11 | | gfx.gl.STENCIL_BUFFER_BIT, 12 | ); 13 | 14 | // unbind everything 15 | const numTextureUnits = gfx.gl.getParameter( 16 | gfx.gl.MAX_TEXTURE_IMAGE_UNITS, 17 | ); 18 | 19 | for (let unit = 0; unit < numTextureUnits; unit++) { 20 | gfx.gl.activeTexture(gfx.gl.TEXTURE0 + unit); 21 | gfx.gl.bindTexture(gfx.gl.TEXTURE_2D, null); 22 | gfx.gl.bindTexture(gfx.gl.TEXTURE_CUBE_MAP, null); 23 | } 24 | 25 | gfx.gl.bindBuffer(gfx.gl.ARRAY_BUFFER, null); 26 | gfx.gl.bindBuffer(gfx.gl.ELEMENT_ARRAY_BUFFER, null); 27 | gfx.gl.bindRenderbuffer(gfx.gl.RENDERBUFFER, null); 28 | gfx.gl.bindFramebuffer(gfx.gl.FRAMEBUFFER, null); 29 | 30 | // run all scattered gc events 31 | ggl.destroy(); 32 | gc.forEach((f) => f()); 33 | 34 | // remove canvas 35 | app.canvas.remove(); 36 | }); 37 | }; 38 | 39 | export const onCleanup = (action: () => void) => { 40 | _k.gc.push(action); 41 | }; 42 | -------------------------------------------------------------------------------- /src/data/assets/bean.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/data/assets/bean.png -------------------------------------------------------------------------------- /src/data/assets/boom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/data/assets/boom.png -------------------------------------------------------------------------------- /src/data/assets/burp.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/data/assets/burp.mp3 -------------------------------------------------------------------------------- /src/data/assets/happy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/data/assets/happy.png -------------------------------------------------------------------------------- /src/data/assets/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.mp3" { 2 | const value: Uint8Array; 3 | export default value; 4 | } 5 | 6 | declare module "*.png" { 7 | const value: string; 8 | export default value; 9 | } 10 | -------------------------------------------------------------------------------- /src/data/assets/ka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/data/assets/ka.png -------------------------------------------------------------------------------- /src/ecs/components/draw/blend.ts: -------------------------------------------------------------------------------- 1 | import { BlendMode, type Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link blend `blend()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface BlendComp extends Comp { 9 | blend: BlendMode; 10 | } 11 | 12 | export function blend(blend: BlendMode): BlendComp { 13 | return { 14 | id: "color", 15 | blend: blend ?? BlendMode.Normal, 16 | inspect() { 17 | return `blend: ${ 18 | this.blend == BlendMode.Normal 19 | ? "normal" 20 | : this.blend == BlendMode.Add 21 | ? "add" 22 | : this.blend == BlendMode.Multiply 23 | ? "multiply" 24 | : "screen" 25 | }`; 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/ecs/components/draw/color.ts: -------------------------------------------------------------------------------- 1 | import { type Color, type ColorArgs, rgb } from "../../../math/color"; 2 | import type { Comp } from "../../../types"; 3 | 4 | /** 5 | * The {@link color `color()`} component. 6 | * 7 | * @group Component Types 8 | */ 9 | export interface ColorComp extends Comp { 10 | color: Color; 11 | } 12 | 13 | export function color(...args: ColorArgs): ColorComp { 14 | return { 15 | id: "color", 16 | color: rgb(...args), 17 | inspect() { 18 | return `color: ${this.color.toString()}`; 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/ecs/components/draw/drawon.ts: -------------------------------------------------------------------------------- 1 | import type { Picture } from "../../../gfx/draw/drawPicture"; 2 | import type { FrameBuffer } from "../../../gfx/FrameBuffer"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | 5 | export type DrawonOpt = { 6 | childrenOnly?: boolean; 7 | refreshOnly?: boolean; 8 | }; 9 | 10 | export interface DrawonComp extends Comp { 11 | refresh(): void; 12 | } 13 | 14 | export function drawon(c: FrameBuffer | Picture, opt?: DrawonOpt) { 15 | return { 16 | add(this: GameObj) { 17 | this.target = { 18 | destination: c, 19 | childrenOnly: opt?.childrenOnly, 20 | refreshOnly: opt?.refreshOnly, 21 | }; 22 | }, 23 | refresh(this: GameObj) { 24 | if (this.target) { 25 | this.target.isFresh = false; 26 | } 27 | }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/ecs/components/draw/fadeIn.ts: -------------------------------------------------------------------------------- 1 | import { map } from "../../../math/math"; 2 | import { _k } from "../../../shared"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { OpacityComp } from "./opacity"; 5 | 6 | export function fadeIn(time: number = 1): Comp { 7 | let finalOpacity: number; 8 | let t = 0; 9 | let done = false; 10 | 11 | return { 12 | require: ["opacity"], 13 | add(this: GameObj) { 14 | finalOpacity = this.opacity; 15 | this.opacity = 0; 16 | }, 17 | update(this: GameObj) { 18 | if (done) return; 19 | t += _k.app.dt(); 20 | this.opacity = map(t, 0, time, 0, finalOpacity); 21 | 22 | if (t >= time) { 23 | this.opacity = finalOpacity; 24 | done = true; 25 | } 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/ecs/components/draw/mask.ts: -------------------------------------------------------------------------------- 1 | import type { Comp, Mask } from "../../../types"; 2 | 3 | /** 4 | * The {@link mask `mask()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface MaskComp extends Comp { 9 | mask: Mask; 10 | } 11 | 12 | export function mask(m: Mask = "intersect"): MaskComp { 13 | return { 14 | id: "mask", 15 | mask: m, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/ecs/components/draw/opacity.ts: -------------------------------------------------------------------------------- 1 | import { type EaseFunc, easings } from "../../../math/easings"; 2 | import { _k } from "../../../shared"; 3 | import type { Comp } from "../../../types"; 4 | import { toFixed } from "../../../utils/numbers"; 5 | import type { TweenController } from "../misc/timer"; 6 | 7 | /** 8 | * The {@link opacity `opacity()`} component. 9 | * 10 | * @group Component Types 11 | */ 12 | export interface OpacityComp extends Comp { 13 | /** Opacity of the current object. */ 14 | opacity: number; 15 | /** Fade in at the start. */ 16 | fadeIn(time?: number, easeFunc?: EaseFunc): TweenController; 17 | /** Fade out at the start. */ 18 | fadeOut(time?: number, easeFunc?: EaseFunc): TweenController; 19 | } 20 | 21 | export function opacity(a: number): OpacityComp { 22 | return { 23 | id: "opacity", 24 | opacity: a ?? 1, 25 | fadeIn(time = 1, easeFunc = easings.linear): TweenController { 26 | return _k.game.root.tween( 27 | 0, 28 | this.opacity, 29 | time, 30 | (a) => this.opacity = a, 31 | easeFunc, 32 | ); 33 | }, 34 | fadeOut(time = 1, easeFunc = easings.linear): TweenController { 35 | return _k.game.root.tween( 36 | this.opacity, 37 | 0, 38 | time, 39 | (a) => this.opacity = a, 40 | easeFunc, 41 | ); 42 | }, 43 | inspect() { 44 | return `opacity: ${toFixed(this.opacity, 1)}`; 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/ecs/components/draw/outline.ts: -------------------------------------------------------------------------------- 1 | import type { LineCap, LineJoin } from "../../../gfx/draw/drawLine"; 2 | import { Color, rgb } from "../../../math/color"; 3 | import type { Comp, Outline } from "../../../types"; 4 | 5 | /** 6 | * The {@link outline `outline()`} component. 7 | * 8 | * @group Component Types 9 | */ 10 | export interface OutlineComp extends Comp { 11 | outline: Outline; 12 | } 13 | 14 | export function outline( 15 | width: number = 1, 16 | color: Color = rgb(0, 0, 0), 17 | opacity: number = 1, 18 | join: LineJoin = "miter", 19 | miterLimit: number = 10, 20 | cap: LineCap = "butt", 21 | ): OutlineComp { 22 | return { 23 | id: "outline", 24 | outline: { 25 | width, 26 | color, 27 | opacity, 28 | join, 29 | miterLimit, 30 | cap, 31 | }, 32 | inspect() { 33 | return `outline: ${this.outline.width}px, ${this.outline.color}`; 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/ecs/components/draw/picture.ts: -------------------------------------------------------------------------------- 1 | import { getRenderProps } from "../../../game/utils"; 2 | import { drawPicture, type Picture } from "../../../gfx/draw/drawPicture"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | 5 | export interface PictureComp extends Comp { 6 | picture: Picture; 7 | } 8 | 9 | export type PictureCompOpt = { 10 | picture: Picture; 11 | }; 12 | 13 | export function picture(picture: Picture): PictureComp { 14 | return { 15 | id: "picture", 16 | picture: picture, 17 | draw(this: GameObj) { 18 | drawPicture(this.picture, getRenderProps(this)); 19 | }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/ecs/components/draw/raycast.ts: -------------------------------------------------------------------------------- 1 | import type { RaycastResult } from "../../../math/math"; 2 | import type { Vec2 } from "../../../math/Vec2"; 3 | import { _k } from "../../../shared"; 4 | 5 | // this is not a component lol 6 | export function raycast( 7 | origin: Vec2, 8 | direction: Vec2, 9 | exclude?: string[], 10 | ) { 11 | let minHit: RaycastResult; 12 | 13 | const shapes = _k.game.root.get("area"); 14 | 15 | shapes.forEach(s => { 16 | if (exclude && exclude.some(tag => s.is(tag))) return; 17 | const shape = s.worldArea(); 18 | const hit = shape.raycast(origin, direction); 19 | if (hit) { 20 | if (minHit) { 21 | if (hit.fraction < minHit.fraction) { 22 | minHit = hit; 23 | minHit!.object = s; 24 | } 25 | } 26 | else { 27 | minHit = hit; 28 | minHit!.object = s; 29 | } 30 | } 31 | }); 32 | 33 | return minHit!; 34 | } 35 | -------------------------------------------------------------------------------- /src/ecs/components/draw/shader.ts: -------------------------------------------------------------------------------- 1 | import type { Uniform } from "../../../assets/shader"; 2 | import type { Comp } from "../../../types"; 3 | 4 | /** 5 | * The {@link shader `shader()`} component. 6 | * 7 | * @group Component Types 8 | */ 9 | export interface ShaderComp extends Comp { 10 | /** 11 | * Uniform values to pass to the shader. 12 | */ 13 | uniform?: Uniform; 14 | /** 15 | * The shader ID. 16 | */ 17 | shader: string; 18 | } 19 | 20 | export function shader( 21 | id: string, 22 | uniform?: Uniform | (() => Uniform), 23 | ): ShaderComp { 24 | return { 25 | id: "shader", 26 | shader: id, 27 | ...(typeof uniform === "function" 28 | ? { 29 | uniform: uniform(), 30 | update() { 31 | this.uniform = uniform(); 32 | }, 33 | } 34 | : { 35 | uniform: uniform, 36 | }), 37 | inspect() { 38 | return `shader: ${id}`; 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/ecs/components/draw/uvquad.ts: -------------------------------------------------------------------------------- 1 | import { getRenderProps } from "../../../game/utils"; 2 | import { drawUVQuad } from "../../../gfx/draw/drawUVQuad"; 3 | import { Rect, vec2 } from "../../../math/math"; 4 | import type { Comp, GameObj } from "../../../types"; 5 | 6 | /** 7 | * The {@link uvquad `uvquad()`} component. 8 | * 9 | * @group Component Types 10 | */ 11 | export interface UVQuadComp extends Comp { 12 | draw: Comp["draw"]; 13 | /** 14 | * Width of rect. 15 | */ 16 | width: number; 17 | /** 18 | * Height of height. 19 | */ 20 | height: number; 21 | /** 22 | * @since v3000.0 23 | */ 24 | renderArea(): Rect; 25 | } 26 | 27 | export function uvquad(w: number, h: number): UVQuadComp { 28 | let _shape: Rect | undefined; 29 | let _width = w; 30 | let _height = h; 31 | return { 32 | id: "uvquad", 33 | get width() { 34 | return _width; 35 | }, 36 | set width(value) { 37 | _width = value; 38 | if (_shape) _shape.width = value; 39 | }, 40 | get height() { 41 | return _height; 42 | }, 43 | set height(value) { 44 | _height = value; 45 | if (_shape) _shape.height = value; 46 | }, 47 | draw(this: GameObj) { 48 | drawUVQuad(Object.assign(getRenderProps(this), { 49 | width: _width, 50 | height: _height, 51 | })); 52 | }, 53 | renderArea() { 54 | if (!_shape) { 55 | _shape = new Rect(vec2(0), _width, _height); 56 | } 57 | return _shape; 58 | }, 59 | inspect() { 60 | return `uvquad: (${Math.ceil(_width)}w, ${Math.ceil(_height)})h`; 61 | }, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/ecs/components/misc/boom.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import { _k } from "../../../shared"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { ScaleComp } from "../transform/scale"; 5 | 6 | export function boom(speed: number = 2, size: number = 1): Comp { 7 | let time = 0; 8 | return { 9 | require: ["scale"], 10 | update(this: GameObj) { 11 | const s = Math.sin(time * speed) * size; 12 | if (s < 0) { 13 | this.destroy(); 14 | } 15 | this.scale = vec2(s); 16 | time += _k.app.dt(); 17 | }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/ecs/components/misc/lifespan.ts: -------------------------------------------------------------------------------- 1 | import { easings } from "../../../math/easings"; 2 | import { _k } from "../../../shared"; 3 | import type { EmptyComp, GameObj } from "../../../types"; 4 | import type { OpacityComp } from "../draw/opacity"; 5 | 6 | /** 7 | * The {@link lifespan `lifespan()`} component. 8 | * 9 | * @group Component Types 10 | */ 11 | export interface LifespanCompOpt { 12 | /** 13 | * Fade out duration (default 0 which is no fade out). 14 | */ 15 | fade?: number; 16 | } 17 | 18 | export function lifespan(time: number, opt: LifespanCompOpt = {}): EmptyComp { 19 | if (time == null) { 20 | throw new Error("lifespan() requires time"); 21 | } 22 | const fade = opt.fade ?? 0; 23 | return { 24 | id: "lifespan", 25 | require: ["opacity"], 26 | add(this: GameObj) { 27 | _k.game.root.wait(time, () => { 28 | this.opacity = this.opacity ?? 1; 29 | 30 | if (fade > 0) { 31 | _k.game.root.tween( 32 | this.opacity, 33 | 0, 34 | fade, 35 | (a) => this.opacity = a, 36 | easings.linear, 37 | ).onEnd(() => { 38 | this.destroy(); 39 | }); 40 | } 41 | else { 42 | this.destroy(); 43 | } 44 | }); 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/ecs/components/misc/named.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link named `named()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface NamedComp extends Comp { 9 | /** The name assigned to this object. */ 10 | name: string; 11 | } 12 | 13 | export function named(name: string): NamedComp { 14 | return { 15 | id: "named", 16 | name, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/ecs/components/misc/stay.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link stay `stay()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface StayComp extends Comp { 9 | /** 10 | * If the obj should not be destroyed on scene switch. 11 | */ 12 | stay: boolean; 13 | /** 14 | * Array of scenes that the obj will stay on. 15 | */ 16 | scenesToStay?: string[]; 17 | } 18 | 19 | export function stay(scenesToStay?: string[]): StayComp { 20 | return { 21 | id: "stay", 22 | stay: true, 23 | scenesToStay, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/ecs/components/physics/doubleJump.ts: -------------------------------------------------------------------------------- 1 | import type { KEventController } from "../../../events/events"; 2 | import type { Comp, GameObj } from "../../../types"; 3 | import type { BodyComp } from "./body"; 4 | 5 | /** 6 | * The {@link doubleJump `doubleJump()`} component. 7 | * 8 | * @group Component Types 9 | */ 10 | export interface DoubleJumpComp extends Comp { 11 | /** 12 | * Number of jumps allowed. 13 | */ 14 | numJumps: number; 15 | /** 16 | * Performs double jump (the initial jump only happens if player is grounded). 17 | */ 18 | doubleJump(force?: number): void; 19 | /** 20 | * Register an event that runs when the object performs the second jump when double jumping. 21 | */ 22 | onDoubleJump(action: () => void): KEventController; 23 | } 24 | 25 | export function doubleJump(numJumps: number = 2): DoubleJumpComp { 26 | let jumpsLeft = numJumps; 27 | return { 28 | id: "doubleJump", 29 | require: ["body"], 30 | numJumps: numJumps, 31 | add(this: GameObj) { 32 | this.onGround(() => { 33 | jumpsLeft = this.numJumps; 34 | }); 35 | }, 36 | doubleJump( 37 | this: GameObj, 38 | force?: number, 39 | ) { 40 | if (jumpsLeft <= 0) { 41 | return; 42 | } 43 | if (jumpsLeft < this.numJumps) { 44 | this.trigger("doubleJump"); 45 | } 46 | jumpsLeft--; 47 | this.jump(force); 48 | }, 49 | onDoubleJump(this: GameObj, action: () => void): KEventController { 50 | return this.on("doubleJump", action); 51 | }, 52 | inspect(this: GameObj) { 53 | return `jumpsLeft: ${jumpsLeft}`; 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/ecs/components/transform/anchor.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../../math/Vec2"; 2 | import type { Anchor, Comp } from "../../../types"; 3 | 4 | /** 5 | * The {@link anchor `anchor()`} component. 6 | * 7 | * @group Component Types 8 | */ 9 | export interface AnchorComp extends Comp { 10 | /** 11 | * Anchor point for render. 12 | */ 13 | anchor: Anchor | Vec2; 14 | } 15 | 16 | export function anchor(o: Anchor | Vec2): AnchorComp { 17 | if (!o) { 18 | throw new Error("Please define an anchor"); 19 | } 20 | return { 21 | id: "anchor", 22 | anchor: o, 23 | inspect() { 24 | if (typeof this.anchor === "string") { 25 | return `anchor: ` + this.anchor; 26 | } 27 | else { 28 | return `anchor: ` + this.anchor.toString(); 29 | } 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/ecs/components/transform/fixed.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link fixed `fixed()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface FixedComp extends Comp { 9 | /** 10 | * If the obj is unaffected by camera 11 | */ 12 | fixed: boolean; 13 | } 14 | 15 | export function fixed(): FixedComp { 16 | return { 17 | id: "fixed", 18 | fixed: true, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/ecs/components/transform/follow.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import { Vec2 } from "../../../math/Vec2"; 3 | import type { Comp, GameObj } from "../../../types"; 4 | import type { PosComp } from "./pos"; 5 | 6 | /** 7 | * The {@link follow `follow()`} component. 8 | * 9 | * @group Component Types 10 | */ 11 | export interface FollowComp extends Comp { 12 | follow: { 13 | /** 14 | * The object to follow. 15 | */ 16 | obj: GameObj; 17 | /** 18 | * The offset to follow the object by. 19 | */ 20 | offset: Vec2; 21 | }; 22 | } 23 | 24 | export function follow(obj: GameObj, offset?: Vec2): FollowComp { 25 | return { 26 | id: "follow", 27 | require: ["pos"], 28 | follow: { 29 | obj: obj, 30 | offset: offset ?? vec2(0), 31 | }, 32 | add(this: GameObj) { 33 | if (obj.exists()) { 34 | this.pos = this.follow.obj.pos.add(this.follow.offset); 35 | } 36 | }, 37 | update(this: GameObj) { 38 | if (obj.exists()) { 39 | this.pos = this.follow.obj.pos.add(this.follow.offset); 40 | } 41 | }, 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/ecs/components/transform/layer.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../../shared"; 2 | import type { Comp } from "../../../types"; 3 | 4 | /** 5 | * The {@link layer `layer()`} component. 6 | * 7 | * @group Component Types 8 | */ 9 | export interface LayerComp extends Comp { 10 | /** 11 | * Get the index of the current layer the object is assigned to. 12 | * 13 | * @returns The index of the layer the object is assigned to, or `null` if the layer does not exist. 14 | */ 15 | get layerIndex(): number | null; 16 | /** 17 | * Get the name of the current layer the object is assigned to. 18 | * 19 | * @returns The name of the layer the object is assigned to. 20 | */ 21 | get layer(): string | null; 22 | /** 23 | * Set the name of the layer the object should be assigned to. 24 | */ 25 | set layer(name: string); 26 | } 27 | 28 | export function layer(layer: string): LayerComp { 29 | let _layerIndex = _k.game.layers?.indexOf(layer); 30 | 31 | return { 32 | id: "layer", 33 | get layerIndex() { 34 | return _layerIndex ?? null; 35 | }, 36 | get layer(): string | null { 37 | if (!_layerIndex) return null; 38 | 39 | return _k.game.layers?.[_layerIndex] ?? null; 40 | }, 41 | set layer(value: string) { 42 | _layerIndex = _k.game.layers?.indexOf(value); 43 | 44 | if (_layerIndex == -1) throw Error("Invalid layer name"); 45 | }, 46 | inspect() { 47 | return `layer: ${this.layer}`; 48 | }, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/ecs/components/transform/move.ts: -------------------------------------------------------------------------------- 1 | import { Vec2 } from "../../../math/Vec2"; 2 | import type { EmptyComp, GameObj } from "../../../types"; 3 | import type { PosComp } from "./pos"; 4 | 5 | export function move(dir: number | Vec2, speed: number): EmptyComp { 6 | const d = typeof dir === "number" ? Vec2.fromAngle(dir) : dir.unit(); 7 | return { 8 | id: "move", 9 | require: ["pos"], 10 | update(this: GameObj) { 11 | this.move(d.scale(speed)); 12 | }, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/ecs/components/transform/rotate.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link rotate `rotate()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface RotateComp extends Comp { 9 | /** 10 | * Angle in degrees. 11 | */ 12 | angle: number; 13 | /** 14 | * Rotate in degrees. 15 | */ 16 | rotateBy(angle: number): void; 17 | /** 18 | * Rotate to a degree (like directly assign to .angle) 19 | * 20 | * @since v3000.0 21 | */ 22 | rotateTo(s: number): void; 23 | } 24 | 25 | export function rotate(a?: number): RotateComp { 26 | return { 27 | id: "rotate", 28 | angle: a ?? 0, 29 | rotateBy(angle: number) { 30 | this.angle += angle; 31 | }, 32 | rotateTo(angle: number) { 33 | this.angle = angle; 34 | }, 35 | inspect() { 36 | return `angle: ${Math.round(this.angle)}`; 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/ecs/components/transform/z.ts: -------------------------------------------------------------------------------- 1 | import type { Comp } from "../../../types"; 2 | 3 | /** 4 | * The {@link z `z()`} component. 5 | * 6 | * @group Component Types 7 | */ 8 | export interface ZComp extends Comp { 9 | /** 10 | * Defines the z-index of this game obj 11 | */ 12 | z: number; 13 | } 14 | 15 | export function z(z: number): ZComp { 16 | return { 17 | id: "z", 18 | z: z, 19 | inspect() { 20 | return `z: ${this.z}`; 21 | }, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/ecs/entity/premade/addKaboom.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../../math/Vec2"; 2 | import { _k } from "../../../shared"; 3 | import type { CompList, GameObj } from "../../../types"; 4 | import { sprite } from "../../components/draw/sprite"; 5 | import { boom } from "../../components/misc/boom"; 6 | import { stay } from "../../components/misc/stay"; 7 | import { timer } from "../../components/misc/timer"; 8 | import { anchor } from "../../components/transform/anchor"; 9 | import { pos } from "../../components/transform/pos"; 10 | import { scale } from "../../components/transform/scale"; 11 | 12 | /** 13 | * @group Options 14 | */ 15 | export interface BoomOpt { 16 | /** 17 | * Animation speed. 18 | */ 19 | speed?: number; 20 | /** 21 | * Scale. 22 | */ 23 | scale?: number; 24 | /** 25 | * Additional components. 26 | * 27 | * @since v3000.0 28 | */ 29 | comps?: CompList; 30 | } 31 | 32 | export function addKaboom(p: Vec2, opt: BoomOpt = {}): GameObj { 33 | if (!_k.game.defaultAssets.boom || !_k.game.defaultAssets.ka) { 34 | throw new Error("You can't use addKaboom with kaplay/mini"); 35 | } 36 | 37 | const kaboom = _k.game.root.add([ 38 | pos(p), 39 | stay(), 40 | ]); 41 | 42 | const speed = (opt.speed || 1) * 5; 43 | const s = opt.scale || 1; 44 | 45 | kaboom.add([ 46 | sprite(_k.game.defaultAssets.boom), 47 | scale(0), 48 | anchor("center"), 49 | boom(speed, s), 50 | ...opt.comps ?? [], 51 | ]); 52 | 53 | const ka = kaboom.add([ 54 | sprite(_k.game.defaultAssets.ka), 55 | scale(0), 56 | anchor("center"), 57 | timer(), 58 | ...opt.comps ?? [], 59 | ]); 60 | 61 | ka.wait(0.4 / speed, () => ka.use(boom(speed, s))); 62 | ka.onDestroy(() => kaboom.destroy()); 63 | 64 | return kaboom; 65 | } 66 | -------------------------------------------------------------------------------- /src/ecs/entity/premade/addLevel.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../../math/math"; 2 | import type { Vec2 } from "../../../math/Vec2"; 3 | import { _k } from "../../../shared"; 4 | import type { GameObj } from "../../../types"; 5 | import { 6 | level, 7 | type LevelComp, 8 | type LevelOpt, 9 | } from "../../components/level/level"; 10 | import { pos, type PosComp } from "../../components/transform/pos"; 11 | 12 | /** 13 | * Options for the {@link addLevel `addLevel()`}. 14 | * 15 | * @group Options 16 | */ 17 | export interface AddLevelOpt extends LevelOpt { 18 | /** 19 | * Position of the first block. 20 | */ 21 | pos?: Vec2; 22 | } 23 | 24 | export function addLevel( 25 | map: string[], 26 | opt: AddLevelOpt, 27 | parent: GameObj = _k.game.root, 28 | ): GameObj { 29 | return parent.add([pos(opt.pos ?? vec2(0)), level(map, opt)]); 30 | } 31 | -------------------------------------------------------------------------------- /src/ecs/entity/utils.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import type { GameObj } from "../../types"; 3 | 4 | export function destroy(obj: GameObj) { 5 | obj.destroy(); 6 | } 7 | 8 | export function getTreeRoot(): GameObj { 9 | return _k.game.root; 10 | } 11 | 12 | export function isFixed(obj: GameObj): boolean { 13 | if (obj.fixed) return true; 14 | return obj.parent ? isFixed(obj.parent) : false; 15 | } 16 | -------------------------------------------------------------------------------- /src/ecs/systems/systems.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | 3 | export type System = { 4 | name: string; 5 | run: () => void; 6 | when: LCEvents[]; 7 | }; 8 | 9 | // Lifecycle events 10 | export enum LCEvents { 11 | "BeforeUpdate", 12 | "BeforeFixedUpdate", 13 | "BeforeDraw", 14 | "AfterUpdate", 15 | "AfterFixedUpdate", 16 | "AfterDraw", 17 | } 18 | 19 | export const system = (name: string, action: () => void, when: LCEvents[]) => { 20 | const systems = _k.game.systems; 21 | const replacingSystemIdx = systems.findIndex((s) => s.name === name); 22 | 23 | // if existent system, remove it 24 | if (replacingSystemIdx != -1) { 25 | const replacingSystem = systems[replacingSystemIdx]; 26 | const when = replacingSystem.when; 27 | 28 | for (const loc of when) { 29 | const idx = _k.game.systemsByEvent[loc].findIndex( 30 | (s) => s.name === name, 31 | ); 32 | _k.game.systemsByEvent[loc].splice(idx, 1); 33 | } 34 | } 35 | 36 | const system: System = { 37 | name, 38 | run: action, 39 | when, 40 | }; 41 | 42 | for (const loc of when) { 43 | _k.game.systemsByEvent[loc].push(system); 44 | } 45 | 46 | systems.push({ name, run: action, when }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/game/gameLoop.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaplayjs/kaplay/c2b04ec3f0b99e107b7509e73a200d0f81cbd915/src/game/gameLoop.ts -------------------------------------------------------------------------------- /src/game/gravity.ts: -------------------------------------------------------------------------------- 1 | // Gravity manipulation 2 | 3 | import { vec2 } from "../math/math"; 4 | import { type Vec2 } from "../math/Vec2"; 5 | import { _k } from "../shared"; 6 | 7 | export function setGravity(g: number) { 8 | // If g > 0 use either the current direction or use (0, 1) 9 | // Else null 10 | _k.game.gravity = g 11 | ? (_k.game.gravity || vec2(0, 1)).unit().scale(g) 12 | : null; 13 | } 14 | 15 | export function getGravity() { 16 | // If gravity > 0 return magnitude 17 | // Else 0 18 | return _k.game.gravity ? _k.game.gravity.len() : 0; 19 | } 20 | 21 | export function setGravityDirection(d: Vec2) { 22 | // If gravity > 0 keep magnitude, otherwise use 1 23 | _k.game.gravity = d.unit().scale( 24 | _k.game.gravity ? _k.game.gravity.len() : 1, 25 | ); 26 | } 27 | 28 | export function getGravityDirection() { 29 | // If gravity != null return unit vector, otherwise return (0, 1) 30 | return _k.game.gravity ? _k.game.gravity.unit() : vec2(0, 1); 31 | } 32 | -------------------------------------------------------------------------------- /src/game/layers.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import { deprecateMsg } from "../utils/log"; 3 | 4 | // Layering 5 | 6 | export function setLayers(layerNames: string[], defaultLayer: string) { 7 | if (_k.game.layers) { 8 | throw Error("Layers can only be assigned once."); 9 | } 10 | const defaultLayerIndex = layerNames.indexOf(defaultLayer); 11 | if (defaultLayerIndex == -1) { 12 | throw Error( 13 | "The default layer name should be present in the layers list.", 14 | ); 15 | } 16 | _k.game.layers = layerNames; 17 | _k.game.defaultLayerIndex = defaultLayerIndex; 18 | } 19 | 20 | export function getLayers() { 21 | return _k.game.layers; 22 | } 23 | 24 | export function getDefaultLayer() { 25 | return _k.game.layers?.[_k.game.defaultLayerIndex] ?? null; 26 | } 27 | 28 | export function layers(layerNames: string[], defaultLayer: string) { 29 | deprecateMsg("layers", "setLayers"); 30 | setLayers(layerNames, defaultLayer); 31 | } 32 | -------------------------------------------------------------------------------- /src/game/scenes.ts: -------------------------------------------------------------------------------- 1 | import { initAppEvents } from "../app/appEvents"; 2 | import type { KEventController } from "../events/events"; 3 | import { Mat23, vec2 } from "../math/math"; 4 | import { _k } from "../shared"; 5 | 6 | /** 7 | * The name of a scene. 8 | */ 9 | export type SceneName = string; 10 | export type SceneDef = (...args: any) => void; 11 | 12 | export function scene(id: SceneName, def: SceneDef) { 13 | _k.game.scenes[id] = def; 14 | } 15 | 16 | export function go(name: SceneName, ...args: unknown[]) { 17 | if (!_k.game.scenes[name]) { 18 | throw new Error(`Scene not found: ${name}`); 19 | } 20 | 21 | _k.game.events.onOnce("frameEnd", () => { 22 | _k.game.events.trigger("sceneLeave", name); 23 | _k.app.events.clear(); 24 | _k.game.events.clear(); 25 | 26 | [..._k.game.root.children].forEach((obj) => { 27 | if ( 28 | !obj.stay 29 | || (obj.scenesToStay && !obj.scenesToStay.includes(name)) 30 | ) { 31 | _k.game.root.remove(obj); 32 | } 33 | else { 34 | obj.trigger("sceneEnter", name); 35 | } 36 | }); 37 | 38 | _k.game.root.clearEvents(); 39 | initAppEvents(); 40 | 41 | // cam 42 | _k.game.cam = { 43 | pos: null, 44 | scale: vec2(1), 45 | angle: 0, 46 | shake: 0, 47 | transform: new Mat23(), 48 | }; 49 | 50 | _k.game.scenes[name](...args); 51 | }); 52 | 53 | _k.game.currentScene = name; 54 | } 55 | 56 | export function onSceneLeave( 57 | action: (newScene?: string) => void, 58 | ): KEventController { 59 | return _k.game.events.on("sceneLeave", action); 60 | } 61 | 62 | export function getSceneName() { 63 | return _k.game.currentScene; 64 | } 65 | -------------------------------------------------------------------------------- /src/game/utils.ts: -------------------------------------------------------------------------------- 1 | import type { GameObj } from "../types"; 2 | 3 | // Note: I will doom this soon 😈😈😈😈 4 | export function getRenderProps(obj: GameObj) { 5 | return { 6 | color: obj.color, 7 | opacity: obj.opacity, 8 | anchor: obj.anchor, 9 | outline: obj.outline, 10 | shader: obj.shader, 11 | uniform: obj.uniform, 12 | blend: obj.blend, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/gfx/anchor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BOTTOM, 3 | BOTTOM_LEFT, 4 | BOTTOM_RIGHT, 5 | CENTER, 6 | LEFT, 7 | RIGHT, 8 | TOP, 9 | TOP_LEFT, 10 | TOP_RIGHT, 11 | } from "../constants/math"; 12 | import { Vec2 } from "../math/Vec2"; 13 | import { type Anchor } from "../types"; 14 | import type { TextAlign } from "./draw/drawText"; 15 | 16 | // convert anchor string to a vec2 offset 17 | export function anchorPt(orig: Anchor | Vec2): Vec2 { 18 | switch (orig) { 19 | case "topleft": 20 | return TOP_LEFT; 21 | case "top": 22 | return TOP; 23 | case "topright": 24 | return TOP_RIGHT; 25 | case "left": 26 | return LEFT; 27 | case "center": 28 | return CENTER; 29 | case "right": 30 | return RIGHT; 31 | case "botleft": 32 | return BOTTOM_LEFT; 33 | case "bot": 34 | return BOTTOM; 35 | case "botright": 36 | return BOTTOM_RIGHT; 37 | default: 38 | return orig; 39 | } 40 | } 41 | 42 | export function alignPt(align: TextAlign): number { 43 | switch (align) { 44 | case "left": 45 | return 0; 46 | case "center": 47 | return 0.5; 48 | case "right": 49 | return 1; 50 | default: 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/gfx/bg.ts: -------------------------------------------------------------------------------- 1 | import { type ColorArgs, rgb } from "../math/color"; 2 | import { _k } from "../shared"; 3 | 4 | export function setBackground(...args: ColorArgs) { 5 | const color = rgb(...args); 6 | const alpha = args[3] ?? 1; 7 | 8 | _k.gfx.bgColor = color; 9 | _k.gfx.bgAlpha = alpha; 10 | 11 | _k.gfx.ggl.gl.clearColor( 12 | color.r / 255, 13 | color.g / 255, 14 | color.b / 255, 15 | alpha, 16 | ); 17 | } 18 | 19 | export function getBackground() { 20 | return _k.gfx.bgColor?.clone?.() ?? null; 21 | } 22 | -------------------------------------------------------------------------------- /src/gfx/canvasBuffer.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | import type { Canvas } from "../types"; 3 | import { FrameBuffer } from "./FrameBuffer"; 4 | import { flush } from "./stack"; 5 | 6 | export const makeCanvas = (w: number, h: number): Canvas => { 7 | const fb = new FrameBuffer(_k.ggl, w, h); 8 | 9 | return { 10 | clear: () => fb.clear(), 11 | free: () => fb.free(), 12 | toDataURL: () => fb.toDataURL(), 13 | toImageData: () => fb.toImageData(), 14 | width: fb.width, 15 | height: fb.height, 16 | draw: (action: () => void) => { 17 | flush(); 18 | fb.bind(); 19 | action(); 20 | flush(); 21 | fb.unbind(); 22 | }, 23 | get fb() { 24 | return fb; 25 | }, 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /src/gfx/draw/drawBezier.ts: -------------------------------------------------------------------------------- 1 | import { evaluateBezier } from "../../math/math"; 2 | import { type Vec2 } from "../../math/Vec2"; 3 | import { drawCurve, type DrawCurveOpt } from "./drawCurve"; 4 | 5 | export type DrawBezierOpt = DrawCurveOpt & { 6 | /** 7 | * The first point. 8 | */ 9 | pt1: Vec2; 10 | /** 11 | * The the first control point. 12 | */ 13 | pt2: Vec2; 14 | /** 15 | * The the second control point. 16 | */ 17 | pt3: Vec2; 18 | /** 19 | * The second point. 20 | */ 21 | pt4: Vec2; 22 | }; 23 | 24 | export function drawBezier(opt: DrawBezierOpt) { 25 | drawCurve( 26 | t => evaluateBezier(opt.pt1, opt.pt2, opt.pt3, opt.pt4, t), 27 | opt, 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCanvas.ts: -------------------------------------------------------------------------------- 1 | import { vec2 } from "../../math/math"; 2 | import type { Canvas, DrawUVQuadOpt, RenderProps } from "../../types"; 3 | import { height } from "../stack"; 4 | import { drawUVQuad } from "./drawUVQuad"; 5 | 6 | export type DrawCanvasOpt = DrawUVQuadOpt & { 7 | canvas: Canvas; 8 | }; 9 | 10 | export function drawCanvas(opt: DrawCanvasOpt) { 11 | const fb = opt.canvas.fb; 12 | drawUVQuad(Object.assign({}, opt, { 13 | tex: fb.tex, 14 | width: opt.width || fb.width, 15 | height: opt.height || fb.height, 16 | pos: (opt.pos || vec2()).add(0, height()), 17 | scale: (opt.scale || vec2(1)).scale(1, -1), 18 | })); 19 | } 20 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCircle.ts: -------------------------------------------------------------------------------- 1 | import type { Color } from "../../math/color"; 2 | import type { Vec2 } from "../../math/Vec2"; 3 | import type { Anchor, RenderProps } from "../../types"; 4 | import { drawEllipse } from "./drawEllipse"; 5 | 6 | /** 7 | * How the circle should look like. 8 | */ 9 | export type DrawCircleOpt = Omit & { 10 | /** 11 | * Radius of the circle. 12 | */ 13 | radius: number; 14 | /** 15 | * Starting angle. 16 | */ 17 | start?: number; 18 | /** 19 | * Ending angle. 20 | */ 21 | end?: number; 22 | /** 23 | * If fill the shape with color (set this to false if you only want an outline). 24 | */ 25 | fill?: boolean; 26 | /** 27 | * Use gradient instead of solid color. 28 | * 29 | * @since v3000.0 30 | */ 31 | gradient?: [Color, Color]; 32 | /** 33 | * Multiplier for circle vertices resolution (default 1) 34 | */ 35 | resolution?: number; 36 | /** 37 | * The anchor point, or the pivot point. Default to "topleft". 38 | */ 39 | anchor?: Anchor | Vec2; 40 | }; 41 | 42 | export function drawCircle(opt: DrawCircleOpt) { 43 | if (typeof opt.radius !== "number") { 44 | throw new Error("drawCircle() requires property \"radius\"."); 45 | } 46 | 47 | if (opt.radius === 0) { 48 | return; 49 | } 50 | 51 | drawEllipse(Object.assign({}, opt, { 52 | radiusX: opt.radius, 53 | radiusY: opt.radius, 54 | angle: 0, 55 | })); 56 | } 57 | -------------------------------------------------------------------------------- /src/gfx/draw/drawCurve.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../math/Vec2"; 2 | import type { RenderProps } from "../../types"; 3 | import { drawLines } from "./drawLine"; 4 | 5 | export type DrawCurveOpt = RenderProps & { 6 | /** 7 | * The amount of line segments to draw. 8 | */ 9 | segments?: number; 10 | /** 11 | * The width of the line. 12 | */ 13 | width?: number; 14 | }; 15 | 16 | export function drawCurve(curve: (t: number) => Vec2, opt: DrawCurveOpt) { 17 | const segments = opt.segments ?? 16; 18 | const p: Vec2[] = []; 19 | 20 | for (let i = 0; i <= segments; i++) { 21 | p.push(curve(i / segments)); 22 | } 23 | 24 | drawLines({ 25 | pts: p, 26 | width: opt.width || 1, 27 | pos: opt.pos, 28 | color: opt.color, 29 | opacity: opt.opacity, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /src/gfx/draw/drawEllipse.ts: -------------------------------------------------------------------------------- 1 | import { getArcPts } from "../../math/various"; 2 | import { Vec2 } from "../../math/Vec2"; 3 | import type { DrawEllipseOpt } from "../../types"; 4 | import { anchorPt } from "../anchor"; 5 | import { drawPolygon } from "./drawPolygon"; 6 | 7 | export function drawEllipse(opt: DrawEllipseOpt) { 8 | if (opt.radiusX === undefined || opt.radiusY === undefined) { 9 | throw new Error( 10 | "drawEllipse() requires properties \"radiusX\" and \"radiusY\".", 11 | ); 12 | } 13 | 14 | if (opt.radiusX === 0 || opt.radiusY === 0) { 15 | return; 16 | } 17 | 18 | const start = opt.start ?? 0; 19 | const end = opt.end ?? 360; 20 | const offset = anchorPt(opt.anchor ?? "center").scale( 21 | new Vec2(-opt.radiusX, -opt.radiusY), 22 | ); 23 | 24 | const pts = getArcPts( 25 | offset, 26 | opt.radiusX, 27 | opt.radiusY, 28 | start, 29 | end, 30 | opt.resolution, 31 | ); 32 | 33 | // center 34 | pts.unshift(offset); 35 | 36 | const polyOpt = Object.assign({}, opt, { 37 | pts, 38 | radius: 0, 39 | ...(opt.gradient 40 | ? { 41 | colors: [ 42 | opt.gradient[0], 43 | ...Array(pts.length - 1).fill(opt.gradient[1]), 44 | ], 45 | } 46 | : {}), 47 | }); 48 | 49 | // full circle with outline shouldn't have the center point 50 | if (end - start >= 360 && opt.outline) { 51 | if (opt.fill !== false) { 52 | drawPolygon(Object.assign({}, polyOpt, { 53 | outline: null, 54 | })); 55 | } 56 | drawPolygon(Object.assign({}, polyOpt, { 57 | pts: pts.slice(1), 58 | fill: false, 59 | })); 60 | return; 61 | } 62 | 63 | drawPolygon(polyOpt); 64 | } 65 | -------------------------------------------------------------------------------- /src/gfx/draw/drawFrame.ts: -------------------------------------------------------------------------------- 1 | import { lerp } from "../../math/lerp"; 2 | import { rand } from "../../math/math"; 3 | import { Vec2 } from "../../math/Vec2"; 4 | import { _k } from "../../shared"; 5 | import { center, flush } from "../stack"; 6 | 7 | export function drawFrame() { 8 | // calculate camera matrix 9 | const cam = _k.game.cam; 10 | const shake = Vec2.fromAngle(rand(0, 360)).scale(cam.shake); 11 | 12 | cam.shake = lerp(cam.shake, 0, 5 * _k.app.dt()); 13 | cam.transform.setIdentity() 14 | .translateSelfV(center()) 15 | .scaleSelfV(cam.scale) 16 | .rotateSelf(cam.angle) 17 | .translateSelfV((cam.pos ?? center()).scale(-1).add(shake)); 18 | 19 | _k.game.root.draw(); 20 | flush(); 21 | } 22 | -------------------------------------------------------------------------------- /src/gfx/draw/drawInspectText.ts: -------------------------------------------------------------------------------- 1 | import { DBG_FONT } from "../../constants/general"; 2 | import { rgb } from "../../math/color"; 3 | import { vec2 } from "../../math/math"; 4 | import { type Vec2 } from "../../math/Vec2"; 5 | import { formatText } from "../formatText"; 6 | import { 7 | height, 8 | multTranslateV, 9 | popTransform, 10 | pushTransform, 11 | width, 12 | } from "../stack"; 13 | import { drawFormattedText } from "./drawFormattedText"; 14 | import { drawRect } from "./drawRect"; 15 | import { drawUnscaled } from "./drawUnscaled"; 16 | 17 | export function drawInspectText(pos: Vec2, txt: string) { 18 | drawUnscaled(() => { 19 | const pad = vec2(8); 20 | 21 | pushTransform(); 22 | multTranslateV(pos); 23 | 24 | const ftxt = formatText({ 25 | text: txt, 26 | font: DBG_FONT, 27 | size: 16, 28 | pos: pad, 29 | color: rgb(255, 255, 255), 30 | fixed: true, 31 | }); 32 | 33 | const bw = ftxt.width + pad.x * 2; 34 | const bh = ftxt.height + pad.x * 2; 35 | 36 | if (pos.x + bw >= width()) { 37 | multTranslateV(vec2(-bw, 0)); 38 | } 39 | 40 | if (pos.y + bh >= height()) { 41 | multTranslateV(vec2(0, -bh)); 42 | } 43 | 44 | drawRect({ 45 | width: bw, 46 | height: bh, 47 | color: rgb(0, 0, 0), 48 | radius: 4, 49 | opacity: 0.8, 50 | fixed: true, 51 | }); 52 | 53 | drawFormattedText(ftxt); 54 | popTransform(); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/gfx/draw/drawLoadingScreen.ts: -------------------------------------------------------------------------------- 1 | import { loadProgress } from "../../assets/asset"; 2 | import { rgb } from "../../math/color"; 3 | import { vec2 } from "../../math/math"; 4 | import { _k } from "../../shared"; 5 | import { height, width } from "../stack"; 6 | import { drawRect } from "./drawRect"; 7 | import { drawUnscaled } from "./drawUnscaled"; 8 | 9 | export function drawLoadScreen() { 10 | const progress = loadProgress(); 11 | 12 | if (_k.game.events.numListeners("loading") > 0) { 13 | _k.game.events.trigger("loading", progress); 14 | } 15 | else { 16 | drawUnscaled(() => { 17 | const w = width() / 2; 18 | const h = 24; 19 | const pos = vec2(width() / 2, height() / 2).sub( 20 | vec2(w / 2, h / 2), 21 | ); 22 | drawRect({ 23 | pos: vec2(0), 24 | width: width(), 25 | height: height(), 26 | color: rgb(0, 0, 0), 27 | }); 28 | drawRect({ 29 | pos: pos, 30 | width: w, 31 | height: h, 32 | fill: false, 33 | outline: { 34 | width: 4, 35 | }, 36 | }); 37 | drawRect({ 38 | pos: pos, 39 | width: w * progress, 40 | height: h, 41 | }); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/gfx/draw/drawMasked.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { drawStenciled } from "./drawStenciled"; 3 | 4 | export function drawMasked(content: () => void, mask: () => void) { 5 | const gl = _k.gfx.ggl.gl; 6 | 7 | drawStenciled(content, mask, gl.EQUAL); 8 | } 9 | -------------------------------------------------------------------------------- /src/gfx/draw/drawRaw.ts: -------------------------------------------------------------------------------- 1 | import { Asset } from "../../assets/asset"; 2 | import { resolveShader, type Uniform } from "../../assets/shader"; 3 | import { _k } from "../../shared"; 4 | import { type Attributes, BlendMode, type RenderProps } from "../../types"; 5 | import type { Texture } from "../gfx"; 6 | import { height, width } from "../stack"; 7 | 8 | export function drawRaw( 9 | attributes: Attributes, 10 | indices: number[], 11 | fixed: boolean = false, 12 | tex?: Texture, 13 | shaderSrc?: RenderProps["shader"], 14 | uniform?: Uniform, 15 | blend?: BlendMode, 16 | ) { 17 | const parsedTex = tex ?? _k.gfx.defTex; 18 | const parsedShader = shaderSrc ?? _k.gfx.defShader; 19 | const shader = resolveShader(parsedShader); 20 | 21 | if (!shader || shader instanceof Asset) { 22 | return; 23 | } 24 | 25 | const transform = _k.gfx.transform; 26 | 27 | const vertLength = attributes.pos.length / 2; 28 | const vv: number[] = new Array(vertLength * 8); 29 | 30 | let index = 0; 31 | for (let i = 0; i < vertLength; i++) { 32 | _k.gfx.scratchPt.x = attributes.pos[i * 2]; 33 | _k.gfx.scratchPt.y = attributes.pos[i * 2 + 1]; 34 | transform.transformPointV(_k.gfx.scratchPt, _k.gfx.scratchPt); 35 | 36 | vv[index++] = _k.gfx.scratchPt.x; 37 | vv[index++] = _k.gfx.scratchPt.y; 38 | vv[index++] = attributes.uv[i * 2]; 39 | vv[index++] = attributes.uv[i * 2 + 1]; 40 | vv[index++] = attributes.color[i * 3] / 255; 41 | vv[index++] = attributes.color[i * 3 + 1] / 255; 42 | vv[index++] = attributes.color[i * 3 + 2] / 255; 43 | vv[index++] = attributes.opacity[i]; 44 | } 45 | 46 | _k.gfx.renderer.push( 47 | _k.gfx.ggl.gl.TRIANGLES, 48 | vv, 49 | indices, 50 | shader, 51 | parsedTex, 52 | uniform, 53 | blend ?? BlendMode.Normal, 54 | width(), 55 | height(), 56 | _k.gfx.fixed || fixed, 57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/gfx/draw/drawStenciled.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { flush } from "../stack"; 3 | 4 | export function drawStenciled( 5 | content: () => void, 6 | mask: () => void, 7 | test: number, 8 | ) { 9 | const gl = _k.gfx.ggl.gl; 10 | 11 | flush(); 12 | gl.clear(gl.STENCIL_BUFFER_BIT); 13 | gl.enable(gl.STENCIL_TEST); 14 | 15 | // don't perform test, pure write 16 | gl.stencilFunc( 17 | gl.NEVER, 18 | 1, 19 | 0xFF, 20 | ); 21 | 22 | // always replace since we're writing to the buffer 23 | gl.stencilOp( 24 | gl.REPLACE, 25 | gl.REPLACE, 26 | gl.REPLACE, 27 | ); 28 | 29 | mask(); 30 | flush(); 31 | 32 | // perform test 33 | gl.stencilFunc( 34 | test, 35 | 1, 36 | 0xFF, 37 | ); 38 | 39 | // don't write since we're only testing 40 | gl.stencilOp( 41 | gl.KEEP, 42 | gl.KEEP, 43 | gl.KEEP, 44 | ); 45 | 46 | content(); 47 | flush(); 48 | gl.disable(gl.STENCIL_TEST); 49 | } 50 | -------------------------------------------------------------------------------- /src/gfx/draw/drawSubstracted.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { drawStenciled } from "./drawStenciled"; 3 | 4 | export function drawSubtracted(content: () => void, mask: () => void) { 5 | const gl = _k.gfx.ggl.gl; 6 | 7 | drawStenciled(content, mask, gl.NOTEQUAL); 8 | } 9 | -------------------------------------------------------------------------------- /src/gfx/draw/drawTriangle.ts: -------------------------------------------------------------------------------- 1 | import type { Vec2 } from "../../math/Vec2"; 2 | import type { RenderProps } from "../../types"; 3 | import { drawPolygon } from "./drawPolygon"; 4 | 5 | /** 6 | * How the triangle should look like. 7 | */ 8 | export type DrawTriangleOpt = RenderProps & { 9 | /** 10 | * First point of triangle. 11 | */ 12 | p1: Vec2; 13 | /** 14 | * Second point of triangle. 15 | */ 16 | p2: Vec2; 17 | /** 18 | * Third point of triangle. 19 | */ 20 | p3: Vec2; 21 | /** 22 | * If fill the shape with color (set this to false if you only want an outline). 23 | */ 24 | fill?: boolean; 25 | /** 26 | * The radius of each corner. 27 | */ 28 | radius?: number; 29 | }; 30 | 31 | export function drawTriangle(opt: DrawTriangleOpt) { 32 | if (!opt.p1 || !opt.p2 || !opt.p3) { 33 | throw new Error( 34 | "drawTriangle() requires properties \"p1\", \"p2\" and \"p3\".", 35 | ); 36 | } 37 | 38 | return drawPolygon(Object.assign({}, opt, { 39 | pts: [opt.p1, opt.p2, opt.p3], 40 | })); 41 | } 42 | -------------------------------------------------------------------------------- /src/gfx/draw/drawUnscaled.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../../shared"; 2 | import { flush } from "../stack"; 3 | 4 | export function drawUnscaled(content: () => void) { 5 | flush(); 6 | const ow = _k.gfx.width; 7 | const oh = _k.gfx.height; 8 | _k.gfx.width = _k.gfx.viewport.width; 9 | _k.gfx.height = _k.gfx.viewport.height; 10 | content(); 11 | flush(); 12 | _k.gfx.width = ow; 13 | _k.gfx.height = oh; 14 | } 15 | -------------------------------------------------------------------------------- /src/math/clamp.ts: -------------------------------------------------------------------------------- 1 | export const clamp = ( 2 | val: number, 3 | min: number, 4 | max: number, 5 | ): number => { 6 | if (min > max) { 7 | return clamp(val, max, min); 8 | } 9 | return Math.min(Math.max(val, min), max); 10 | }; 11 | -------------------------------------------------------------------------------- /src/math/lerp.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "./color"; 2 | import { Vec2 } from "./Vec2"; 3 | 4 | /** 5 | * A valid value for lerp. 6 | * 7 | * @group Math 8 | */ 9 | export type LerpValue = number | Vec2 | Color; 10 | 11 | export function lerp( 12 | a: V, 13 | b: V, 14 | t: number, 15 | ): V { 16 | if (typeof a === "number" && typeof b === "number") { 17 | // we don't call lerpNumber just for performance, but should be the same 18 | return a + (b - a) * t as V; 19 | } 20 | // check for Vec2 21 | else if (a instanceof Vec2 && b instanceof Vec2) { 22 | return a.lerp(b, t) as V; 23 | } 24 | else if (a instanceof Color && b instanceof Color) { 25 | return a.lerp(b, t) as V; 26 | } 27 | 28 | throw new Error( 29 | `Bad value for lerp(): ${a}, ${b}. Only number, Vec2 and Color is supported.`, 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/math/lerpNumber.ts: -------------------------------------------------------------------------------- 1 | export function lerpNumber( 2 | a: number, 3 | b: number, 4 | t: number, 5 | ) { 6 | return a + (b - a) * t; 7 | } 8 | -------------------------------------------------------------------------------- /src/math/minkowski.ts: -------------------------------------------------------------------------------- 1 | import type { Shape } from "../types"; 2 | import { Rect, vec2 } from "./math"; 3 | import { Vec2 } from "./Vec2"; 4 | 5 | function minkowskiRectDifference(r1: Rect, r2: Rect): Rect { 6 | return new Rect( 7 | vec2( 8 | r1.pos.x - (r2.pos.x + r2.width), 9 | r1.pos.y - (r2.pos.y + r2.height), 10 | ), 11 | r1.width + r2.width, 12 | r1.height + r2.height, 13 | ); 14 | } 15 | 16 | export function minkowskiRectShapeIntersection(shape1: Shape, shape2: Shape) { 17 | const s1 = shape1 instanceof Rect 18 | ? shape1 19 | : shape1.bbox(); 20 | const s2 = shape2 instanceof Rect 21 | ? shape2 22 | : shape2.bbox(); 23 | const res = minkowskiRectDifference(s1, s2); 24 | 25 | if (!res.contains(new Vec2())) { 26 | return null; 27 | } 28 | 29 | const distance = Math.min( 30 | Math.abs(res.pos.x), 31 | Math.abs(res.pos.x + res.width), 32 | Math.abs(res.pos.y), 33 | Math.abs(res.pos.y + res.height), 34 | ); 35 | 36 | let normal = vec2(); 37 | 38 | switch (distance) { 39 | case Math.abs(res.pos.x): 40 | normal = vec2(1, 0); 41 | break; 42 | case Math.abs(res.pos.x + res.width): 43 | normal = vec2(-1, 0); 44 | break; 45 | case Math.abs(res.pos.y): 46 | normal = vec2(0, 1); 47 | break; 48 | case Math.abs(res.pos.y + res.height): 49 | normal = vec2(0, -1); 50 | break; 51 | } 52 | 53 | return { 54 | normal, 55 | distance, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/math/vec3.ts: -------------------------------------------------------------------------------- 1 | export class Vec3 { 2 | x: number; 3 | y: number; 4 | z: number; 5 | constructor(x: number, y: number, z: number) { 6 | this.x = x; 7 | this.y = y; 8 | this.z = z; 9 | } 10 | 11 | dot(other: Vec3) { 12 | return this.x * other.x + this.y * other.y + this.z * other.z; 13 | } 14 | 15 | cross(other: Vec3) { 16 | return new Vec3( 17 | this.y * other.z - this.z * other.y, 18 | this.z * other.x - this.x * other.z, 19 | this.x * other.y - this.y * other.x, 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | import type { Engine } from "./core/engine"; 2 | 3 | /** 4 | * KAPLAY.js internal data 5 | */ 6 | 7 | export let _k: Engine; 8 | 9 | export function updateEngine(e: Engine) { 10 | _k = e; 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/asserts.ts: -------------------------------------------------------------------------------- 1 | import { Color } from "../math/color"; 2 | import { Vec2 } from "../math/Vec2"; 3 | 4 | export function arrayIsColor(arr: unknown[]): arr is Color[] { 5 | return arr[0] instanceof Color; 6 | } 7 | 8 | export function arrayIsVec2(arr: unknown[]): arr is Vec2[] { 9 | return arr[0] instanceof Vec2; 10 | } 11 | 12 | export function arrayIsNumber(arr: unknown[]): arr is number[] { 13 | return typeof arr[0] === "number"; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/benchmark.ts: -------------------------------------------------------------------------------- 1 | export function benchmark(task: () => void, times: number = 1) { 2 | const t1 = performance.now(); 3 | for (let i = 0; i < times; i++) { 4 | task(); 5 | } 6 | const t2 = performance.now(); 7 | return t2 - t1; 8 | } 9 | 10 | export function comparePerf(t1: () => void, t2: () => void, times: number = 1) { 11 | return benchmark(t2, times) / benchmark(t1, times); 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/dataURL.ts: -------------------------------------------------------------------------------- 1 | export function base64ToArrayBuffer(base64: string): ArrayBuffer { 2 | const binstr = window.atob(base64); 3 | const len = binstr.length; 4 | const bytes = new Uint8Array(len); 5 | for (let i = 0; i < len; i++) { 6 | bytes[i] = binstr.charCodeAt(i); 7 | } 8 | return bytes.buffer; 9 | } 10 | 11 | export function dataURLToArrayBuffer(url: string): ArrayBuffer { 12 | return base64ToArrayBuffer(url.split(",")[1]); 13 | } 14 | 15 | export function download(filename: string, url: string) { 16 | const a = document.createElement("a"); 17 | a.href = url; 18 | a.download = filename; 19 | a.click(); 20 | } 21 | 22 | export function downloadText(filename: string, text: string) { 23 | download(filename, "data:text/plain;charset=utf-8," + text); 24 | } 25 | 26 | export function downloadJSON(filename: string, data: any) { 27 | downloadText(filename, JSON.stringify(data)); 28 | } 29 | 30 | export function downloadBlob(filename: string, blob: Blob) { 31 | const url = URL.createObjectURL(blob); 32 | download(filename, url); 33 | URL.revokeObjectURL(url); 34 | } 35 | 36 | export const isDataURL = (str: string) => str.match(/^data:\w+\/\w+;base64,.+/); 37 | 38 | export const getFileName = (p: string) => p.split(".").slice(0, -1).join("."); 39 | -------------------------------------------------------------------------------- /src/utils/deepEq.ts: -------------------------------------------------------------------------------- 1 | export function deepEq(o1: any, o2: any): boolean { 2 | if (o1 === o2) { 3 | return true; 4 | } 5 | const t1 = typeof o1; 6 | const t2 = typeof o2; 7 | if (t1 !== t2) { 8 | return false; 9 | } 10 | if (t1 === "object" && t2 === "object" && o1 !== null && o2 !== null) { 11 | if (Array.isArray(o1) !== Array.isArray(o2)) { 12 | return false; 13 | } 14 | const k1 = Object.keys(o1); 15 | const k2 = Object.keys(o2); 16 | if (k1.length !== k2.length) { 17 | return false; 18 | } 19 | for (const k of k1) { 20 | const v1 = o1[k]; 21 | const v2 = o2[k]; 22 | if (!deepEq(v1, v2)) { 23 | return false; 24 | } 25 | } 26 | return true; 27 | } 28 | return false; 29 | } 30 | -------------------------------------------------------------------------------- /src/utils/log.ts: -------------------------------------------------------------------------------- 1 | import { _k } from "../shared"; 2 | 3 | export const getErrorMessage = (error: unknown) => 4 | (error instanceof Error) ? error.message : String(error); 5 | 6 | export function deprecate( 7 | oldName: string, 8 | newName: string, 9 | newFunc: (...args: unknown[]) => any, 10 | ) { 11 | return (...args: unknown[]) => { 12 | deprecateMsg(oldName, newName); 13 | return newFunc(...args); 14 | }; 15 | } 16 | 17 | export function warn(msg: string) { 18 | if (!_k.game.warned.has(msg)) { 19 | _k.game.warned.add(msg); 20 | console.warn(msg); 21 | } 22 | } 23 | 24 | export function deprecateMsg(oldName: string, newName: string) { 25 | warn(`${oldName} is deprecated. Use ${newName} instead.`); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | export function toFixed(n: number, f: number) { 2 | return Number(n.toFixed(f)); 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/overload.ts: -------------------------------------------------------------------------------- 1 | type Func = (...args: any[]) => any; 2 | 3 | export function overload2( 4 | fn1: A, 5 | fn2: B, 6 | ): A & B { 7 | return ((...args) => { 8 | const al = args.length; 9 | if (al === fn1.length) return fn1(...args); 10 | if (al === fn2.length) return fn2(...args); 11 | }) as A & B; 12 | } 13 | 14 | export function overload3< 15 | A extends Func, 16 | B extends Func, 17 | C extends Func, 18 | >(fn1: A, fn2: B, fn3: C): A & B & C { 19 | return ((...args) => { 20 | const al = args.length; 21 | if (al === fn1.length) return fn1(...args); 22 | if (al === fn2.length) return fn2(...args); 23 | if (al === fn3.length) return fn3(...args); 24 | }) as A & B & C; 25 | } 26 | 27 | export function overload4< 28 | A extends Func, 29 | B extends Func, 30 | C extends Func, 31 | D extends Func, 32 | >(fn1: A, fn2: B, fn3: C, fn4: D): A & B & C & D { 33 | return ((...args) => { 34 | const al = args.length; 35 | if (al === fn1.length) return fn1(...args); 36 | if (al === fn2.length) return fn2(...args); 37 | if (al === fn3.length) return fn3(...args); 38 | if (al === fn4.length) return fn4(...args); 39 | }) as A & B & C & D; 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/sets.ts: -------------------------------------------------------------------------------- 1 | export const isEqOrIncludes = (listOrSmt: T | T[], el: unknown): boolean => { 2 | if (Array.isArray(listOrSmt)) { 3 | return (listOrSmt as any[])?.includes(el); 4 | } 5 | 6 | return listOrSmt === el; 7 | }; 8 | 9 | export const setHasOrIncludes = ( 10 | set: Set, 11 | key: K | K[], 12 | ): boolean => { 13 | if (Array.isArray(key)) { 14 | return key.some((k) => set.has(k)); 15 | } 16 | 17 | return set.has(key); 18 | }; 19 | 20 | export const mapAddOrPush = ( 21 | map: Map, 22 | key: K, 23 | value: V, 24 | ): void => { 25 | if (map.has(key)) { 26 | map.get(key)?.push(value); 27 | } 28 | else { 29 | map.set(key, [value]); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert a union type to an intersection type. 3 | */ 4 | export type UnionToIntersection = ( 5 | U extends any ? (k: U) => void : never 6 | ) extends (k: infer I) => void ? I 7 | : never; 8 | 9 | /** 10 | * It removes the properties that are undefined. 11 | */ 12 | export type Defined = T extends any 13 | ? Pick 14 | : never; 15 | 16 | /** 17 | * It obligates to TypeScript to Expand the type. 18 | * 19 | * Instead of being `{ id: 1 } | { name: "hi" }` 20 | * makes 21 | * It's `{ id: 1, name: "hi" }` 22 | * 23 | * https://www.totaltypescript.com/concepts/the-prettify-helper 24 | */ 25 | export type Expand = T extends infer U ? { [K in keyof U]: U[K] } : never; 26 | 27 | /** 28 | * It merges all the objects into one. 29 | */ 30 | export type MergeObj = Expand>>; 31 | 32 | export type TupleWithoutFirst = T extends [infer R, ...infer E] 33 | ? E 34 | : never; 35 | -------------------------------------------------------------------------------- /tests/.env.d.ts: -------------------------------------------------------------------------------- 1 | import type { kaplay as KAPLAY } from "../src/kaplay"; 2 | 3 | declare global { 4 | const kaplay: typeof KAPLAY; 5 | } 6 | -------------------------------------------------------------------------------- /tests/auto/color.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { Color, rgb } from "../../src/math/color"; 3 | 4 | describe("Color creation using class", () => { 5 | test("new Color(44, 44, 44) should return Color(44, 44, 44)", () => { 6 | const color = new Color(44, 44, 44); 7 | expect(color).toEqual({ r: 44, g: 44, b: 44 }); 8 | }); 9 | 10 | test("Color().fromArray([22, 43, 79]) should return Color(22, 43, 79)", () => { 11 | const color = Color.fromArray([22, 43, 79]); 12 | expect(color).toEqual({ r: 22, g: 43, b: 79 }); 13 | }); 14 | 15 | test("Color().fromHex(0x123456) should return Color(18, 52, 86)", () => { 16 | const color = Color.fromHex(0x123456); 17 | expect(color).toEqual({ r: 18, g: 52, b: 86 }); 18 | }); 19 | 20 | test("Color().fromHex('#234567') should return Color(35, 69, 103)", () => { 21 | const color = Color.fromHex("#234567"); 22 | expect(color).toEqual({ r: 35, g: 69, b: 103 }); 23 | }); 24 | }); 25 | 26 | describe("Color creation using rbg() utility", () => { 27 | test("rgb(255, 255, 255) should return Color(255, 255, 255)", () => { 28 | const color = rgb(255, 255, 255); 29 | expect(color).toEqual({ r: 255, g: 255, b: 255 }); 30 | }); 31 | 32 | test("rgb(11, 25, 6) should return Color(11, 25, 6)", () => { 33 | const color = rgb(11, 25, 6); 34 | expect(color).toEqual({ r: 11, g: 25, b: 6 }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/auto/kaplay.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | 3 | describe("Context Initialization", () => { 4 | beforeAll(async () => { 5 | await page.addScriptTag({ path: "dist/kaplay.js" }); 6 | }); 7 | 8 | test( 9 | "VERSION constant shouldn't be defined in global scope when kaplay({ global: false })", 10 | async () => { 11 | const version = await page.evaluate(() => { 12 | kaplay({ global: false }); 13 | // @ts-ignore 14 | return window["VERSION"]; 15 | }); 16 | 17 | expect(version).toBeUndefined(); 18 | }, 19 | 20000, 20 | ); 21 | 22 | test( 23 | "VERSION constant should be defined in global scope wwhen kaplay()", 24 | async () => { 25 | const version = await page.evaluate(() => { 26 | kaplay(); 27 | // @ts-ignore 28 | return window["VERSION"]; 29 | }); 30 | 31 | expect(version).toBeDefined(); 32 | }, 33 | 20000, 34 | ); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/auto/missingComps.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | 3 | // [subject] should [behavior when condition] 4 | 5 | describe("Components validation in add()", async () => { 6 | beforeAll(async () => { 7 | await page.addScriptTag({ path: "dist/kaplay.js" }); 8 | }); 9 | 10 | test( 11 | "add() should throw an error when a body() without pos() is passed", 12 | async () => { 13 | async function useBodyWithoutPos() { 14 | return page.evaluate(() => { 15 | const k = kaplay(); 16 | 17 | return new Promise((res, rej) => { 18 | k.onError((e) => { 19 | console.log(e); 20 | rej(e.message); 21 | }); 22 | 23 | k.add([ 24 | k.body(), 25 | ]); 26 | }); 27 | }); 28 | } 29 | 30 | await expect(useBodyWithoutPos).rejects.toThrow(/requires/); 31 | }, 32 | 20000, 33 | ); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/auto/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from "vitest"; 2 | import type { KAPLAYCtx } from "../../src/types"; 3 | 4 | describe("Plugin loading", () => { 5 | beforeAll(async () => { 6 | await page.addScriptTag({ path: "dist/kaplay.js" }); 7 | }); 8 | 9 | test("testPlugin methods should exist in context", async () => { 10 | const method = await page.evaluate(() => { 11 | const testPlugin = (k: KAPLAYCtx) => ({ 12 | myMethod() { 13 | return k.VERSION; 14 | }, 15 | }); 16 | 17 | const k = kaplay({ plugins: [testPlugin] }); 18 | 19 | return k.myMethod; 20 | }); 21 | 22 | expect(method).toBeDefined(); 23 | }, 20000); 24 | 25 | test("testPlugin methods should work in context", async () => { 26 | const [version, methodResult] = await page.evaluate(() => { 27 | const testPlugin = (k: KAPLAYCtx) => ({ 28 | myMethod() { 29 | return k.VERSION; 30 | }, 31 | }); 32 | 33 | const k = kaplay({ plugins: [testPlugin] }); 34 | 35 | return [k.VERSION, k.myMethod()]; 36 | }); 37 | 38 | expect(methodResult).toBe(version); 39 | }, 20000); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/playtests/colorCSS.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file CSS Colors 3 | * @description Use CSS-defined colors colors in KAPLAY. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | * @test 8 | */ 9 | 10 | kaplay(); 11 | loadHappy(); 12 | 13 | add([ 14 | rect(512, 512, { 15 | radius: [0, 96, 96, 96], 16 | }), 17 | color("rebeccapurple"), 18 | pos(40, 40), 19 | ]); 20 | 21 | add([ 22 | text("css", { size: 192, font: "happy" }), 23 | pos(90, 310), 24 | ]); 25 | -------------------------------------------------------------------------------- /tests/playtests/compsMissing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Missing comps 3 | * @description Test behaviour when missing components at add(). 4 | * @difficulty 3 5 | * @tags comps 6 | * @minver 3001.0 7 | */ 8 | 9 | // Test when a dependency is missing on obj creation 10 | 11 | const k = kaplay({}); 12 | 13 | k.onError((e) => { 14 | if (e == "Error: Component \"body\" requires component \"pos\"") { 15 | console.log("TEST PASSED"); 16 | } 17 | }); 18 | 19 | k.add([ 20 | k.body(), 21 | ]); 22 | -------------------------------------------------------------------------------- /tests/playtests/compsMissingDynamic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Missing components on run-time 3 | * @description Test missing component dependencies at run-time. 4 | * @difficulty 3 5 | * @tags comps 6 | * @minver 3001.0 7 | */ 8 | 9 | // Test when a dependency is missing on obj.use() 10 | 11 | const k = kaplay({}); 12 | 13 | k.onError((e) => { 14 | if (e == "Error: Component \"body\" requires component \"pos\"") { 15 | console.log("TEST PASSED"); 16 | } 17 | }); 18 | 19 | const dummy = k.add([]); 20 | 21 | dummy.use(body()); 22 | -------------------------------------------------------------------------------- /tests/playtests/debugFramePhysics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Step Frame 3 | * @description Test different physics using debug.stepFrame(). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | // Build levels with addLevel() 10 | 11 | // Start game 12 | kaplay(); 13 | 14 | // Load assets 15 | loadSprite("coin", "/sprites/coin.png"); 16 | loadSprite("grass", "/sprites/grass.png"); 17 | 18 | setGravity(2400); 19 | 20 | addLevel([ 21 | // Design the level layout with symbols 22 | " ", 23 | " ", 24 | " ", 25 | " ", 26 | "=======", 27 | ], { 28 | // The size of each grid 29 | tileWidth: 64, 30 | tileHeight: 64, 31 | // The position of the top left block 32 | pos: vec2(100), 33 | // Define what each symbol means (in components) 34 | tiles: { 35 | "=": () => [ 36 | sprite("grass"), 37 | area(), 38 | body({ isStatic: true }), 39 | ], 40 | }, 41 | }); 42 | 43 | loop(0.2, () => { 44 | const coin = add([ 45 | pos(rand(100, 400), 0), 46 | sprite("coin"), 47 | area(), 48 | body(), 49 | "coin", 50 | ]); 51 | wait(3, () => coin.destroy()); 52 | }); 53 | 54 | debug.paused = true; 55 | 56 | onKeyPressRepeat("space", () => { 57 | debug.stepFrame(); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/playtests/debugTimeScale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Time Scale 3 | * @description Test time scale and dt() difference. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | let firstFrameDt; 12 | 13 | wait(0.5, () => { 14 | firstFrameDt = dt(); 15 | debug.timeScale = 2; 16 | }); 17 | 18 | wait(1, () => { 19 | const newDt = dt(); 20 | 21 | if (newDt > firstFrameDt) { 22 | debug.log( 23 | `✔ TEST PASSED. 1st dt: ${firstFrameDt}, modified dt: ${newDt}`, 24 | ); 25 | } 26 | else { 27 | debug.log(`TEST FAILED.`); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /tests/playtests/fastLoop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fast loop 3 | * @description TBD 4 | * @difficulty 0 5 | * @tags ui, input 6 | * @minver 4000.0 7 | * @category concepts 8 | */ 9 | 10 | kaplay(); 11 | 12 | loadBean(); 13 | 14 | const interval = 0.001; 15 | const delay = 1; 16 | 17 | loop(interval, () => { 18 | add([ 19 | sprite("bean"), 20 | pos(rand(vec2(0), vec2(width(), height()))), 21 | lifespan(delay), 22 | opacity(1), 23 | ]); 24 | }); 25 | 26 | const counter = add([ 27 | pos(10, 10), 28 | text(""), 29 | z(9999), 30 | ]); 31 | add([ 32 | pos(counter.pos), 33 | rect(counter.width, counter.height), 34 | color(BLACK), 35 | z(counter.z - 1), 36 | { 37 | update() { 38 | this.width = counter.width; 39 | this.height = counter.height; 40 | }, 41 | }, 42 | ]); 43 | 44 | var beanCount = 0; 45 | onAdd(() => beanCount++); 46 | onDestroy(() => beanCount--); 47 | loop(0.1, () => { 48 | const error = 100 * Math.abs((delay / interval) - beanCount) 49 | / (delay / interval); 50 | counter.text = `${beanCount.toFixed().padStart(5)} beans\nerror: ${ 51 | error.toFixed(2).padStart(5) 52 | }%`; 53 | }); 54 | -------------------------------------------------------------------------------- /tests/playtests/fixedUpdate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Fixed Update 3 | * @description TBD 4 | * @difficulty 1 5 | * @tags ui, input 6 | * @minver 4000.0 7 | */ 8 | // @ts-check 9 | 10 | kaplay(); 11 | 12 | let fixedCount = 0; 13 | let count = 0; 14 | 15 | onFixedUpdate(() => { 16 | fixedCount++; 17 | }); 18 | 19 | onUpdate(() => { 20 | count++; 21 | debug.log( 22 | `${fixedDt()} ${Math.floor(fixedCount / time())} ${dt()} ${ 23 | Math.floor(count / time()) 24 | }`, 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/playtests/hover.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | background: "#f2ae99", 3 | }); 4 | 5 | loadBean(); 6 | 7 | const origPos = add([ 8 | anchor("center"), 9 | pos(center().x, 100), 10 | circle(4), 11 | color("#5ba675"), 12 | outline(4, { color: BLACK }), 13 | ]); 14 | 15 | const card = add([ 16 | anchor("center"), 17 | pos(origPos.pos), 18 | rect(300, 100, { radius: 16 }), 19 | color(WHITE), 20 | outline(8, BLACK), 21 | ]); 22 | 23 | const bean = card.add([ 24 | sprite("bean"), 25 | anchor("center"), 26 | scale(2), 27 | area(), 28 | z(1), 29 | ]); 30 | 31 | bean.onHover(() => { 32 | setCursor("pointer"); 33 | card.color = Color.fromHex("#5ba675"); 34 | }); 35 | 36 | bean.onHoverEnd(() => { 37 | setCursor("default"); 38 | card.color = WHITE; 39 | }); 40 | 41 | wait(2, () => { 42 | tween( 43 | card.pos.y, 44 | center().y, 45 | 0.25, 46 | y => card.pos.y = y, 47 | easings.easeOutBack, 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/playtests/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": [ 4 | "../../dist/declaration/global.d.ts" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/playtests/largeTexture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Large Texture 3 | * @description Test how KAPLAY handles large textures (big sprites). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | // Loads a random 2500px image 12 | loadSprite("bigyoshi", "/sprites/YOSHI.png"); 13 | 14 | let cameraPosition = getCamPos(); 15 | 16 | add([ 17 | sprite("bigyoshi"), 18 | ]); 19 | 20 | // Adds a label 21 | const label = add([ 22 | text("Click and drag the mouse, scroll the wheel"), 23 | z(1), 24 | ]); 25 | 26 | add([ 27 | rect(label.width, label.height), 28 | color(0, 0, 0), 29 | z(0), 30 | ]); 31 | 32 | // Mouse handling 33 | onUpdate(() => { 34 | if (isMouseDown("left") && isMouseMoved()) { 35 | cameraPosition = cameraPosition.sub( 36 | mouseDeltaPos().scale(1 / cameraScale), 37 | ); 38 | setCamPos(cameraPosition); 39 | } 40 | }); 41 | 42 | let cameraScale = 1; 43 | 44 | onScroll((delta) => { 45 | cameraScale = cameraScale * (1 - 0.1 * Math.sign(delta.y)); 46 | setCamScale(cameraScale); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/playtests/mouse.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 1280, 3 | height: 720, 4 | pixelDensity: 2, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | fakeMouse(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseLetterbox.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 400, 3 | height: 200, 4 | letterbox: true, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | ]); 13 | 14 | onMouseMove(pos => { 15 | redDot.pos = toWorld(pos); 16 | }); 17 | 18 | onKeyPress("f", () => { 19 | setFullscreen(!isFullscreen()); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/playtests/mouseLetterbox2.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | width: 200, 3 | height: 400, 4 | letterbox: true, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | ]); 13 | 14 | onMouseMove(pos => { 15 | redDot.pos = toWorld(pos); 16 | }); 17 | 18 | onKeyPress("f", () => { 19 | setFullscreen(!isFullscreen()); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/playtests/mouseScale.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 7, 3 | width: 200, 4 | height: 400, 5 | }); 6 | 7 | const redDot = add([ 8 | anchor("center"), 9 | circle(3), 10 | color(RED), 11 | pos(), 12 | fakeMouse(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleLetterbox.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 7, 3 | width: 400, 4 | height: 200, 5 | letterbox: true, 6 | }); 7 | 8 | const redDot = add([ 9 | anchor("center"), 10 | circle(3), 11 | color(RED), 12 | pos(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleLetterbox2.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 5, 3 | width: 200, 4 | height: 400, 5 | letterbox: true, 6 | }); 7 | 8 | const redDot = add([ 9 | anchor("center"), 10 | circle(3), 11 | color(RED), 12 | pos(), 13 | ]); 14 | 15 | onMouseMove(pos => { 16 | redDot.pos = toWorld(pos); 17 | }); 18 | 19 | onKeyPress("f", () => { 20 | setFullscreen(!isFullscreen()); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/mouseScaleStretch.js: -------------------------------------------------------------------------------- 1 | kaplay({ 2 | scale: 3, 3 | }); 4 | 5 | const redDot = add([ 6 | anchor("center"), 7 | circle(3), 8 | color(RED), 9 | pos(), 10 | ]); 11 | 12 | onMouseMove(pos => { 13 | redDot.pos = toWorld(pos); 14 | }); 15 | 16 | onKeyPress("f", () => { 17 | setFullscreen(!isFullscreen()); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/playtests/mouseStretch.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | const redDot = add([ 4 | anchor("center"), 5 | circle(3), 6 | color(RED), 7 | pos(), 8 | ]); 9 | 10 | onMouseMove(pos => { 11 | redDot.pos = toWorld(pos); 12 | }); 13 | 14 | onKeyPress("f", () => { 15 | setFullscreen(!isFullscreen()); 16 | }); 17 | -------------------------------------------------------------------------------- /tests/playtests/parenttest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Parent Test 3 | * @description TBD. 4 | * @difficulty 1 5 | * @tags basics 6 | * @minver 4000.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | loadBean(); 12 | 13 | const centerBean = add([ 14 | pos(center()), 15 | anchor("center"), 16 | sprite("bean"), 17 | area(), 18 | rotate(0), 19 | scale(2), 20 | { 21 | update() { 22 | this.angle += 20 * dt(); 23 | }, 24 | }, 25 | ]); 26 | 27 | const orbitingBean = centerBean.add([ 28 | pos(vec2(100, 0)), 29 | anchor("center"), 30 | sprite("bean"), 31 | area(), 32 | rotate(0), 33 | scale(1), 34 | color(), 35 | { 36 | update() { 37 | this.angle = -this.parent.transform.getRotation(); 38 | if (this.isHovering()) { 39 | this.color = RED; 40 | } 41 | else { 42 | this.color = WHITE; 43 | } 44 | }, 45 | }, 46 | ]); 47 | 48 | onMousePress(() => { 49 | if (orbitingBean.parent === centerBean /* && orbitingBean.isHovering()*/) { 50 | orbitingBean.setParent(getTreeRoot(), { keep: KeepFlags.All }); 51 | } 52 | }); 53 | 54 | onMouseMove((pos, delta) => { 55 | if (orbitingBean.parent !== centerBean) { 56 | orbitingBean.pos = orbitingBean.pos.add(delta); 57 | } 58 | }); 59 | 60 | onMouseRelease(() => { 61 | if (orbitingBean.parent !== centerBean) { 62 | orbitingBean.setParent(centerBean, { keep: KeepFlags.All }); 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /tests/playtests/paused.js: -------------------------------------------------------------------------------- 1 | kaplay({ scale: 2 }); 2 | 3 | loadBean(); 4 | 5 | let countdown = 4; 6 | const bean = add([ 7 | pos(center()), 8 | sprite("bean"), 9 | anchor("center"), 10 | rotate(), 11 | area(), 12 | ]); 13 | 14 | // Listeners registered after paused will run regardless 15 | bean.paused = true; 16 | 17 | loop(1, () => { 18 | debug.log("will pause in", countdown -= 1); 19 | }, countdown).then(() => { 20 | bean.paused = true; 21 | debug.log("paused, shouldn't spin on space down"); 22 | }); 23 | 24 | bean.onKeyDown("space", () => { 25 | bean.angle += dt() * 100; 26 | }); 27 | 28 | bean.onUpdate(() => { 29 | debug.log("updateee"); 30 | }); 31 | 32 | bean.onClick(() => { 33 | debug.log("clicked"); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/playtests/prettyDebug.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Pretty Debug 3 | * @description Will see how pretty is our debug log 4 | * @difficulty 3 5 | * @tags debug 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const pretty = { 12 | i: "am pretty", 13 | all: "own properties are shown", 14 | even: { 15 | nested: "objects", 16 | }, 17 | arrays: ["show", "like", "you", "would", "write", "them"], 18 | "own toString is used": vec2(10, 10), 19 | }; 20 | 21 | pretty.recursive = pretty; 22 | 23 | debug.log("Text in [brackets] doesn't cause issues"); 24 | debug.log(pretty); // recursive doesn't cause issues 25 | debug.error("This is an error message"); // errors in red 26 | -------------------------------------------------------------------------------- /tests/playtests/raycastLevelTest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Raycast Level Test 3 | * @description Ok MF explain this 4 | * @difficulty 3 5 | * @tags debug 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const level = addLevel([ 12 | "a", 13 | ], { 14 | tileHeight: 100, 15 | tileWidth: 100, 16 | tiles: { 17 | a: () => [ 18 | rect(32, 32), 19 | area(), 20 | color(RED), 21 | ], 22 | }, 23 | }); 24 | try { 25 | level.raycast(vec2(50, 50), vec2(-50, -50)); 26 | } catch (e) { 27 | debug.error(e.stack); 28 | throw e; 29 | } 30 | -------------------------------------------------------------------------------- /tests/playtests/test536.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Offscreen test 3 | * @description Test for offscreening with camera. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | loadBean(); 12 | 13 | const bean = add([ 14 | sprite("bean"), 15 | pos(), 16 | offscreen({ pause: true, unpause: true }), 17 | rotate(), 18 | anchor("center"), 19 | { 20 | update() { 21 | this.angle += 400 * dt(); 22 | }, 23 | }, 24 | ]); 25 | 26 | camPos(0, 0); 27 | wait(1, () => { 28 | camPos(1000, 1000); 29 | wait(1, () => { 30 | camPos(0, 0); 31 | }); 32 | }); 33 | 34 | onUpdate(() => { 35 | debug.log(bean.isOffScreen()); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/playtests/textBig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Big Text 3 | * @description Testing for big big text. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | add([ 12 | text( 13 | "text text text text text text text text text text text text text text text\ntext [big]big[/big] text\ntext text text", 14 | { 15 | styles: { 16 | big() { 17 | return { 18 | scale: vec2(wave(3, 5, time())), 19 | stretchInPlace: false, 20 | }; 21 | }, 22 | }, 23 | width: width() / 2, 24 | }, 25 | ), 26 | pos(100, 100), 27 | area(), 28 | ]); 29 | debug.inspect = true; 30 | -------------------------------------------------------------------------------- /tests/playtests/textPaused.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Paused text 3 | * @description Test paused text objcts and their transforms. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | add([ 12 | text("this text should not be moving", { 13 | transform(i) { 14 | return { pos: vec2(0, wave(-10, 10, time() + i)) }; 15 | }, 16 | }), 17 | pos(100, 100), 18 | ]).paused = true; 19 | -------------------------------------------------------------------------------- /tests/playtests/textShader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Shader text with text styles 3 | * @description Test adding shaders to a text style. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | loadShaderURL("blink", null, "../..//shaders/blink.frag"); 12 | 13 | add([ 14 | pos(100, 100), 15 | text( 16 | "text [blink]i blink[/blink] text\n\n\n" 17 | + "why are the spaces so tall".replaceAll(" ", "[blink] [/blink]"), 18 | { 19 | styles: { 20 | blink: (i) => ({ 21 | shader: "blink", 22 | uniform: { 23 | u_time: time() - i / 20, 24 | u_fore: WHITE, 25 | u_back: BLACK, 26 | }, 27 | }), 28 | }, 29 | }, 30 | ), 31 | ]); 32 | -------------------------------------------------------------------------------- /tests/playtests/textStyleChangeFont.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Style change font 3 | * @description Test how text handles changing font on styles. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000", crisp: true }); 10 | loadFont("Romantique", "//fonts/Romantique.ttf", { filter: "nearest" }); 11 | 12 | const x = add([ 13 | text( 14 | "everything here is arial except the e's are romantique and red and stretched" 15 | .replaceAll("e", "[e]e[/e]"), 16 | { 17 | transform: { 18 | font: "Arial", 19 | stretchInPlace: false, 20 | }, 21 | styles: { 22 | e: (i, ch) => { 23 | const w = wave(0.5, 2, time() * 3 + i); 24 | return { 25 | font: "Romantique", 26 | color: RED, 27 | scale: vec2(w, 1), 28 | stretchInPlace: w > 1, 29 | }; 30 | }, 31 | }, 32 | width: width(), 33 | size: 75, 34 | }, 35 | ), 36 | ]); 37 | 38 | onUpdate(() => x.width = width()); 39 | -------------------------------------------------------------------------------- /tests/playtests/textStyleOverride.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Style override 3 | * @description Test overriding different styles in text objects 4 | * @difficulty 3 5 | * @tags text 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | add([ 12 | pos(100, 100), 13 | text("No override: Hello [foo]styled[/foo] text", { 14 | transform: { 15 | color: WHITE.darken(200), 16 | }, 17 | styles: { 18 | foo: { 19 | color: RED, 20 | }, 21 | }, 22 | }), 23 | ]); 24 | 25 | add([ 26 | pos(100, 150), 27 | text("With override: Hello [foo]styled[/foo] text", { 28 | transform: { 29 | color: WHITE.darken(200), 30 | }, 31 | styles: { 32 | foo: { 33 | color: RED, 34 | override: true, 35 | }, 36 | }, 37 | }), 38 | ]); 39 | 40 | add([ 41 | pos(100, 200), 42 | text("With override and color(): Hello [foo]styled[/foo] text", { 43 | styles: { 44 | foo: { 45 | color: RED, 46 | override: true, 47 | }, 48 | }, 49 | }), 50 | color(WHITE.darken(200)), 51 | ]); 52 | -------------------------------------------------------------------------------- /tests/playtests/textUnparse.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text unparse 3 | * @description Ok dragoncoder explain this 4 | * @difficulty 3 5 | * @tags text 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const text = 12 | "hello [foo]styled[/foo] world \\[weird tag] with \\] [barbaz]some more [nested]text[/nested][/barbaz] bloop"; 13 | // const text = "hello [a][b]a[/b][/a] goodbye"; 14 | const formatted = compileStyledText(text); 15 | console.log(formatted); 16 | 17 | function tagDiff(l1, l2) { 18 | var min = 0; 19 | var out = ""; 20 | while (l1[min] === l2[min] && min < l1.length && min < l2.length) { 21 | min++; 22 | } 23 | for (var i = l1.length - 1; i >= min; i--) { 24 | out += `[/${l1[i]}]`; 25 | } 26 | for (var j = min; j < l2.length; j++) { 27 | out += `[${l2[j]}]`; 28 | } 29 | return out; 30 | } 31 | 32 | function sliceUnparse(formatted, start, end) { 33 | if (!end) end = formatted.text.length; 34 | var out = ""; 35 | var lastTags = []; 36 | for (var i = start; i < end; i++) { 37 | const curTags = formatted.charStyleMap[i] ?? []; 38 | out += tagDiff(lastTags, curTags); 39 | out += formatted.text[i].replace(/([\[\\])/g, "\\$1"); 40 | lastTags = curTags; 41 | } 42 | out += tagDiff(lastTags, []); 43 | return out; 44 | } 45 | 46 | for (var start = 0; start < formatted.text.length; start++) { 47 | for (var end = start + 1; end <= formatted.text.length; end++) { 48 | const t = sliceUnparse(formatted, start, end); 49 | console.log(t); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/playtests/textWeirdTags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text tags 3 | * @description Test the behaviour of text tags in certain cases. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const txtEl = add([ 12 | text("", { 13 | styles: { 14 | pink: { 15 | color: MAGENTA, 16 | }, 17 | }, 18 | }), 19 | pos(100, 100), 20 | ]); 21 | const base = "[pink]hello\n[/pink]ohhi\n"; 22 | const txt = "foo\n\\[1]\nbar"; 23 | var i = -1; 24 | const c = loop(0.5, () => { 25 | if (txt[i] === "\\") i++; 26 | i++; 27 | txtEl.text = base + txt.slice(0, i) + "[pink]bye[/pink]"; 28 | if (i > txt.length) { 29 | console.log(txtEl.text); 30 | c.cancel(); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /tests/playtests/textWrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Text Wrap 3 | * @description Test text wrap. 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay({ background: "#000000" }); 10 | 11 | const theText = `MAN PAGE 12 | Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. Very long description paragraph. 13 | 14 | ANOTHER SECTION 15 | Yet some more text here. Yet some more text here. Yet some more text here. Yet some more text here.`; 16 | 17 | add([ 18 | pos(100, 100), 19 | text(theText, { 20 | size: 16, 21 | width: 17 * 16, 22 | }), 23 | ]); 24 | 25 | add([ 26 | pos(400, 100), 27 | text(theText, { 28 | size: 16, 29 | width: 17 * 16, 30 | indentAll: true, 31 | }), 32 | ]); 33 | -------------------------------------------------------------------------------- /tests/playtests/timeLeft.js: -------------------------------------------------------------------------------- 1 | kaplay(); 2 | 3 | loadBean(); 4 | const bean = add([sprite("bean"), pos()]); 5 | 6 | const tweening = tween( 7 | bean.pos, 8 | center(), 9 | 5, 10 | (p) => bean.pos = p, 11 | easings.easeOutQuint, 12 | ); 13 | 14 | // Also has a setter 15 | onClick(() => { 16 | tweening.timeLeft = 5; 17 | }); 18 | 19 | onUpdate(() => { 20 | debug.log(`${tweening.currentTime}/5`); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/playtests/twokaplays.js: -------------------------------------------------------------------------------- 1 | const k = kaplay(); 2 | 3 | k.loadBean(); 4 | 5 | k.quit(); 6 | 7 | k.add([ 8 | text("hi"), 9 | ]); 10 | 11 | k.add([ 12 | sprite("bean"), 13 | ]); 14 | 15 | const k2 = kaplay(); 16 | 17 | k2.loadBean(); 18 | 19 | k2.add([ 20 | text("hi how are you?"), 21 | ]); 22 | 23 | k2.add([ 24 | sprite("bean"), 25 | ]); 26 | -------------------------------------------------------------------------------- /tests/playtests/wait1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Wait 3 | * @description Test the behaviour of wait(). 4 | * @difficulty 3 5 | * @tags basics 6 | * @minver 3001.0 7 | */ 8 | 9 | kaplay(); 10 | 11 | const txt = add([ 12 | pos(100, 100), 13 | text(""), 14 | ]); 15 | 16 | var theTime = 0; 17 | const foo = onUpdate(() => { 18 | theTime += dt(); 19 | txt.text = theTime; 20 | }); 21 | 22 | wait(1, () => { 23 | debug.log("callback"); 24 | }).onEnd(() => { 25 | debug.log("onEnd"); 26 | foo.cancel(); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "noImplicitThis": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "emitDeclarationOnly": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "declarationDir": "dist/declaration", 14 | // always use import type for type only imports 15 | "verbatimModuleSyntax": true, 16 | "strict": true, 17 | "types": [ 18 | "./.env.d.ts", 19 | "../node_modules/vitest-puppeteer/dist/index.d.ts" 20 | ] 21 | }, 22 | "include": [ 23 | "./**/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tests/types/plugins.test-d.ts: -------------------------------------------------------------------------------- 1 | import { describe, expectTypeOf, test } from "vitest"; 2 | import { kaplay } from "../../src/kaplay"; 3 | import type { KAPLAYCtx } from "../../src/types"; 4 | 5 | type ImplicitTestPlug = ReturnType; 6 | 7 | const implicitTestPlug = (k: KAPLAYCtx) => ({ 8 | getVersion() { 9 | return k.VERSION; 10 | }, 11 | }); 12 | 13 | describe("Type Inference from plugins", () => { 14 | // Inferred Plugin 15 | 16 | test("type of plugin should be inferred from kaplay({ plugins: [ implicitTestPlug ] })", () => { 17 | const k = kaplay({ plugins: [implicitTestPlug] }); 18 | 19 | k.getVersion; 20 | 21 | expectTypeOf(k).toEqualTypeOf< 22 | KAPLAYCtx<{}, never> & ImplicitTestPlug 23 | >(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "skipLibCheck": false, 8 | "resolveJsonModule": true, 9 | "emitDeclarationOnly": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "declarationDir": "dist/declaration", 13 | "strict": true 14 | }, 15 | "include": [ 16 | "src/index.ts", 17 | "src/.env.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "noImplicitThis": true, 8 | "skipLibCheck": true, 9 | "resolveJsonModule": true, 10 | "emitDeclarationOnly": true, 11 | "declaration": true, 12 | "declarationMap": true, 13 | "declarationDir": "dist/declaration", 14 | // always use import type for type only imports 15 | "verbatimModuleSyntax": true, 16 | "strict": true, 17 | "types": [ 18 | "./src/.env.d.ts" 19 | ] 20 | }, 21 | "include": [ 22 | "src/**/*", 23 | "scripts/**/*" 24 | ], 25 | "exclude": [ 26 | "src/index.ts", 27 | "./tests/**" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | name: "KAPLAY.js", 6 | environment: "puppeteer", 7 | globalSetup: "vitest-environment-puppeteer/global-init", 8 | fileParallelism: false, 9 | typecheck: { 10 | ignoreSourceErrors: true, 11 | tsconfig: "./tests/tsconfig.json", 12 | }, 13 | }, 14 | }); 15 | --------------------------------------------------------------------------------