├── .gitignore ├── bundle.sh ├── src ├── engine │ ├── camera.js │ ├── loop.js │ ├── gl.js │ ├── input.js │ └── math.js ├── main.js ├── levels.js ├── scene.js ├── palette.js ├── ui │ ├── main.js │ ├── utils.js │ ├── levels.js │ ├── custom.js │ ├── hud.js │ ├── start.js │ └── pause.js ├── sound │ ├── sounds.js │ └── zzfx.js ├── shapes.js ├── game.js ├── selector.js ├── platform.js ├── tile.js ├── backdrop.js ├── player.js └── editor.js ├── rollup.config.js ├── static ├── index.html └── app.css ├── package.json ├── README.md ├── LICENSE └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | app/ 3 | zip/ 4 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p zip 4 | rm zip/* 5 | 6 | echo Creating new zip... 7 | zip -j -9 zip/game app/* 8 | echo Finished. 9 | echo Zip size: `stat --format="%s bytes" zip/game.zip` 10 | echo 13Kb = 13,312 bytes 11 | -------------------------------------------------------------------------------- /src/engine/camera.js: -------------------------------------------------------------------------------- 1 | import { identity, orthographic, transform } from "./math"; 2 | 3 | export let projectionMatrix = identity(); 4 | 5 | export let update = (width, height) => { 6 | projectionMatrix = identity(); 7 | projectionMatrix = orthographic(0, width, height, 0, 2000, -2000); 8 | }; 9 | 10 | export let move = (x, y) => { 11 | transform(projectionMatrix, { x, y }); 12 | }; 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from "rollup-plugin-terser"; 2 | 3 | export default { 4 | input: "src/main.js", 5 | output: { 6 | file: "app/main.js", 7 | format: "iife", 8 | }, 9 | plugins: [ 10 | terser({ 11 | warnings: false, 12 | compress: { ecma: 2016, passes: 1, unsafe_arrows: true }, 13 | mangle: { module: true, toplevel: true }, 14 | }), 15 | ], 16 | }; 17 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Fourfold 8 | 9 | 10 | 11 | 12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/engine/loop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Start the game loop 3 | * Return any falsey value to terminate. 4 | * 5 | * @param {(delta: number) => void} step update function 6 | */ 7 | export let startLoop = (step, onEnd) => { 8 | let last = 0; 9 | let loop = function (now) { 10 | let dt = now - last; 11 | last = now; 12 | // Sanity check - absorb random lag spike / frame jumps 13 | // (expected delta is 1000/60 = ~16.67ms) 14 | if (dt > 500) { 15 | dt = 500; 16 | } 17 | // Stop on falsey return and run callback 18 | step(dt / 1000, now) ? requestAnimationFrame(loop) : onEnd(); 19 | }; 20 | requestAnimationFrame(loop); 21 | }; 22 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { initGame } from "./game.js"; 2 | import { showMainMenu } from "./ui/main.js"; 3 | 4 | let canvas = document.getElementById("app"); 5 | 6 | let WIDTH = 1280, 7 | HEIGHT = 720; 8 | 9 | let aspect = WIDTH / HEIGHT; 10 | canvas.width = WIDTH; 11 | canvas.height = HEIGHT; 12 | 13 | initGame(canvas, WIDTH, HEIGHT); 14 | 15 | // Maintain aspect ratio 16 | onresize = () => { 17 | canvas.height = Math.min( 18 | innerHeight, 19 | innerWidth < WIDTH ? Math.floor(innerWidth / aspect) : HEIGHT 20 | ); 21 | canvas.width = Math.min( 22 | innerWidth, 23 | innerHeight < HEIGHT ? Math.floor(innerHeight * aspect) : WIDTH 24 | ); 25 | }; 26 | onresize(); 27 | 28 | showMainMenu(); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js13k", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "prebuild": "mkdir -p app && cp static/* app/", 8 | "build": "esbuild --bundle --minify --outdir=app src/main.js", 9 | "preroll": "mkdir -p app && cp static/* app/", 10 | "roll": "rollup --config", 11 | "predevbuild": "mkdir -p app && cp static/* app/", 12 | "devbuild": "esbuild --bundle --outdir=app src/main.js", 13 | "bundle": "./bundle.sh", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "author": "", 17 | "license": "MIT", 18 | "devDependencies": { 19 | "esbuild": "^0.6.21", 20 | "rollup": "^2.26.2", 21 | "rollup-plugin-terser": "^7.0.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/levels.js: -------------------------------------------------------------------------------- 1 | export let levels = [ 2 | "7:5bca6ba6ba6bax5ba", 3 | "6:x3bab5ab5ab5ab6a5ac", 4 | "7:5babx4bab5bac", 5 | "8:cab5a2aba2b2axb6aa2ba2b2a", 6 | "7:bab3acbab3abbab3abxababab4abab", 7 | "10:2bab6a3ab6a2bacabx3a4a2b4aa2b7aaba2b5", 8 | "9:2baxbab2a6ab2ac2a2b4ab5ab2a3a2bab2a", 9 | "5:3bacb4axb3a2b3a", 10 | "7:2ac4a2ab4a7aa2b4abx2b3a", 11 | "6:x3b2a6a2a2bac", 12 | "6:x2babaa2baba3a2ba3abac", 13 | "9:3aba4bxbab5a3a2b2aba5acaba", 14 | "6:abab2aaba2baxb2abaaba2ba6a3ac2a", 15 | "6:b3dadxb2dadb3dac", 16 | "6:2acb2adx4aababda4abadbdada", 17 | "6:dbac2aad4axadb2abdbd2a6adbd3a", 18 | "10:8aca3adad4a3adada2babx2dad4a3adadabda3adad4a8ac", 19 | "8:xbdbadacbdbdab2adbdbad2abdbd2aba", 20 | "7:5aca3adb2ax2dadba2db2d2a", 21 | "9:2adbdb3a9ab2d2a2d2ax2d2a2dac9a2adbdb3a", 22 | ]; 23 | -------------------------------------------------------------------------------- /src/scene.js: -------------------------------------------------------------------------------- 1 | import * as Camera from "./engine/camera"; 2 | import { Key } from "./engine/input"; 3 | import * as Platform from "./platform"; 4 | import * as Backdrop from "./backdrop"; 5 | 6 | let pauseNextIteration = false; 7 | 8 | export let init = (gl, width, height) => { 9 | Backdrop.init(gl); 10 | Platform.init(gl, width, height); 11 | }; 12 | 13 | export let loadLevel = Platform.loadLevel; 14 | 15 | // used to pause through UI 16 | export let pauseScene = () => (pauseNextIteration = true); 17 | 18 | export let update = () => { 19 | if (Key.esc || pauseNextIteration) { 20 | pauseNextIteration = false; 21 | return 2; 22 | } 23 | if (Key.mouse.down) { 24 | Camera.move(Key.mouse.x, Key.mouse.y); 25 | Key.mouse.x = Key.mouse.y = 0; 26 | } 27 | return Platform.update(); 28 | }; 29 | 30 | export let draw = (gl, time) => { 31 | Backdrop.draw(gl, time); 32 | Platform.draw(gl); 33 | }; 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fourfold 2 | 3 | A game created for the [js13kGames](https://js13kgames.com/) competition. Play it [here](https://js13kgames.com/entries/fourfold). 4 | 5 | Making of/analysis: [saud.wtf/blog/fourfold](https://saud.wtf/blog/fourfold/) 6 | 7 | ## Build 8 | 9 | ### Install dependencies: 10 | ```sh 11 | yarn install 12 | ``` 13 | 14 | ### Development build: 15 | (no code minification, creates sourcemap) 16 | ```sh 17 | yarn devbuild 18 | ``` 19 | 20 | ### Production build: 21 | (minified, no sourcemap) 22 | ```sh 23 | yarn build 24 | ``` 25 | Generated files will be stored in the `app` directory. 26 | 27 | ### Compress built files: 28 | (requires the [zip](https://github.com/LuaDist/zip) command) 29 | ```sh 30 | yarn bundle 31 | ``` 32 | 33 | ## Stuff used 34 | 35 | [esbuild](https://github.com/evanw/esbuild) for minification & bundling. 36 | 37 | [ZzFXM](https://github.com/keithclark/ZzFXM) for sound. 38 | 39 | ## License 40 | MIT 41 | -------------------------------------------------------------------------------- /src/palette.js: -------------------------------------------------------------------------------- 1 | import { floor } from "./engine/math"; 2 | 3 | export let lightDirection = [-0.5, 0.8, -1.0]; 4 | 5 | export let tileColor = { 6 | // start tile, behaves similar to "b" 7 | x: [0.061, 0.581, 0.821], 8 | // basic non-interactive tile 9 | b: [0.939, 0.131, 0.125], 10 | // destination tile 11 | c: [0.502, 0.847, 0.412], 12 | // one step tile 13 | d: [0.91, 0.82, 0.14], 14 | }; 15 | 16 | export let playerColor = [0.44, 0.525, 0.627]; 17 | export let playerGlowColor = [0.933, 0.894, 0.882]; 18 | 19 | export let backdropBase = [0.583, 0.644, 0.752]; 20 | 21 | export let setBackdropColor = (val) => { 22 | backdropBase = val; 23 | }; 24 | 25 | // source: https://stackoverflow.com/a/5624139/7683374 26 | export let rgbToHex = ([r, g, b]) => 27 | "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); 28 | export let denormalize = ([r, g, b]) => [ 29 | floor(r * 255), 30 | floor(g * 255), 31 | floor(b * 255), 32 | ]; 33 | -------------------------------------------------------------------------------- /src/ui/main.js: -------------------------------------------------------------------------------- 1 | import { 2 | CENTERED_FADEIN, 3 | textElement, 4 | buttonElement, 5 | setUIElement, 6 | } from "./utils"; 7 | import { showLevelsMenu } from "./levels"; 8 | import { showCustomLevelsMenu } from "./custom"; 9 | import { enableTouchButton } from "./hud"; 10 | 11 | export let showMainMenu = () => { 12 | let wrapper = document.createElement("div"); 13 | wrapper.id = "mainmenu"; 14 | wrapper.className = CENTERED_FADEIN; 15 | let fadeOut = () => (wrapper.className = "centered zoomin"); 16 | 17 | // Title text 18 | let title = textElement("FOURFOLD", "title"); 19 | 20 | // Main start button 21 | let startButton = buttonElement("START", "button", () => { 22 | fadeOut(); 23 | setTimeout(showLevelsMenu, 500); 24 | }); 25 | 26 | // Custom levels button 27 | let customLevelsButton = buttonElement("CUSTOM LEVELS", "button", () => { 28 | fadeOut(); 29 | setTimeout(showCustomLevelsMenu, 500); 30 | }); 31 | 32 | wrapper.append(title, startButton, customLevelsButton, enableTouchButton); 33 | setUIElement(wrapper); 34 | }; 35 | -------------------------------------------------------------------------------- /src/sound/sounds.js: -------------------------------------------------------------------------------- 1 | import { zzfxG, zzfxP, zzfxM } from "./zzfx"; 2 | 3 | let buttonClick = zzfxG(...[, 0, , 0.05, , 0.5]); 4 | 5 | let piano = [0.5, 0, 190, , 0.08, 0.5, 3]; 6 | 7 | let moveset = [ 8 | [ 9 | zzfxM(...[[piano], [[[, , 5, , , , ,]]], [0]]), 10 | zzfxM(...[[piano], [[[, , 6, , , , ,]]], [0]]), 11 | zzfxM(...[[piano], [[[, , 8, , , , ,]]], [0]]), 12 | zzfxM(...[[piano], [[[, , 17, , , , ,]]], [0]]), 13 | ], 14 | [ 15 | zzfxM(...[[piano], [[[, , 3, , , , ,]]], [0]]), 16 | zzfxM(...[[piano], [[[, , 5, , , , ,]]], [0]]), 17 | zzfxM(...[[piano], [[[, , 6, , , , ,]]], [0]]), 18 | zzfxM(...[[piano], [[[, , 15, , , , ,]]], [0]]), 19 | ], 20 | ]; 21 | let activeSet = 0; 22 | 23 | let completed = zzfxM(...[[piano], [[[, , 2, 3, 7, 20, , , , ,]]], [0]]); 24 | 25 | export let buttonClickSound = () => { 26 | zzfxP(buttonClick); 27 | }; 28 | 29 | export let moveSound = (jump) => { 30 | zzfxP(...moveset[activeSet][jump]); 31 | jump === 3 && (activeSet = Number(!activeSet)); 32 | }; 33 | 34 | export let finishedSound = () => { 35 | zzfxP(...completed); 36 | }; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mohammed Saud 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 | 23 | -------------------------------------------------------------------------------- /src/shapes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create vertex array for visible part of a cube 3 | * Contains 18 vertices(6 triangles) 4 | * 5 | * @param width {number} 6 | * @param height {number} 7 | */ 8 | export let partialCube = (width, height) => [ 9 | // top face 10 | width, 0, 0, // top right 11 | 0, 0, 0, // top left 12 | 0, width, 0, // bottom left 13 | width, 0, 0, // top right 14 | 0, width, 0, // bottom left 15 | width, width, 0, // bottom right 16 | 17 | // right face 18 | width, width, 0, // top right 19 | 0, width, 0, // top left 20 | 0, width, height, // bottom left 21 | width, width, 0, // top right 22 | 0, width, height, // bottom left 23 | width, width, height, // bottom right 24 | 25 | // left face 26 | 0, width, 0, // top right 27 | 0, 0, 0, // top left 28 | 0, 0, height, // bottom left 29 | 0, width, 0, // top right 30 | 0, 0, height, // bottom left 31 | 0, width, height, // bottom right 32 | ]; 33 | 34 | export let partialCubeNormal = () => [ 35 | // top face 36 | 0, 0, -1, 37 | 0, 0, -1, 38 | 0, 0, -1, 39 | 0, 0, -1, 40 | 0, 0, -1, 41 | 0, 0, -1, 42 | // right face 43 | 0, 1, 0, 44 | 0, 1, 0, 45 | 0, 1, 0, 46 | 0, 1, 0, 47 | 0, 1, 0, 48 | 0, 1, 0, 49 | // left face 50 | -1, 0, 0, 51 | -1, 0, 0, 52 | -1, 0, 0, 53 | -1, 0, 0, 54 | -1, 0, 0, 55 | -1, 0, 0, 56 | ]; 57 | -------------------------------------------------------------------------------- /src/ui/utils.js: -------------------------------------------------------------------------------- 1 | import { checkMonetization } from "../game"; 2 | import { buttonClickSound } from "../sound/sounds"; 3 | 4 | export let base = document.getElementById("ui"), 5 | hud = document.getElementById("hud"), 6 | // helpful constants 7 | CENTERED_FADEIN = "centered fadein", 8 | CENTERED_FADEOUT = "centered fadeout", 9 | VISIBLE = "visible", 10 | HIDDEN = "hidden", 11 | EMPTY = "", 12 | storateString = "js13k-20-fourfold", 13 | TIMEOUT_INTERVAL = 500; 14 | 15 | export let create = (type, id, text) => { 16 | let ele = document.createElement(type); 17 | ele.id = id; 18 | ele.innerText = text || ""; 19 | return ele; 20 | }; 21 | 22 | export let textElement = (text, id) => create("div", id, text); 23 | 24 | export let buttonElement = (text, id, callback) => { 25 | let ele = textElement(text, id); 26 | ele.onclick = (e) => { 27 | buttonClickSound(); 28 | callback(e); 29 | }; 30 | return ele; 31 | }; 32 | 33 | export let setUIElement = (ele) => { 34 | base.style.visibility = VISIBLE; 35 | hud.style.visibility = HIDDEN; 36 | base.innerHTML = EMPTY; 37 | base.append(ele); 38 | checkMonetization(); 39 | }; 40 | 41 | export let getLevelsCompleted = () => 42 | Number(localStorage.getItem(storateString)) || 0; 43 | 44 | export let setLevelsCompleted = (level) => 45 | localStorage.setItem(storateString, level); 46 | -------------------------------------------------------------------------------- /src/ui/levels.js: -------------------------------------------------------------------------------- 1 | import { gameState } from "../game"; 2 | import { loadLevel } from "../scene"; 3 | import { levels } from "../levels"; 4 | import { 5 | CENTERED_FADEIN, 6 | CENTERED_FADEOUT, 7 | buttonElement, 8 | textElement, 9 | setUIElement, 10 | TIMEOUT_INTERVAL, 11 | getLevelsCompleted, 12 | } from "./utils"; 13 | import { showMainMenu } from "./main"; 14 | import { startGame } from "./start"; 15 | 16 | export let showLevelsMenu = () => { 17 | let wrapper = document.createElement("div"); 18 | wrapper.id = "mainmenu"; 19 | wrapper.className = CENTERED_FADEIN; 20 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 21 | 22 | let backButton = buttonElement("←", "backbutton", () => { 23 | fadeOut(); 24 | setTimeout(showMainMenu, TIMEOUT_INTERVAL); 25 | }); 26 | 27 | let title = textElement("SELECT LEVEL", "subtitle"); 28 | 29 | let levelsGrid = document.createElement("div"); 30 | levelsGrid.id = "levelsgrid"; 31 | 32 | let completed = getLevelsCompleted(); 33 | levelsGrid.append(backButton); 34 | levelsGrid.append( 35 | ...levels.map((level, i) => { 36 | let startFunc = () => { 37 | gameState.level = i + 1; 38 | fadeOut(); 39 | loadLevel(level); 40 | setTimeout(startGame, TIMEOUT_INTERVAL, false); 41 | }; 42 | let ele = buttonElement( 43 | i + 1, 44 | "level", 45 | i <= completed ? startFunc : () => {} 46 | ); 47 | ele.className = 48 | i < completed ? "completed" : i > completed ? "blocked" : "active"; 49 | return ele; 50 | }) 51 | ); 52 | 53 | wrapper.append(backButton, title, levelsGrid); 54 | setUIElement(wrapper); 55 | }; 56 | -------------------------------------------------------------------------------- /src/game.js: -------------------------------------------------------------------------------- 1 | import * as Camera from "./engine/camera"; 2 | import * as Scene from "./scene"; 3 | import * as Editor from "./editor.js"; 4 | 5 | /** 6 | * Global game state 7 | * 0 = not running 8 | * 1 = playing 9 | * 2 = paused 10 | * 3 = in level editor 11 | * 12 | * @property {0 | 1 | 2 | 3} state - game state 13 | * @property {number} level currently played level 14 | * @property {boolean} hasCoil coil subscription state 15 | */ 16 | export let gameState = { 17 | hasCoil: false, 18 | editedLevel: false, 19 | touchControls: false, 20 | state: 0, 21 | level: 0, 22 | }; 23 | 24 | export let checkMonetization = () => { 25 | gameState.hasCoil || 26 | (document.monetization && 27 | document.monetization.addEventListener("monetizationstart", function () { 28 | gameState.hasCoil = true; 29 | })); 30 | }; 31 | 32 | let gl; 33 | 34 | let clearScreen = () => gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 35 | 36 | export let initGame = (canvas, width, height) => { 37 | gl = canvas.getContext("webgl"); 38 | 39 | Camera.update(width, height); 40 | Scene.init(gl, width, height); 41 | Editor.init(gl, width, height); 42 | 43 | gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); 44 | gl.clearColor(0.1, 0.1, 0.1, 1.0); 45 | gl.clearDepth(1.0); 46 | gl.enable(gl.DEPTH_TEST); 47 | gl.enable(gl.CULL_FACE); 48 | gl.enable(gl.LEQUAL); 49 | }; 50 | 51 | export let gameLoop = (delta, time) => { 52 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 53 | gameState.state = Scene.update(delta); 54 | 55 | clearScreen(); 56 | Scene.draw(gl, time); 57 | 58 | return gameState.state === 1; 59 | }; 60 | 61 | export let editorLoop = (delta, time) => { 62 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 63 | gameState.state = Editor.update(delta); 64 | 65 | clearScreen(); 66 | Editor.draw(gl, time); 67 | 68 | return gameState.state === 3; 69 | }; 70 | -------------------------------------------------------------------------------- /src/ui/custom.js: -------------------------------------------------------------------------------- 1 | import { loadLevel } from "../scene"; 2 | import * as Editor from "../editor.js"; 3 | import { 4 | CENTERED_FADEIN, 5 | CENTERED_FADEOUT, 6 | textElement, 7 | buttonElement, 8 | TIMEOUT_INTERVAL, 9 | setUIElement, 10 | } from "./utils"; 11 | import { showMainMenu } from "./main"; 12 | import { startGame } from "./start"; 13 | import { gameState } from "../game"; 14 | 15 | let showLevelInput = () => { 16 | let wrapper = document.createElement("div"); 17 | wrapper.id = "mainmenu"; 18 | wrapper.className = CENTERED_FADEIN; 19 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 20 | 21 | let backButton = buttonElement("←", "backbutton", () => { 22 | fadeOut(); 23 | setTimeout(showCustomLevelsMenu, TIMEOUT_INTERVAL); 24 | }); 25 | 26 | let title = textElement("ENTER LEVEL DATA", "subtitle"); 27 | 28 | let levelText = document.createElement("input"); 29 | 30 | let startButton = buttonElement("START", "button", () => { 31 | if (loadLevel(levelText.value)) { 32 | fadeOut(); 33 | gameState.level = 0; 34 | setTimeout(startGame, TIMEOUT_INTERVAL, false); 35 | } else { 36 | levelText.className = "wrong"; 37 | } 38 | }); 39 | 40 | wrapper.append(backButton, title, levelText, startButton); 41 | setUIElement(wrapper); 42 | }; 43 | 44 | export let showCustomLevelsMenu = () => { 45 | let wrapper = document.createElement("div"); 46 | wrapper.id = "mainmenu"; 47 | wrapper.className = CENTERED_FADEIN; 48 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 49 | 50 | let title = textElement("CUSTOM LEVELS", "subtitle"); 51 | 52 | let backButton = buttonElement("←", "backbutton", () => { 53 | fadeOut(); 54 | setTimeout(showMainMenu, TIMEOUT_INTERVAL); 55 | }); 56 | 57 | let customLevelButton = buttonElement("LOAD LEVEL", "button", () => { 58 | fadeOut(); 59 | setTimeout(showLevelInput, TIMEOUT_INTERVAL); 60 | }); 61 | let editorButton = buttonElement("CREATE LEVEL", "button", () => { 62 | fadeOut(); 63 | Editor.reset(); 64 | setTimeout(startGame, TIMEOUT_INTERVAL, true); 65 | }); 66 | wrapper.append(backButton, title, customLevelButton, editorButton); 67 | setUIElement(wrapper); 68 | }; 69 | -------------------------------------------------------------------------------- /src/sound/zzfx.js: -------------------------------------------------------------------------------- 1 | // Source: https://github.com/keithclark/ZzFXM 2 | // zzfx() - the universal entry point -- returns a AudioBufferSourceNode 3 | export let zzfx=(...t)=>zzfxP(zzfxG(...t)) 4 | 5 | // zzfxP() - the sound player -- returns a AudioBufferSourceNode 6 | export let zzfxP=(...t)=>{let e=zzfxX.createBufferSource(),f=zzfxX.createBuffer(t.length,t[0].length,zzfxR);t.map((d,i)=>f.getChannelData(i).set(d)),e.buffer=f,e.connect(zzfxX.destination),e.start();return e} 7 | 8 | // zzfxG() - the sound generator -- returns an array of sample data 9 | export let zzfxG=(q=1,k=.05,c=220,e=0,t=0,u=.1,r=0,F=1,v=0,z=0,w=0,A=0,l=0,B=0,x=0,G=0,d=0,y=1,m=0,C=0)=>{let b=2*Math.PI,H=v*=500*b/zzfxR**2,I=(0a?0:(aA&&(c+=w,D+=w,n=0),!l||++J%l||(c=D,v=H,n=n||1);return Z} 10 | 11 | // zzfxV - global volume 12 | export let zzfxV=.3 13 | 14 | // zzfxR - global sample rate 15 | export let zzfxR=44100 16 | 17 | // zzfxX - the common audio context 18 | export let zzfxX=new(top.AudioContext||webkitAudioContext); 19 | 20 | //! ZzFXM (v2.0.3) | (C) Keith Clark | MIT | https://github.com/keithclark/ZzFXM 21 | export let zzfxM=(n,f,t,e=125)=>{let l,o,z,r,g,h,x,a,u,c,d,i,m,p,G,M=0,R=[],b=[],j=[],k=0,q=0,s=1,v={},w=zzfxR/e*60>>2;for(;s;k++)R=[s=a=d=m=0],t.map((e,d)=>{for(x=f[e][k]||[0,0,0],s|=!!f[e][k],G=m+(f[e][0].length-2-!a)*w,p=d==t.length-1,o=2,r=m;ow-99&&u?i+=(i<1)/99:0)h=(1-i)*R[M++]/2||0,b[r]=(b[r]||0)-h*q+h,j[r]=(j[r++]||0)+h*q+h;g&&(i=g%1,q=x[1]||0,(g|=0)&&(R=v[[c=x[M=0]||0,g]]=v[[c,g]]||(l=[...n[c]],l[2]*=2**((g-12)/12),g>0?zzfxG(...l):[])))}m=G});return[b,j]} 22 | -------------------------------------------------------------------------------- /src/engine/gl.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile shaders into program 3 | * 4 | * @param {string} vshader vertex shader 5 | * @param {string} fshader fragment shader 6 | */ 7 | export let compile = (gl, vshader, fshader) => { 8 | let vs = gl.createShader(gl.VERTEX_SHADER); 9 | gl.shaderSource(vs, vshader); 10 | gl.compileShader(vs); 11 | 12 | let fs = gl.createShader(gl.FRAGMENT_SHADER); 13 | gl.shaderSource(fs, fshader); 14 | gl.compileShader(fs); 15 | 16 | let program = gl.createProgram(); 17 | gl.attachShader(program, vs); 18 | gl.attachShader(program, fs); 19 | gl.linkProgram(program); 20 | 21 | // TODO: only for testing, disable before release 22 | //console.log("vertex shader:", gl.getShaderInfoLog(vs) || "OK"); 23 | //console.log("fragment shader:", gl.getShaderInfoLog(fs) || "OK"); 24 | //console.log("program:", gl.getProgramInfoLog(program) || "OK"); 25 | 26 | return { 27 | program, 28 | use: () => gl.useProgram(program), 29 | attribs: { 30 | vertex: gl.getAttribLocation(program, "aVertexPosition"), 31 | normal: gl.getAttribLocation(program, "aNormal"), 32 | }, 33 | uniforms: { 34 | modelMatrix: gl.getUniformLocation(program, "uModelViewMatrix"), 35 | parentTransform: gl.getUniformLocation(program, "uParentTransform"), 36 | projectionMatrix: gl.getUniformLocation(program, "uProjectionMatrix"), 37 | lightDir: gl.getUniformLocation(program, "uLightDir"), 38 | jump: gl.getUniformLocation(program, "uJump"), 39 | color: gl.getUniformLocation(program, "uColor"), 40 | color2: gl.getUniformLocation(program, "uColor2"), 41 | backdrop: gl.getUniformLocation(program, "uBackdrop"), 42 | time: gl.getUniformLocation(program, "uTime"), 43 | aspect: gl.getUniformLocation(program, "uAspect"), 44 | }, 45 | }; 46 | }; 47 | 48 | /** 49 | * Create a buffer 50 | * 51 | * @param gl WebGL context 52 | * @param type buffer type 53 | * @param data buffer data 54 | */ 55 | export let makeBuffer = (gl, type, data) => { 56 | let buffer = gl.createBuffer(); 57 | gl.bindBuffer(type, buffer); 58 | gl.bufferData(type, new Float32Array(data), gl.STATIC_DRAW); 59 | return { 60 | bind: (size, pointer) => { 61 | gl.bindBuffer(type, buffer); 62 | gl.vertexAttribPointer(pointer, size, gl.FLOAT, false, 0, 0); 63 | gl.enableVertexAttribArray(pointer); 64 | }, 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/ui/hud.js: -------------------------------------------------------------------------------- 1 | import { keyCodes } from "../engine/input"; 2 | import { pauseScene } from "../scene"; 3 | import * as Editor from "../editor.js"; 4 | import { gameState } from "../game"; 5 | import { 6 | hud, 7 | HIDDEN, 8 | VISIBLE, 9 | EMPTY, 10 | buttonElement, 11 | textElement, 12 | } from "./utils"; 13 | 14 | // Button to enable touch controls 15 | export let enableTouchButton = buttonElement( 16 | "☐ TOUCH CONTROLS", 17 | "button", 18 | (e) => { 19 | if (gameState.touchControls) { 20 | gameState.touchControls = false; 21 | e.target.innerText = "☐ TOUCH CONTROLS"; 22 | } else { 23 | gameState.touchControls = true; 24 | e.target.innerText = "☑ TOUCH CONTROLS"; 25 | } 26 | } 27 | ); 28 | // touch controls 29 | let touchButton = (id, key, content = "") => { 30 | let ele = textElement(content, id); 31 | ele.onpointerdown = (e) => { 32 | onkeydown({ keyCode: keyCodes[key] }); 33 | e.preventDefault(); 34 | }; 35 | ele.onpointerup = (e) => { 36 | onkeyup({ keyCode: keyCodes[key] }); 37 | e.preventDefault(); 38 | }; 39 | return ele; 40 | }; 41 | 42 | let button0 = touchButton("b0", "up"); 43 | let button1 = touchButton("b1", "right"); 44 | let button2 = touchButton("b2", "left"); 45 | let button3 = touchButton("b3", "down"); 46 | let button4 = touchButton("button", "space", "⮤⮧"); 47 | button4.className = "b4"; 48 | 49 | let touchControlButtons = (isEditor) => { 50 | let controls = document.createElement("div"); 51 | controls.id = "controls"; 52 | controls.append(button0, button1, button2, button3); 53 | isEditor && controls.append(button4); 54 | return controls; 55 | }; 56 | 57 | export let showHUD = (isEditor) => { 58 | hud.style.visibility = VISIBLE; 59 | fadeOut = () => (hud.style.visibility = HIDDEN); 60 | 61 | let pauseButton = buttonElement("II", "pausebutton", () => { 62 | fadeOut(); 63 | isEditor ? Editor.pauseEditor() : pauseScene(); 64 | // no need to pause loop here because it's done by startGame 65 | }); 66 | 67 | let upperHud = document.createElement("div"); 68 | upperHud.id = "hudmenu"; 69 | upperHud.append(pauseButton); 70 | 71 | if (isEditor) { 72 | let editComplete = buttonElement("✓", "pausebutton", () => { 73 | fadeOut(); 74 | Editor.pauseEditor(); 75 | gameState.editedLevel = true; 76 | }); 77 | 78 | let resetButton = buttonElement("↺", "pausebutton", Editor.reset); 79 | 80 | upperHud.append(editComplete, resetButton); 81 | } 82 | 83 | hud.innerHTML = EMPTY; 84 | hud.append(upperHud); 85 | 86 | if (gameState.touchControls) { 87 | hud.append(touchControlButtons(isEditor)); 88 | } 89 | }; 90 | -------------------------------------------------------------------------------- /src/engine/input.js: -------------------------------------------------------------------------------- 1 | export let Key = { 2 | up: false, 3 | right: false, 4 | down: false, 5 | left: false, 6 | esc: false, 7 | space: false, 8 | mouse: { down: false, x: 0, y: 0 }, 9 | }; 10 | 11 | export let keyCodes = { 12 | left: 37, 13 | right: 39, 14 | up: 38, 15 | down: 40, 16 | space: 32, 17 | }; 18 | 19 | // Keydown listener 20 | onkeydown = (e) => { 21 | let keycode = e.keyCode; 22 | let preventMovement = () => e.preventDefault && e.preventDefault(); 23 | // Up (up / W / Z) 24 | if (keycode == 38 || keycode == 90 || keycode == 87) { 25 | Key.up = true; 26 | preventMovement(); 27 | } 28 | 29 | // Right (right / D) 30 | if (keycode == 39 || keycode == 68) { 31 | Key.right = true; 32 | preventMovement(); 33 | } 34 | 35 | // Down (down / S) 36 | if (keycode == 40 || keycode == 83) { 37 | Key.down = true; 38 | preventMovement(); 39 | } 40 | 41 | // Left (left / A / Q) 42 | if (keycode == 37 || keycode == 65 || keycode == 81) { 43 | Key.left = true; 44 | preventMovement(); 45 | } 46 | 47 | // Esc 48 | if (keycode == 27) { 49 | Key.esc = true; 50 | preventMovement(); 51 | } 52 | 53 | // space 54 | if (keycode == 32) { 55 | Key.space = true; 56 | preventMovement(); 57 | } 58 | }; 59 | 60 | // Keyup listener 61 | onkeyup = (e) => { 62 | let keycode = e.keyCode; 63 | // Up 64 | if (keycode == 38 || keycode == 90 || keycode == 87) { 65 | Key.up = false; 66 | } 67 | 68 | // Right 69 | if (keycode == 39 || keycode == 68) { 70 | Key.right = false; 71 | } 72 | 73 | // Down 74 | if (keycode == 40 || keycode == 83) { 75 | Key.down = false; 76 | } 77 | 78 | // Left 79 | if (keycode == 37 || keycode == 65 || keycode == 81) { 80 | Key.left = false; 81 | } 82 | 83 | // Esc 84 | if (keycode == 27) { 85 | Key.esc = false; 86 | } 87 | 88 | // space 89 | if (keycode == 32) { 90 | Key.space = false; 91 | } 92 | }; 93 | 94 | // For handling mouse drag and touch events 95 | let canvas = document.getElementById("app"), 96 | x = 0, 97 | y = 0; 98 | canvas.onpointerdown = (e) => { 99 | Key.mouse.down = true; 100 | x = e.offsetX; 101 | y = e.offsetY; 102 | }; 103 | canvas.onpointerup = () => { 104 | Key.mouse.down = false; 105 | Key.mouse.x = Key.mouse.y = 0; 106 | }; 107 | canvas.onpointermove = (e) => { 108 | Key.mouse.x = Key.mouse.y = 0; 109 | if (Key.mouse.down) { 110 | Key.mouse.x = e.offsetX - x; 111 | Key.mouse.y = e.offsetY - y; 112 | x = e.offsetX; 113 | y = e.offsetY; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /src/ui/start.js: -------------------------------------------------------------------------------- 1 | import { startLoop } from "../engine/loop"; 2 | import { editorLoop, gameLoop, gameState } from "../game"; 3 | import { finishedSound } from "../sound/sounds"; 4 | import { 5 | base, 6 | HIDDEN, 7 | CENTERED_FADEIN, 8 | CENTERED_FADEOUT, 9 | buttonElement, 10 | textElement, 11 | setUIElement, 12 | setLevelsCompleted, 13 | getLevelsCompleted, 14 | create, 15 | } from "./utils"; 16 | import { showMainMenu } from "./main"; 17 | import { showEditCompleteMenu, showPauseMenu } from "./pause"; 18 | import { showHUD } from "./hud"; 19 | import { showCustomLevelsMenu } from "./custom"; 20 | import { showLevelsMenu } from "./levels"; 21 | 22 | let levelCompleted = (isCustom) => { 23 | let wrapper = document.createElement("div"); 24 | wrapper.id = "pausemenu"; 25 | wrapper.className = CENTERED_FADEIN; 26 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 27 | 28 | let title = textElement("COMPLETED!", "subtitle"); 29 | 30 | let continueButton = buttonElement("CONTINUE", "button", () => { 31 | fadeOut(); 32 | setTimeout(isCustom ? showCustomLevelsMenu : showLevelsMenu, 500); 33 | }); 34 | 35 | let mainMenuButton = buttonElement("MAIN MENU", "button", () => { 36 | fadeOut(); 37 | setTimeout(showMainMenu, 500); 38 | }); 39 | 40 | finishedSound(); 41 | isCustom || 42 | (getLevelsCompleted() < gameState.level && 43 | setLevelsCompleted(gameState.level)); 44 | // show special message if all levels completed 45 | if (gameState.level === 20) { 46 | let endMessage = create( 47 | "p", 48 | "", 49 | "You have completed all the levels. Now go create your own!" 50 | ); 51 | let customLevelsButton = buttonElement("CUSTOM LEVELS", "button", () => { 52 | fadeOut(); 53 | setTimeout(showCustomLevelsMenu, 500); 54 | }); 55 | wrapper.append(title, endMessage, customLevelsButton, mainMenuButton); 56 | } else { 57 | wrapper.append(title, continueButton, mainMenuButton); 58 | } 59 | setUIElement(wrapper); 60 | }; 61 | 62 | export let startGame = (isEditor) => { 63 | // set ui elements to null to hide scrollbar 64 | setUIElement(); 65 | base.style.visibility = HIDDEN; 66 | showHUD(isEditor); 67 | 68 | startLoop(isEditor ? editorLoop : gameLoop, () => { 69 | if (!gameState.state) { 70 | // TODO: splash screen for level completion 71 | levelCompleted(gameState.level < 1); 72 | } else { 73 | // paused through esc button 74 | if (isEditor && gameState.editedLevel) { 75 | gameState.editedLevel = false; 76 | showEditCompleteMenu(); 77 | } else { 78 | showPauseMenu(() => startGame(isEditor)); 79 | } 80 | } 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/ui/pause.js: -------------------------------------------------------------------------------- 1 | import { gameState } from "../game.js"; 2 | import * as Editor from "../editor.js"; 3 | import { changeBackdrop } from "../backdrop.js"; 4 | import { 5 | setUIElement, 6 | CENTERED_FADEIN, 7 | CENTERED_FADEOUT, 8 | textElement, 9 | buttonElement, 10 | create, 11 | } from "./utils"; 12 | import { showMainMenu } from "./main"; 13 | import { enableTouchButton } from "./hud"; 14 | 15 | export let showPauseMenu = (onResume) => { 16 | let wrapper = create("div", "pausemenu"); 17 | wrapper.className = CENTERED_FADEIN; 18 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 19 | 20 | let themeList = create("div", "dropdown"); 21 | let themes = gameState.hasCoil 22 | ? ["morning", "night", "retrowave", "abstract"] 23 | : ["morning", "night"]; 24 | themeList.append( 25 | ...themes.map((val, i) => { 26 | let btn = buttonElement(val, "dropitem", () => changeBackdrop(i)); 27 | return btn; 28 | }) 29 | ); 30 | 31 | let themeButton = create("div", "button", "THEME ▾"); 32 | themeButton.className = "themebtn"; 33 | themeButton.append(themeList); 34 | // hack to simulate a dropdown menu 35 | wrapper.onclick = (e) => 36 | (themeList.style.visibility = e.target.matches(".themebtn") 37 | ? "visible" 38 | : "hidden"); 39 | 40 | let title = textElement("PAUSED", "title"); 41 | 42 | let resumeButton = buttonElement("RESUME", "button", () => { 43 | fadeOut(); 44 | onResume(); 45 | }); 46 | 47 | let mainMenuButton = buttonElement("MAIN MENU", "button", () => { 48 | fadeOut(); 49 | setTimeout(showMainMenu, 500); 50 | }); 51 | 52 | wrapper.append( 53 | title, 54 | resumeButton, 55 | themeButton, 56 | enableTouchButton, 57 | mainMenuButton 58 | ); 59 | setUIElement(wrapper); 60 | }; 61 | 62 | export let showEditCompleteMenu = () => { 63 | let wrapper = document.createElement("div"); 64 | wrapper.id = "pausemenu"; 65 | wrapper.className = CENTERED_FADEIN; 66 | let fadeOut = () => (wrapper.className = CENTERED_FADEOUT); 67 | 68 | let title = textElement("LEVEL CREATED", "subtitle"); 69 | 70 | let levelText = document.createElement("input"); 71 | levelText.value = Editor.getEncodedLevel(); 72 | levelText.readOnly = true; 73 | 74 | let tweetButton = create("a", "button", "SHARE 🐦"); 75 | tweetButton.href = 76 | "https://twitter.com/intent/tweet?url=https%3A%2F%2Fjs13kgames.com%2Fentries%2Ffourfold&text=Checkout%20this%20custom%20level%20I%20made%20in%20Fourfold%3A%20%22" + 77 | levelText.value.replace(":", "%3A") + 78 | "%22"; 79 | tweetButton.target = "_blank"; 80 | 81 | let mainMenuButton = buttonElement("MAIN MENU", "button", () => { 82 | fadeOut(); 83 | setTimeout(showMainMenu, 500); 84 | }); 85 | 86 | // give it some time to attach to DOM 87 | setTimeout(() => { 88 | levelText.focus(); 89 | levelText.select(); 90 | }, 50); 91 | 92 | wrapper.append(title, levelText, tweetButton, mainMenuButton); 93 | setUIElement(wrapper); 94 | }; 95 | -------------------------------------------------------------------------------- /static/app.css: -------------------------------------------------------------------------------- 1 | html{height:100%;} 2 | body{ 3 | font:100 2.5rem Trebuchet,sans-serif; 4 | background:radial-gradient(#89a,#454); 5 | margin:0; 6 | color:#333; 7 | overflow-x:hidden; 8 | } 9 | p{font-size:1.5rem;font-weight:150} 10 | .centered{ 11 | position:absolute; 12 | left:0; 13 | right:0; 14 | margin:auto; 15 | } 16 | #app{ 17 | position:absolute; 18 | } 19 | #hud{ 20 | position:absolute; 21 | visibility:hidden; 22 | } 23 | #hudmenu{ 24 | display:flex; 25 | } 26 | #ui{ 27 | display:flex; 28 | height:100vh; 29 | flex-direction:column; 30 | background:inherit; 31 | } 32 | #mainmenu,#pausemenu{ 33 | display:flex; 34 | max-width:10em; 35 | top:10vh; 36 | flex-direction:column; 37 | } 38 | #title{ 39 | margin:3.5rem auto; 40 | transform:scaleY(1.3); 41 | } 42 | #subtitle{ 43 | margin:3.5rem auto; 44 | transform:scaleY(1.1); 45 | } 46 | #text{ 47 | color:#ccc; 48 | background:#222; 49 | margin:2rem auto; 50 | } 51 | input{ 52 | font:inherit; 53 | width:100%; 54 | background:#ccc; 55 | padding:0; 56 | border:0; 57 | border-bottom:2px solid #222; 58 | } 59 | .wrong{ 60 | border:3px solid red; 61 | } 62 | #button,#backbutton,#pausebutton,#dropitem{ 63 | font-size:2rem; 64 | background:#ccc; 65 | cursor:pointer; 66 | margin:1rem auto; 67 | padding:10px; 68 | transition:0.3s; 69 | border:0; 70 | } 71 | #button:hover,#level:hover,#backbutton:hover,#pausebutton:hover{ 72 | background:#333; 73 | color:#6fc; 74 | transform:scale(1.1); 75 | } 76 | #button:active,#level:active,#backbutton:active{ 77 | color:#ecf; 78 | transform:scale(0.9); 79 | } 80 | #backbutton{ 81 | padding:0 20px; 82 | margin-left:0; 83 | } 84 | #pausebutton{ 85 | width:50px; 86 | text-align:center; 87 | } 88 | #levelsgrid{ 89 | max-width:13em; 90 | display:grid; 91 | grid-gap:10px; 92 | grid-template-columns:repeat(4,1fr); 93 | } 94 | #dropdown{ 95 | position:absolute; 96 | visibility:hidden; 97 | background:#ccc; 98 | z-index:2; 99 | } 100 | #dropitem{ 101 | background:#ccc; 102 | color:#222; 103 | transition:0s; 104 | } 105 | #dropitem:hover{background:#aaa} 106 | #level{ 107 | cursor:pointer; 108 | text-align:center; 109 | padding:50px; 110 | align-self:center; 111 | transition:0.3s; 112 | } 113 | .active{background:#ccc} 114 | .completed{background:#acd} 115 | .blocked{background:#999} 116 | #controls{ 117 | position:fixed; 118 | display:grid; 119 | bottom:0; 120 | left:50%; 121 | transform:translateX(-50%); 122 | grid-template-columns:1fr 1fr; 123 | } 124 | #b0,#b1,#b2,#b3{ 125 | width:0; 126 | height:0; 127 | margin:5px; 128 | border:30px solid #333; 129 | border-radius:9px; 130 | cursor:pointer; 131 | } 132 | #b0:active,#b1:active,#b2:active,#b3:active{transform:scale(0.9)} 133 | 134 | #b0,#b1{border-bottom:30px solid #aaa} 135 | #b2,#b3{border-top :30px solid #aaa} 136 | #b0,#b2{border-right :30px solid #aaa} 137 | #b1,#b3{border-left :30px solid #aaa} 138 | .b4{position:absolute;left:10rem;} 139 | 140 | @media(max-width:540px){ 141 | #levelsgrid{grid-template-columns:repeat(2,1fr)} 142 | body{font:100 2rem Trebuchet,sans-serif} 143 | #subtitle{margin:3rem auto} 144 | #button,#backbutton,#pausebutton{font-size:1.5rem} 145 | } 146 | 147 | .fadein{animation:0.7s ease fadein} 148 | @keyframes fadein{from{opacity:0}to{opacity:1}} 149 | .fadeout{animation:0.9s ease fadeout} 150 | @keyframes fadeout{to{opacity:0}} 151 | .zoomin{animation:1s ease zoomin} 152 | @keyframes zoomin{to{transform:scale(3);opacity:0}} 153 | -------------------------------------------------------------------------------- /src/selector.js: -------------------------------------------------------------------------------- 1 | import { identity, transform } from "./engine/math"; 2 | import { Key } from "./engine/input"; 3 | import * as Camera from "./engine/camera"; 4 | import { compile, makeBuffer } from "./engine/gl"; 5 | import { TILEWIDTH, TILEGAP } from "./tile"; 6 | 7 | export let X = 0, 8 | Y = 0, 9 | modelView = identity(), 10 | state = 0, 11 | SPEED = 0.1, 12 | pos = [0, 0], 13 | nextPos = [0, 0], 14 | program, 15 | buffer; 16 | 17 | export let reset = () => { 18 | state = 0; 19 | modelView = identity(); 20 | platforms = []; 21 | X = 0; 22 | Y = 0; 23 | pos = [0, 0]; 24 | nextPos = [0, 0]; 25 | }; 26 | 27 | export let setContextPos = (x, y) => { 28 | X = x; 29 | Y = y; 30 | pos = [x, y]; 31 | nextPos = [x, y]; 32 | }; 33 | 34 | // Vertex shader 35 | let vshader = `attribute vec4 aVertexPosition; 36 | 37 | uniform mat4 uModelViewMatrix; 38 | uniform mat4 uParentTransform; 39 | uniform mat4 uProjectionMatrix; 40 | 41 | void main() { 42 | gl_Position = uProjectionMatrix * uParentTransform * uModelViewMatrix * aVertexPosition; 43 | }`; 44 | 45 | // Fragment shader 46 | let fshader = `precision mediump float; 47 | void main() { 48 | gl_FragColor = vec4(1.,1.,1.,1.); 49 | }`; 50 | 51 | export let init = (gl) => { 52 | program = compile(gl, vshader, fshader); 53 | 54 | buffer = makeBuffer(gl, gl.ARRAY_BUFFER, [ 55 | 0, 56 | 0, 57 | TILEWIDTH, 58 | 0, 59 | TILEWIDTH, 60 | TILEWIDTH, 61 | 0, 62 | TILEWIDTH, 63 | ]); 64 | }; 65 | 66 | export let update = () => { 67 | switch (state) { 68 | // wait for input 69 | case 0: 70 | if (Key.up || Key.down || Key.left || Key.right) { 71 | state = 1; 72 | if (Key.up) { 73 | nextPos[1]--; 74 | } else if (Key.down) { 75 | nextPos[1]++; 76 | } else if (Key.left) { 77 | nextPos[0]--; 78 | } else if (Key.right) { 79 | nextPos[0]++; 80 | } 81 | } 82 | break; 83 | 84 | // move selector to next position 85 | case 1: 86 | let xDisp = 0, 87 | yDisp = 0; 88 | 89 | // move in X 90 | if (pos[0] !== nextPos[0]) { 91 | pos[0] < nextPos[0] 92 | ? ((pos[0] += SPEED), (xDisp += SPEED)) 93 | : ((pos[0] -= SPEED), (xDisp -= SPEED)); 94 | 95 | // too small values don't matter 96 | if (Math.abs(pos[0] - nextPos[0]) < 0.01) { 97 | pos[0] = nextPos[0]; 98 | } 99 | // move in Y 100 | } else if (pos[1] !== nextPos[1]) { 101 | pos[1] < nextPos[1] 102 | ? ((pos[1] += SPEED), (yDisp += SPEED)) 103 | : ((pos[1] -= SPEED), (yDisp -= SPEED)); 104 | 105 | if (Math.abs(pos[1] - nextPos[1]) < 0.01) { 106 | pos[1] = nextPos[1]; 107 | } 108 | // moved, we can proceed 109 | } else { 110 | X = nextPos[0]; 111 | Y = nextPos[1]; 112 | state = 0; 113 | } 114 | transform(modelView, { 115 | x: xDisp * (TILEWIDTH + TILEGAP), 116 | y: yDisp * (TILEWIDTH + TILEGAP), 117 | }); 118 | } 119 | }; 120 | 121 | export let draw = (gl, parentTransform) => { 122 | program.use(); 123 | buffer.bind(2, program.attribs.vertex); 124 | gl.uniformMatrix4fv(program.uniforms.parentTransform, false, parentTransform); 125 | gl.uniformMatrix4fv( 126 | program.uniforms.projectionMatrix, 127 | false, 128 | Camera.projectionMatrix 129 | ); 130 | gl.uniformMatrix4fv(program.uniforms.modelMatrix, false, modelView); 131 | gl.drawArrays(gl.LINE_LOOP, 0, 4); 132 | }; 133 | -------------------------------------------------------------------------------- /src/platform.js: -------------------------------------------------------------------------------- 1 | import { transform, identity, degToRad } from "./engine/math"; 2 | import * as Camera from "./engine/camera"; 3 | import * as Player from "./player"; 4 | import * as Tile from "./tile"; 5 | 6 | let gridWidth, 7 | parentTransform = identity(), 8 | state = 0, 9 | levelFinished = false, 10 | initPos = [0, 0], 11 | gameWidth, 12 | gameHeight; 13 | export let platforms = []; 14 | 15 | export let init = (gl, width, height) => { 16 | gameWidth = width; 17 | gameHeight = height; 18 | Tile.init(gl); 19 | Player.init(gl); 20 | }; 21 | 22 | export let update = (delta) => { 23 | switch (state) { 24 | // Enter scene 25 | case 0: 26 | platforms.forEach((tile, i) => { 27 | Tile.setEnterPos(tile, i); 28 | }); 29 | // Check if the last tile has been moved to position 30 | if (platforms[0].zpos === 0) { 31 | state = 1; 32 | Player.initPos(initPos[0], initPos[1]); 33 | } 34 | return 1; 35 | // Play scene 36 | case 1: 37 | Player.update(delta); 38 | // Check bounds 39 | if ( 40 | Player.X < 0 || 41 | Player.Y < 0 || 42 | Player.X >= gridWidth || 43 | Player.Y >= gridWidth 44 | ) { 45 | Player.fall(); 46 | state = 2; 47 | } else { 48 | let tileInfo = Tile.checkTile( 49 | platforms[Player.X + gridWidth * Player.Y] 50 | ); 51 | if (tileInfo === 2) { 52 | Player.fall(); 53 | state = 2; 54 | } 55 | if (tileInfo === 1) { 56 | Player.fall(); 57 | state = 2; 58 | levelFinished = true; 59 | } 60 | } 61 | platforms.forEach((tile, i) => 62 | Tile.updateState(tile, i === Player.X + gridWidth * Player.Y) 63 | ); 64 | return 1; 65 | // Wait for player move animation 66 | case 2: 67 | if (Player.update()) { 68 | Player.initPos(initPos[0], initPos[1]); 69 | platforms.forEach((tile) => Tile.resetState(tile)); 70 | state = 1; 71 | if (levelFinished) { 72 | return 0; 73 | } 74 | } 75 | return 1; 76 | } 77 | }; 78 | 79 | export let draw = (gl) => { 80 | Tile.loadTileBuffer(gl, parentTransform); 81 | platforms.forEach((tile) => Tile.drawTile(gl, tile)); 82 | 83 | Player.load(gl, parentTransform); 84 | Player.draw(gl); 85 | }; 86 | 87 | export let loadLevel = (levelData) => { 88 | Camera.update(gameWidth, gameHeight); 89 | // to temporarily hide player until tiles show up 90 | Player.initPos(-10, -10); 91 | [gridWidth, tiles] = levelData.split(":"); 92 | gridWidth = Number(gridWidth); 93 | state = 0; 94 | levelFinished = false; 95 | if (!levelData || !gridWidth || gridWidth < 0) { 96 | return 0; 97 | } 98 | 99 | { 100 | parentTransform = transform(identity(), { 101 | x: gameWidth / 3, 102 | y: (gameHeight * 2) / 3, 103 | rx: -degToRad(30), 104 | rz: -0.5, 105 | }); 106 | } 107 | // Fastest array initialization https://stackoverflow.com/q/1295584/7683374 108 | (parsedTiles = []).length = gridWidth * gridWidth; 109 | // init with tile gap 110 | parsedTiles.fill("a"); 111 | // First, create array of decoded tile chars 112 | for (let i = 0, curPos = 0; i < tiles.length; i++) { 113 | let val = tiles.charAt(i); 114 | 115 | if (Number(val)) { 116 | let count = Number(val); 117 | parsedTiles.fill(tiles.charAt(++i), curPos, curPos + count); 118 | curPos += count; 119 | } else { 120 | parsedTiles[curPos++] = val; 121 | } 122 | } 123 | // Next, fill platform wih position & detail metadata for each tile 124 | (platforms = []).length = 0; 125 | for (let y = 0; y < gridWidth; y++) { 126 | for (let x = 0; x < gridWidth; x++) { 127 | let type = parsedTiles[x + y * gridWidth]; 128 | 129 | // TODO is this efficient? Only done once, so does it matter? 130 | if (type === "x") { 131 | initPos[0] = x; 132 | initPos[1] = y; 133 | } 134 | platforms.push(Tile.createTileData(x, y, type)); 135 | } 136 | } 137 | return 1; 138 | }; 139 | -------------------------------------------------------------------------------- /src/tile.js: -------------------------------------------------------------------------------- 1 | import { compile, makeBuffer } from "./engine/gl"; 2 | import * as Camera from "./engine/camera"; 3 | import { identity, transform } from "./engine/math"; 4 | import { partialCube, partialCubeNormal } from "./shapes"; 5 | import { lightDirection, tileColor, backdropBase } from "./palette"; 6 | import { gameState } from "./game"; 7 | 8 | export let TILEGAP = 10, 9 | TILEWIDTH = 50, 10 | TILEHEIGHT = 900, 11 | STARTZPOS = 1000; 12 | 13 | let program; 14 | 15 | // Vertex shader 16 | // TODO hardcoded tile height 17 | let vshader = `attribute vec4 aVertexPosition; 18 | attribute vec3 aNormal; 19 | 20 | uniform mat4 uModelViewMatrix; 21 | uniform mat4 uParentTransform; 22 | uniform mat4 uProjectionMatrix; 23 | 24 | varying vec3 vNormal; 25 | varying float vDepth; 26 | 27 | void main() { 28 | gl_Position = uProjectionMatrix * uParentTransform * uModelViewMatrix * aVertexPosition; 29 | vNormal = aNormal; 30 | vDepth = aVertexPosition.z/900.; 31 | }`; 32 | 33 | // Fragment shader 34 | let fshader = `precision mediump float; 35 | uniform vec3 uLightDir; 36 | uniform vec3 uColor; 37 | uniform vec3 uBackdrop; 38 | 39 | varying vec3 vNormal; 40 | varying float vDepth; 41 | 42 | void main() { 43 | float light = dot(normalize(vNormal), uLightDir); 44 | gl_FragColor = mix(vec4(uColor,1.), vec4(uBackdrop, 1.), vDepth); 45 | gl_FragColor.xyz *= light; 46 | }`; 47 | 48 | export let init = (gl) => { 49 | program = compile(gl, vshader, fshader); 50 | 51 | vertexBuffer = makeBuffer( 52 | gl, 53 | gl.ARRAY_BUFFER, 54 | partialCube(TILEWIDTH, TILEHEIGHT) 55 | ); 56 | normalBuffer = makeBuffer(gl, gl.ARRAY_BUFFER, partialCubeNormal()); 57 | }; 58 | 59 | export let createTileData = (x, y, type, startAtZero = false) => { 60 | switch (type) { 61 | case "a": 62 | return { 63 | type, 64 | zpos: STARTZPOS, 65 | modelView: identity(), 66 | }; 67 | case "x": 68 | // gap 69 | case "a": 70 | // basic non-interactive tile 71 | case "b": 72 | // destination tile 73 | case "c": 74 | return { 75 | type, 76 | zpos: STARTZPOS, 77 | modelView: transform(identity(), { 78 | x: x * TILEWIDTH + TILEGAP * x, 79 | y: y * TILEWIDTH + TILEGAP * y, 80 | z: startAtZero ? 0 : STARTZPOS, 81 | }), 82 | }; 83 | case "d": 84 | return { 85 | type, 86 | steps: 1, 87 | oSteps: 1, 88 | zpos: STARTZPOS, 89 | modelView: transform(identity(), { 90 | x: x * TILEWIDTH + TILEGAP * x, 91 | y: y * TILEWIDTH + TILEGAP * y, 92 | z: startAtZero ? 0 : STARTZPOS, 93 | }), 94 | }; 95 | default: 96 | return null; 97 | } 98 | }; 99 | 100 | export let getTilesList = () => { 101 | return gameState.hasCoil ? ["x", "a", "b", "c", "d"] : ["x", "a", "b", "c"]; 102 | }; 103 | 104 | export let setEnterPos = (tile, index) => { 105 | if (!tile || tile.zpos === 0) return; 106 | switch (tile.type) { 107 | // start tile, behaves similar to "b" 108 | case "x": 109 | // gap 110 | case "a": 111 | // basic non-interactive tile 112 | case "b": 113 | // destination tile 114 | case "c": 115 | // one step tile 116 | case "d": 117 | // cleanup if tile moved too far away, else move it up gradually 118 | let displace = tile.zpos < 0 ? -tile.zpos : -7 - (index + 1); 119 | transform(tile.modelView, { z: displace }); 120 | tile.zpos += displace; 121 | } 122 | }; 123 | 124 | export let checkTile = (tile) => { 125 | // treat null as empty gap 126 | if (!tile) { 127 | return 2; 128 | } 129 | switch (tile.type) { 130 | // Win 131 | case "c": 132 | return 1; 133 | // Fall 134 | case "a": 135 | return 2; 136 | case "d": 137 | // stepping, fall down next 138 | if (tile.steps) { 139 | tile.steps--; 140 | } else if (tile.zpos > 0) { 141 | return 2; 142 | } 143 | // Continue 144 | default: 145 | return 0; 146 | } 147 | }; 148 | 149 | export let updateState = (tile, isPlayerOn) => { 150 | if (!tile) { 151 | return; 152 | } 153 | if (tile.type === "d") { 154 | // available steps of tile exhausted, fall 155 | if (!isPlayerOn && !tile.steps) { 156 | tile.zpos += 20; 157 | transform(tile.modelView, { z: 20 }); 158 | // reached the end. can ignore tile from now 159 | if (tile.zpos >= STARTZPOS) { 160 | tile.type = "a"; 161 | } 162 | } 163 | } 164 | }; 165 | 166 | export let resetState = (tile) => { 167 | if (!tile) { 168 | return; 169 | } 170 | // this is a step tile 171 | if (tile.oSteps) { 172 | tile.type = "d"; 173 | tile.steps = tile.oSteps; 174 | transform(tile.modelView, { z: -tile.zpos }); 175 | tile.zpos = 0; 176 | } 177 | }; 178 | 179 | export let loadTileBuffer = (gl, parentTransform) => { 180 | program.use(); 181 | 182 | vertexBuffer.bind(3, program.attribs.vertex); 183 | normalBuffer.bind(3, program.attribs.normal); 184 | 185 | gl.uniformMatrix4fv(program.uniforms.parentTransform, false, parentTransform); 186 | gl.uniformMatrix4fv( 187 | program.uniforms.projectionMatrix, 188 | false, 189 | Camera.projectionMatrix 190 | ); 191 | gl.uniform3fv(program.uniforms.lightDir, lightDirection); 192 | gl.uniform3fv(program.uniforms.backdrop, backdropBase); 193 | }; 194 | 195 | export let drawTile = (gl, tile) => { 196 | if (!tile || tile.type === "a") { 197 | return; 198 | } 199 | gl.uniform3fv(program.uniforms.color, tileColor[tile.type]); 200 | gl.uniformMatrix4fv(program.uniforms.modelMatrix, false, tile.modelView); 201 | gl.drawArrays(gl.TRIANGLES, 0, 18); 202 | }; 203 | -------------------------------------------------------------------------------- /src/backdrop.js: -------------------------------------------------------------------------------- 1 | import { compile, makeBuffer } from "./engine/gl"; 2 | import { setBackdropColor } from "./palette"; 3 | 4 | let vertexBuffer, program1, program2, program3, program4, activeProgram; 5 | 6 | let vshader = `attribute vec2 aVertexPosition; 7 | uniform float uAspect; 8 | varying vec2 vST; 9 | 10 | void main() { 11 | gl_Position = vec4(aVertexPosition.xy, .9, 1.); 12 | vST = (aVertexPosition+1.)/2.; 13 | vST.x = vST.x * uAspect; 14 | }`; 15 | 16 | let fsImports = `precision mediump float; 17 | varying vec2 vST; 18 | uniform float uTime;`; 19 | 20 | // hash function from https://www.shadertoy.com/view/4djSRW 21 | // given a value between 0 and 1 22 | // returns a value between 0 and 1 that *appears* kind of random 23 | let hashFunc = ` 24 | float hash(vec2 co){ 25 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); 26 | } 27 | float newrand(vec2 p, float num) { 28 | return fract(hash(p) * num); 29 | }`; 30 | 31 | let circleSDF = ` 32 | float circle(vec2 p, float r) { 33 | return distance(vST,p)-r; 34 | }`; 35 | 36 | let scale = ` 37 | mat2 scale(vec2 _s) { 38 | return mat2(_s.x,0.,0.,_s.y); 39 | }`; 40 | 41 | let rotate = ` 42 | mat2 rot(float r) { 43 | return mat2(cos(r), -sin(r), sin(r), cos(r)); 44 | }`; 45 | 46 | // Don't want shader comments inside build, so I'll just put them here 47 | // 48 | // light: glowing light at top right 49 | // mnt1, mnt2: layers of mountains, made by simple sine waves, slightly shifted, 50 | // dividing them by 3 & 2 determines lightness shade 51 | // back: adding all layers of effects, and mixing with supplied color uniform 52 | let fshader1 = 53 | fsImports + 54 | circleSDF + 55 | hashFunc + 56 | ` 57 | void main() { 58 | vec3 col = vec3(.58,.64,.75); 59 | float light = 1.-circle(vec2(.8,.9),.1); 60 | float mnt1 = step(vST.y, abs( sin(vST.x * 8. + uTime) ) / 6. + .3) / 3.; 61 | float mnt2 = step(vST.y, abs( cos(vST.x * 6. - uTime) ) / 4. + .1) / 2.; 62 | vec3 back = mix(vec3(light+max(mnt1,mnt2)), col, .7); 63 | gl_FragColor = vec4(vec3(back), 1.0); 64 | }`; 65 | 66 | // starfield shader 67 | // uv: fraction grid coordinate 68 | // id: unique value of each 'container' 69 | // stars: x, y is halved out after taking random, to avoid border clipping 70 | // (can probably be solved with neighbour iteration) 71 | let fshader2 = 72 | fsImports + 73 | hashFunc + 74 | scale + 75 | rotate + 76 | ` 77 | float star(vec2 p, float r) { 78 | return smoothstep(.01, 1., r/length(p)); 79 | } 80 | vec3 layer(vec2 u) { 81 | vec2 grid = fract(u) - .5; 82 | vec2 id = floor(u) / 8.; 83 | float size = newrand(id, 1.)/40.; 84 | float stars = star(grid + vec2((newrand(id, 10.)-.5)/2., (newrand(id, 20.)-.5)/2.), size); 85 | vec3 col = sin(vec3(newrand(id, 35.),newrand(id, 66.),newrand(id,93.))*2.+uTime) / 2. + .5; 86 | col *= vec3(.8,.7,.9); 87 | col += .5; 88 | return stars * col; 89 | } 90 | void main() { 91 | vec3 col = vec3(0.); 92 | for(float i=0.; i<1.; i+=.2) { 93 | float depth = fract(uTime/16. + i); 94 | float scale = mix(20., .5, depth); 95 | col += layer(vST * rot(uTime/8.) * scale + i*32.) * depth; 96 | } 97 | gl_FragColor = vec4(col, 1.0); 98 | }`; 99 | 100 | // st: translated(and bent?) vertex 101 | // lines: drawing grid using step() 102 | // grid: b/w map of drawn lines 103 | // fade: top half 104 | // light: top right circle 105 | let fshader3 = 106 | fsImports + 107 | scale + 108 | rotate + 109 | ` 110 | void main() { 111 | vec3 col = vec3(0.8, .06, .9); 112 | vec3 sun = vec3(.3, .13, .2); 113 | vec2 st = vST * scale(vec2(vST.y + .5, -vST.y*2.)) - vec2(-uTime/8., uTime/8.); 114 | st += vec2(.5,.0); 115 | st *= rot(.01); 116 | vec2 lines = step(vec2(.05), fract(st*16.)); 117 | float grid = 1.-(lines.x*lines.y); 118 | float faded = min(grid, 0.6-vST.y); 119 | float light = .2/length(vST - vec2(.95)); 120 | vec3 final = mix(vec3(faded), col, .3); 121 | final = mix(final, sun, mix(vec3(light), vec3(1.,0.,0.),vST.y)); 122 | gl_FragColor = vec4(final, 1.0); 123 | }`; 124 | 125 | let fshader4 = 126 | fsImports + 127 | hashFunc + 128 | scale + 129 | rotate + 130 | ` 131 | void main() { 132 | float y = vST.y * 8.; 133 | float x = vST.x * 8. + uTime; 134 | float curve = y + vST.x - sin(x)/2. + sin(uTime); 135 | float lines = step(.01, fract(curve)); 136 | vec2 id = vec2(floor(curve)); 137 | vec3 col = vec3(newrand(id, 83.), newrand(id, 23.), newrand(id, 65.)); 138 | col = mix(col, vec3(curve, .9-vST.x, vST.y), .4); 139 | gl_FragColor = vec4(vec3(lines-col), 1.0); 140 | }`; 141 | 142 | export let init = (gl) => { 143 | program1 = compile(gl, vshader, fshader1); 144 | program2 = compile(gl, vshader, fshader2); 145 | program3 = compile(gl, vshader, fshader3); 146 | program4 = compile(gl, vshader, fshader4); 147 | activeProgram = program1; 148 | vertexBuffer = makeBuffer(gl, gl.ARRAY_BUFFER, [-1, 1, -1, -1, 1, 1, 1, -1]); 149 | }; 150 | 151 | export let changeBackdrop = (opt) => { 152 | switch (opt) { 153 | case 0: 154 | setBackdropColor([0.58, 0.64, 0.75]); 155 | activeProgram = program1; 156 | break; 157 | case 1: 158 | setBackdropColor([0, 0, 0]); 159 | activeProgram = program2; 160 | break; 161 | case 2: 162 | setBackdropColor([1, 0.16, 1]); 163 | activeProgram = program3; 164 | break; 165 | case 3: 166 | setBackdropColor([1, 1, 1]); 167 | activeProgram = program4; 168 | break; 169 | } 170 | }; 171 | 172 | export let draw = (gl, time) => { 173 | activeProgram.use(); 174 | 175 | vertexBuffer.bind(2, activeProgram.attribs.vertex); 176 | gl.uniform1f(activeProgram.uniforms.time, time / 10000); 177 | gl.uniform1f(activeProgram.uniforms.aspect, 1280 / 720); 178 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 179 | }; 180 | -------------------------------------------------------------------------------- /src/player.js: -------------------------------------------------------------------------------- 1 | import { compile, makeBuffer } from "./engine/gl"; 2 | import { identity, transform } from "./engine/math"; 3 | import { Key } from "./engine/input"; 4 | import * as Camera from "./engine/camera"; 5 | import { TILEWIDTH, TILEGAP } from "./tile"; 6 | import { partialCube, partialCubeNormal } from "./shapes"; 7 | import { lightDirection, playerColor } from "./palette"; 8 | import { moveSound } from "./sound/sounds"; 9 | 10 | // Vertex shader 11 | // TODO: hardcoded height value 12 | let vshader = `attribute vec4 aVertexPosition; 13 | attribute vec3 aNormal; 14 | 15 | uniform mat4 uModelViewMatrix; 16 | uniform mat4 uParentTransform; 17 | uniform mat4 uProjectionMatrix; 18 | 19 | varying vec3 vNormal; 20 | varying float vHeight; 21 | 22 | void main() { 23 | gl_Position = uProjectionMatrix * uParentTransform * uModelViewMatrix * aVertexPosition; 24 | vNormal = aNormal; 25 | vHeight = (1.-aVertexPosition.z/40.0); 26 | }`; 27 | 28 | // Fragment shader 29 | let fshader = `precision mediump float; 30 | uniform vec3 uLightDir; 31 | uniform vec3 uColor; 32 | uniform float uJump; 33 | 34 | varying vec3 vNormal; 35 | varying float vHeight; 36 | 37 | void main() { 38 | float light = dot(normalize(vNormal), uLightDir); 39 | float glow = step(vHeight, uJump); 40 | gl_FragColor = mix(vec4(0.5, 0.5, 0.5, 1.0), vec4(0.9,1.,0.9,1.), glow); 41 | gl_FragColor.xyz *= light; 42 | }`; 43 | 44 | let buffer, 45 | SIZE = 10, 46 | HEIGHT = 40, 47 | SPEED = 0.09, 48 | targetPos = [0, 0], 49 | activePos = [0, 0], 50 | Z = 0, 51 | jump = 0, 52 | modelView = identity(); 53 | 54 | export let X = 0, 55 | Y = 0, 56 | state = 0; 57 | 58 | export let init = (gl) => { 59 | program = compile(gl, vshader, fshader); 60 | 61 | buffer = makeBuffer(gl, gl.ARRAY_BUFFER, partialCube(SIZE, HEIGHT)); 62 | normalBuffer = makeBuffer(gl, gl.ARRAY_BUFFER, partialCubeNormal()); 63 | }; 64 | 65 | export let update = (_delta) => { 66 | // state = 0 : initialize and run start animation 67 | // state = 1 : no key pressed, stationary, taking input 68 | // state = 2 : input registered, moving, not taking input 69 | // state = 3 : end animation 70 | // state = 4 : dummy state to wait for end of key press 71 | switch (state) { 72 | case 0: 73 | transform(modelView, { 74 | z: -1, 75 | }); 76 | Z--; 77 | if (Z <= -HEIGHT) { 78 | state = 4; 79 | Z = 0; 80 | } 81 | break; 82 | 83 | case 1: 84 | if (Key.up || Key.down || Key.left || Key.right) { 85 | let stride = 1; 86 | moveSound(jump); 87 | if (jump++ > 2) { 88 | stride = 2; 89 | jump = 0; 90 | } 91 | state = 2; 92 | if (Key.up) { 93 | targetPos[1] -= stride; 94 | } else if (Key.down) { 95 | targetPos[1] += stride; 96 | } else if (Key.left) { 97 | targetPos[0] -= stride; 98 | } else if (Key.right) { 99 | targetPos[0] += stride; 100 | } 101 | } 102 | break; 103 | 104 | case 2: 105 | let xDisp = 0, 106 | yDisp = 0; 107 | 108 | if (activePos[0] !== targetPos[0]) { 109 | activePos[0] < targetPos[0] 110 | ? ((activePos[0] += SPEED), (xDisp += SPEED)) 111 | : ((activePos[0] -= SPEED), (xDisp -= SPEED)); 112 | 113 | // sanity check, ignore very small distances 114 | if (Math.abs(activePos[0] - targetPos[0]) < 0.05) { 115 | activePos[0] = targetPos[0]; 116 | } 117 | } else if (activePos[1] !== targetPos[1]) { 118 | activePos[1] < targetPos[1] 119 | ? ((activePos[1] += SPEED), (yDisp += SPEED)) 120 | : ((activePos[1] -= SPEED), (yDisp -= SPEED)); 121 | 122 | // sanity check, ignore very small distances 123 | if (Math.abs(activePos[1] - targetPos[1]) < 0.05) { 124 | activePos[1] = targetPos[1]; 125 | } 126 | } else { 127 | X = targetPos[0]; 128 | Y = targetPos[1]; 129 | state = 4; 130 | } 131 | 132 | transform(modelView, { 133 | x: xDisp * (TILEWIDTH + TILEGAP), 134 | y: yDisp * (TILEWIDTH + TILEGAP), 135 | }); 136 | break; 137 | 138 | case 3: 139 | transform(modelView, { 140 | z: Z++, 141 | }); 142 | // done falling 143 | if (Z > 50) { 144 | Z = 0; 145 | return 1; 146 | } 147 | break; 148 | 149 | case 4: 150 | if (!(Key.up || Key.down || Key.left || Key.right)) { 151 | state = 1; 152 | } 153 | } 154 | }; 155 | 156 | export let fall = () => ((state = 3), (jump = 0)); 157 | 158 | export let load = (gl, parentTransform) => { 159 | program.use(); 160 | 161 | buffer.bind(3, program.attribs.vertex); 162 | normalBuffer.bind(3, program.attribs.normal); 163 | 164 | gl.uniformMatrix4fv(program.uniforms.parentTransform, false, parentTransform); 165 | gl.uniformMatrix4fv( 166 | program.uniforms.projectionMatrix, 167 | false, 168 | Camera.projectionMatrix 169 | ); 170 | gl.uniform3fv(program.uniforms.lightDir, lightDirection); 171 | gl.uniform3fv(program.uniforms.color, playerColor); 172 | gl.uniform1f(program.uniforms.jump, jump / 3); 173 | }; 174 | 175 | export let draw = (gl) => { 176 | gl.uniformMatrix4fv(program.uniforms.modelMatrix, false, modelView); 177 | gl.drawArrays(gl.TRIANGLES, 0, 18); 178 | }; 179 | 180 | export let initPos = (x, y) => { 181 | X = targetPos[0] = activePos[0] = x; 182 | Y = targetPos[1] = activePos[1] = y; 183 | state = Z = jump = 0; 184 | 185 | // Looks scary but really isn't 186 | modelView = transform(identity(), { 187 | x: X * TILEWIDTH + TILEGAP * X + TILEWIDTH / 2 - SIZE / 2, 188 | y: Y * TILEWIDTH + TILEGAP * Y + TILEWIDTH / 2 - SIZE / 2, 189 | z: 0, 190 | }); 191 | }; 192 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | import * as Backdrop from "./backdrop"; 2 | import { identity, transform, degToRad } from "./engine/math"; 3 | import { Key } from "./engine/input"; 4 | import * as Camera from "./engine/camera"; 5 | import * as Selector from "./selector"; 6 | import { createTileData, loadTileBuffer, drawTile, getTilesList } from "./tile"; 7 | 8 | let gridWidth = 1, 9 | state = 0, 10 | parentTransform = identity(), 11 | platforms = [], 12 | tileData = [], 13 | sceneWidth, 14 | sceneHeight, 15 | pauseNextIteration = false, 16 | // These two keep track of the number of negative(outside of grid) steps we've taken 17 | // So we could update tileData grid properly 18 | // parts of code using these are very delicate and subtle! 19 | negativeX = 1, 20 | negativeY = 1; 21 | 22 | export let reset = () => { 23 | state = 0; 24 | Camera.update(sceneWidth, sceneHeight); 25 | parentTransform = transform(identity(), { 26 | x: sceneWidth / 3, 27 | y: (sceneHeight * 2) / 3, 28 | rx: -degToRad(30), 29 | rz: -0.5, 30 | }); 31 | negativeX = negativeY = gridWidth = 1; 32 | Selector.reset(); 33 | platforms = [["x"]]; 34 | tileData = [[createTileData(0, 0, "x", true)]]; 35 | }; 36 | 37 | export let init = (gl, width, height) => { 38 | sceneWidth = width; 39 | sceneHeight = height; 40 | // no need to init tiles, already done in game.js 41 | Selector.init(gl); 42 | }; 43 | 44 | // used to pause through UI 45 | export let pauseEditor = () => (pauseNextIteration = true); 46 | 47 | export let getEncodedLevel = () => { 48 | let encodedRows = []; 49 | for (let y = 0; y < gridWidth; y++) { 50 | let row = platforms[y], 51 | current = row[0], 52 | fullRow = "", 53 | count = 0; 54 | for (let x = 1; x <= gridWidth; x++) { 55 | let val = row[x]; 56 | if (val !== current) { 57 | fullRow += count < 1 ? current : count + 1 + current; 58 | current = val; 59 | count = 0; 60 | } else { 61 | // TODO: current level parser does not recognize double digits, 62 | // so kept them small for now 63 | if (count === 8) { 64 | fullRow += count + 1 + current; 65 | current = val; 66 | count = 0; 67 | } else { 68 | count++; 69 | } 70 | } 71 | } 72 | encodedRows.push(fullRow); 73 | } 74 | // Uncommented because this need to affect gridWidth value 75 | // TODO: this still doesn't trim empty columns 76 | // trim empty rows from bottom 77 | //let rowLength = encodedRows.length - 1; 78 | //while (encodedRows[rowLength--] === gridWidth + "a") { 79 | // encodedRows.pop(); 80 | //} 81 | 82 | // trim empty rows from top 83 | rowLength = 0; 84 | while (encodedRows[rowLength++] === gridWidth + "a") { 85 | encodedRows.shift(); 86 | } 87 | // trim empty rows from bottom 88 | rowLength = gridWidth - 1; 89 | while (encodedRows[rowLength--] === gridWidth + "a") { 90 | encodedRows.pop(); 91 | } 92 | return gridWidth + ":" + encodedRows.reduce((acc, val) => acc + val, ""); 93 | }; 94 | 95 | export let update = () => { 96 | if (Key.esc || pauseNextIteration) { 97 | pauseNextIteration = false; 98 | return 2; 99 | } 100 | if (Key.mouse.down) { 101 | Camera.move(Key.mouse.x, Key.mouse.y); 102 | Key.mouse.x = Key.mouse.y = 0; 103 | } 104 | switch (state) { 105 | case 0: 106 | Selector.update(); 107 | 108 | // The hairy bit 109 | if ( 110 | Selector.X < 0 || 111 | Selector.Y < 0 || 112 | Selector.X >= gridWidth || 113 | Selector.Y >= gridWidth 114 | ) { 115 | if (Selector.X < 0) { 116 | platforms.map((_, i) => { 117 | platforms[i].unshift("a"); 118 | tileData[i].unshift( 119 | createTileData(-negativeX, i + 1 - negativeY, "a", true) 120 | ); 121 | }); 122 | negativeX++; 123 | platforms.push(newPlatformRow()); 124 | tileData.push(newTileRowDown()); 125 | Selector.setContextPos(0, Selector.Y); 126 | } 127 | if (Selector.X >= gridWidth) { 128 | platforms.map((_, i) => { 129 | platforms[i].push("a"); 130 | tileData[i].push( 131 | createTileData( 132 | gridWidth + 1 - negativeX, 133 | i + 1 - negativeY, 134 | "a", 135 | true 136 | ) 137 | ); 138 | }); 139 | platforms.push(newPlatformRow()); 140 | tileData.push(newTileRowDown()); 141 | Selector.setContextPos(gridWidth, Selector.Y); 142 | } 143 | if (Selector.Y < 0) { 144 | platforms.map((_, i) => { 145 | platforms[i].push("a"); 146 | tileData[i].push( 147 | createTileData( 148 | gridWidth + 1 - negativeX, 149 | i + 1 - negativeY, 150 | "a", 151 | true 152 | ) 153 | ); 154 | }); 155 | platforms.unshift(newPlatformRow()); 156 | tileData.unshift(newTileRowUp()); 157 | Selector.setContextPos(Selector.X, 0); 158 | negativeY++; 159 | } 160 | if (Selector.Y >= gridWidth) { 161 | platforms.map((_, i) => { 162 | platforms[i].push("a"); 163 | tileData[i].push( 164 | createTileData( 165 | gridWidth + 1 - negativeX, 166 | i + 1 - negativeY, 167 | "a", 168 | true 169 | ) 170 | ); 171 | }); 172 | platforms.push(newPlatformRow()); 173 | tileData.push(newTileRowDown()); 174 | Selector.setContextPos(Selector.X, gridWidth); 175 | } 176 | 177 | // grid dimensions are now increased 178 | gridWidth++; 179 | } 180 | // cycle tile type if space pressed and move to temp state 181 | if (Key.space) { 182 | let curTile = platforms[Selector.Y][Selector.X]; 183 | let nextTile = getNextTile(curTile); 184 | platforms[Selector.Y][Selector.X] = nextTile; 185 | tileData[Selector.Y][Selector.X] = createTileData( 186 | Selector.X + 1 - negativeX, 187 | Selector.Y + 1 - negativeY, 188 | nextTile, 189 | true 190 | ); 191 | state = 1; 192 | } 193 | break; 194 | // temporary state to wait untill keys are unpressed 195 | case 1: 196 | if (!Key.space) { 197 | state = 0; 198 | } 199 | } 200 | return 3; 201 | }; 202 | 203 | export let draw = (gl, time) => { 204 | Selector.draw(gl, parentTransform); 205 | loadTileBuffer(gl, parentTransform); 206 | tileData.forEach((row) => row.forEach((tile) => drawTile(gl, tile))); 207 | 208 | Backdrop.draw(gl, time); 209 | }; 210 | 211 | let getNextTile = (curTile) => { 212 | let tiles = getTilesList(); 213 | let pos = tiles.indexOf(curTile); 214 | return pos === tiles.length - 1 ? tiles[0] : tiles[pos + 1]; 215 | }; 216 | 217 | let newPlatformRow = () => { 218 | let newRow = []; 219 | newRow.length = gridWidth + 1; 220 | newRow.fill("a"); 221 | return newRow; 222 | }; 223 | let newTileRowUp = () => { 224 | let newRow = []; 225 | for (i = 0; i < gridWidth + 1; i++) { 226 | newRow[i] = createTileData(i + 1 - negativeX, -negativeY, "a", true); 227 | } 228 | return newRow; 229 | }; 230 | let newTileRowDown = () => { 231 | let newRow = []; 232 | for (i = 0; i < gridWidth + 1; i++) { 233 | newRow[i] = createTileData( 234 | i + 1 - negativeX, 235 | gridWidth + 1 - negativeY, 236 | "a", 237 | true 238 | ); 239 | } 240 | return newRow; 241 | }; 242 | -------------------------------------------------------------------------------- /src/engine/math.js: -------------------------------------------------------------------------------- 1 | export let floor = Math.floor; 2 | export let radToDeg = r => r * 180 / Math.PI; 3 | export let degToRad = d => d * Math.PI / 180; 4 | 5 | export let cross = (a, b) => new Float32Array( 6 | [a[1] * b[2] - a[2] * b[1], 7 | a[2] * b[0] - a[0] * b[2], 8 | a[0] * b[1] - a[1] * b[0]]); 9 | 10 | export let subtractVectors = (a, b) => new Float32Array([a[0] - b[0], a[1] - b[1], a[2] - b[2]]); 11 | 12 | export let normalize = (v) => { 13 | let length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); 14 | // make sure we don't divide by 0. 15 | if (length > 0.00001) { 16 | return [v[0] / length, v[1] / length, v[2] / length]; 17 | } else { 18 | return [0, 0, 0]; 19 | } 20 | }; 21 | 22 | /** 23 | * Returns a 4x4 identity matrix 24 | */ 25 | export let identity = () => { 26 | return new Float32Array([ 27 | 1, 0, 0, 0, 28 | 0, 1, 0, 0, 29 | 0, 0, 1, 0, 30 | 0, 0, 0, 1 31 | ]); 32 | }; 33 | 34 | // Note: This matrix flips the Y axis so that 0 is at the top. 35 | export let orthographic = (left, right, bottom, top, near, far) => new Float32Array([ 36 | 2 / (right - left), 0, 0, 0, 37 | 0, 2 / (top - bottom), 0, 0, 38 | 0, 0, 2 / (near - far), 0, 39 | 40 | (left + right) / (left - right), 41 | (bottom + top) / (bottom - top), 42 | (near + far) / (near - far), 43 | 1, 44 | ]); 45 | 46 | // Create a perspective matrix 47 | export let perspective = (FOVInRadians, aspect, near, far) => { 48 | let f = Math.tan(Math.PI * 0.5 - 0.5 * FOVInRadians); 49 | let rangeInv = 1.0 / (near - far); 50 | 51 | return new Float32Array([ 52 | f / aspect, 0, 0, 0, 53 | 0, f, 0, 0, 54 | 0, 0, (near + far) * rangeInv, -1, 55 | 0, 0, near * far * rangeInv * 2, 0 56 | ]); 57 | }; 58 | 59 | export let camLookAt = (cameraPosition, target, up) => { 60 | let zAxis = normalize( 61 | subtractVectors(cameraPosition, target)); 62 | let xAxis = normalize(cross(up, zAxis)); 63 | let yAxis = normalize(cross(zAxis, xAxis)); 64 | 65 | return [ 66 | xAxis[0], xAxis[1], xAxis[2], 0, 67 | yAxis[0], yAxis[1], yAxis[2], 0, 68 | zAxis[0], zAxis[1], zAxis[2], 0, 69 | cameraPosition[0], 70 | cameraPosition[1], 71 | cameraPosition[2], 72 | 1, 73 | ]; 74 | } 75 | 76 | /** 77 | * Compute the multiplication of two mat4 (c = a x b) 78 | * 79 | * @param {Float32Array} a 4x4 Matrix 80 | * @param {Float32Array} b 4x4 Matrix 81 | */ 82 | export let multMat4Mat4 = (a, b) => { 83 | let i, ai0, ai1, ai2, ai3; 84 | for (i = 0; i < 4; i++) { 85 | ai0 = a[i]; 86 | ai1 = a[i+4]; 87 | ai2 = a[i+8]; 88 | ai3 = a[i+12]; 89 | a[i] = ai0 * b[0] + ai1 * b[1] + ai2 * b[2] + ai3 * b[3]; 90 | a[i+4] = ai0 * b[4] + ai1 * b[5] + ai2 * b[6] + ai3 * b[7]; 91 | a[i+8] = ai0 * b[8] + ai1 * b[9] + ai2 * b[10] + ai3 * b[11]; 92 | a[i+12] = ai0 * b[12] + ai1 * b[13] + ai2 * b[14] + ai3 * b[15]; 93 | } 94 | return a; 95 | }; 96 | 97 | /** 98 | * Transform a mat4 99 | * options: x/y/z (translate), rx/ry/rz (rotate), sx/sy/sz (scale) 100 | * 101 | * @param {Float32Array} mat 4x4 Matrix 102 | * @param {{x?: number, y?: number, z?: number, rx?: number, ry?: number, rz?: number, sx?: number, sy?: number, sz?: number}} options x/y/z-translate, rx/ry/rz-rotate sx/sy/sz-scale 103 | */ 104 | export let transform = (mat, options) => { 105 | 106 | let x = options.x || 0; 107 | let y = options.y || 0; 108 | let z = options.z || 0; 109 | 110 | let sx = options.sx || 1; 111 | let sy = options.sy || 1; 112 | let sz = options.sz || 1; 113 | 114 | let rx = options.rx; 115 | let ry = options.ry; 116 | let rz = options.rz; 117 | 118 | // translate 119 | if(x || y || z){ 120 | mat[12] += mat[0] * x + mat[4] * y + mat[8] * z; 121 | mat[13] += mat[1] * x + mat[5] * y + mat[9] * z; 122 | mat[14] += mat[2] * x + mat[6] * y + mat[10] * z; 123 | mat[15] += mat[3] * x + mat[7] * y + mat[11] * z; 124 | } 125 | 126 | // Rotate 127 | if(rx) multMat4Mat4(mat, new Float32Array([1, 0, 0, 0, 0, Math.cos(rx), Math.sin(rx), 0, 0, -Math.sin(rx), Math.cos(rx), 0, 0, 0, 0, 1])); 128 | if(ry) multMat4Mat4(mat, new Float32Array([Math.cos(ry), 0, -Math.sin(ry), 0, 0, 1, 0, 0, Math.sin(ry), 0, Math.cos(ry), 0, 0, 0, 0, 1])); 129 | if(rz) multMat4Mat4(mat, new Float32Array([Math.cos(rz), Math.sin(rz), 0, 0, -Math.sin(rz), Math.cos(rz), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])); 130 | 131 | // Scale 132 | if(sx !== 1){ 133 | mat[0] *= sx; 134 | mat[1] *= sx; 135 | mat[2] *= sx; 136 | mat[3] *= sx; 137 | } 138 | if(sy !== 1){ 139 | mat[4] *= sy; 140 | mat[5] *= sy; 141 | mat[6] *= sy; 142 | mat[7] *= sy; 143 | } 144 | if(sz !== 1){ 145 | mat[8] *= sz; 146 | mat[9] *= sz; 147 | mat[10] *= sz; 148 | mat[11] *= sz; 149 | } 150 | 151 | return mat; 152 | }; 153 | 154 | 155 | // Create a matrix representing a rotation around an arbitrary axis [x, y, z] 156 | export let rotate = (axis, angle) => { 157 | 158 | let x = axis[0], y = axis[1], z = axis[2]; 159 | let len = Math.hypot(x, y, z); 160 | let s, c, t; 161 | 162 | if (len == 0) return null; 163 | 164 | len = 1 / len; 165 | x *= len; 166 | y *= len; 167 | z *= len; 168 | 169 | s = Math.sin(angle); 170 | c = Math.cos(angle); 171 | t = 1 - c; 172 | 173 | return new Float32Array([ 174 | x * x * t + c, y * x * t + z * s, z * x * t - y * s, 0, 175 | x * y * t - z * s, y * y * t + c, z * y * t + x * s, 0, 176 | x * z * t + y * s, y * z * t - x * s, z * z * t + c, 0, 177 | 0, 0, 0, 1 178 | ]); 179 | }; 180 | 181 | // Apply a matrix transformation to a custom axis 182 | export let transformMat4 = (a, m) => { 183 | let x = a[0], 184 | y = a[1], 185 | z = a[2]; 186 | let w = (m[3] * x + m[7] * y + m[11] * z + m[15])|| 1.0; 187 | 188 | return new Float32Array([ 189 | (m[0] * x + m[4] * y + m[8] * z + m[12]) / w, 190 | (m[1] * x + m[5] * y + m[9] * z + m[13]) / w, 191 | (m[2] * x + m[6] * y + m[10] * z + m[14]) / w 192 | ]); 193 | } 194 | 195 | /** 196 | * Transpose a matrix 197 | * 198 | * @param {Float32Array} m 4x4 Matrix 199 | */ 200 | export let transpose = m => { 201 | return new Float32Array([ 202 | m[0], m[4], m[8], m[12], 203 | m[1], m[5], m[9], m[13], 204 | m[2], m[6], m[10], m[14], 205 | m[3], m[7], m[11], m[15] 206 | ]); 207 | }; 208 | 209 | /** 210 | * Get the inverse of a mat4 211 | * The mat4 is not modified, a new mat4 is returned 212 | * 213 | * @param {Float32Array} m 4x4 Matrix 214 | */ 215 | export let inverseTranspose = m => transpose(inverse(m)); 216 | 217 | // 218 | // Optional: a "up" vector can be defined to tilt the camera on one side (vertical by default). 219 | /** 220 | * Place a camera at the position [cameraX, cameraY, cameraZ], make it look at the point [targetX, targetY, targetZ]. 221 | * Optional: a "up" vector can be defined to tilt the camera on one side (vertical by default). 222 | * 223 | * @param {Float32Array} mat 4x4 Matrix 224 | * @param {number} cameraX Camera position x 225 | * @param {number} cameraY Camera position y 226 | * @param {number} cameraZ Camera position z 227 | * @param {number} targetX Look at x 228 | * @param {number} targetY Look at y 229 | * @param {number} targetZ Look at z 230 | * @param {number} upX Tilt camera in x-axis 231 | * @param {number} upY Tilt camera in y-axis 232 | * @param {number} upZ Tilt camera in z-axis 233 | */ 234 | export let lookMatrix = (mat, cameraX, cameraY, cameraZ, targetX, targetY, targetZ, upX = 0, upY = 1, upZ = 0) => { 235 | let fx, fy, fz, rlf, sx, sy, sz, rls, ux, uy, uz; 236 | fx = targetX - cameraX; 237 | fy = targetY - cameraY; 238 | fz = targetZ - cameraZ; 239 | rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz); 240 | fx *= rlf; 241 | fy *= rlf; 242 | fz *= rlf; 243 | sx = fy * upZ - fz * upY; 244 | sy = fz * upX - fx * upZ; 245 | sz = fx * upY - fy * upX; 246 | rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz); 247 | sx *= rls; 248 | sy *= rls; 249 | sz *= rls; 250 | ux = sy * fz - sz * fy; 251 | uy = sz * fx - sx * fz; 252 | uz = sx * fy - sy * fx; 253 | let l = new Float32Array([ 254 | sx, ux, -fx, 0, 255 | sy, uy, -fy, 0, 256 | sz, uz, -fz, 0, 257 | 0, 0, 0, 1 258 | ]); 259 | transform(l, {x: -cameraX, y: -cameraY, z: -cameraZ}); 260 | return multMat4Mat4(mat, l); 261 | } 262 | /** 263 | * Create projection matrix 264 | * 265 | * @param {Float32Array} out Target 4x4 Matrix 266 | * @param {number} fov Field of View 267 | * @param {number} aspect aspect ratio 268 | * @param {number} near near clip point 269 | * @param {number} far far clip point 270 | */ 271 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.10.4": 6 | version "7.10.4" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" 8 | integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== 9 | dependencies: 10 | "@babel/highlight" "^7.10.4" 11 | 12 | "@babel/helper-validator-identifier@^7.10.4": 13 | version "7.10.4" 14 | resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" 15 | integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== 16 | 17 | "@babel/highlight@^7.10.4": 18 | version "7.10.4" 19 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" 20 | integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== 21 | dependencies: 22 | "@babel/helper-validator-identifier" "^7.10.4" 23 | chalk "^2.0.0" 24 | js-tokens "^4.0.0" 25 | 26 | "@types/node@*": 27 | version "14.0.27" 28 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.27.tgz#a151873af5a5e851b51b3b065c9e63390a9e0eb1" 29 | integrity sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g== 30 | 31 | ansi-styles@^3.2.1: 32 | version "3.2.1" 33 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 34 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 35 | dependencies: 36 | color-convert "^1.9.0" 37 | 38 | buffer-from@^1.0.0: 39 | version "1.1.1" 40 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 41 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 42 | 43 | chalk@^2.0.0: 44 | version "2.4.2" 45 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 46 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 47 | dependencies: 48 | ansi-styles "^3.2.1" 49 | escape-string-regexp "^1.0.5" 50 | supports-color "^5.3.0" 51 | 52 | color-convert@^1.9.0: 53 | version "1.9.3" 54 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 55 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 56 | dependencies: 57 | color-name "1.1.3" 58 | 59 | color-name@1.1.3: 60 | version "1.1.3" 61 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 62 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 63 | 64 | commander@^2.20.0: 65 | version "2.20.3" 66 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 67 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 68 | 69 | esbuild@^0.6.21: 70 | version "0.6.27" 71 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.6.27.tgz#e8f4d6b3bb3b4156a5e5679b5b582561b075828d" 72 | integrity sha512-oonxxg/ExZt9tYMPnEUDYAN7YefEPsC21pXez5rwKcCPAF6gzMuxLvAdbU4dCT8Tp0I2avODz61vrGyu5wR/uA== 73 | 74 | escape-string-regexp@^1.0.5: 75 | version "1.0.5" 76 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 77 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 78 | 79 | fsevents@~2.1.2: 80 | version "2.1.3" 81 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" 82 | integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== 83 | 84 | has-flag@^3.0.0: 85 | version "3.0.0" 86 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 87 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 88 | 89 | has-flag@^4.0.0: 90 | version "4.0.0" 91 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 92 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 93 | 94 | jest-worker@^26.2.1: 95 | version "26.3.0" 96 | resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.3.0.tgz#7c8a97e4f4364b4f05ed8bca8ca0c24de091871f" 97 | integrity sha512-Vmpn2F6IASefL+DVBhPzI2J9/GJUsqzomdeN+P+dK8/jKxbh8R3BtFnx3FIta7wYlPU62cpJMJQo4kuOowcMnw== 98 | dependencies: 99 | "@types/node" "*" 100 | merge-stream "^2.0.0" 101 | supports-color "^7.0.0" 102 | 103 | js-tokens@^4.0.0: 104 | version "4.0.0" 105 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 106 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 107 | 108 | merge-stream@^2.0.0: 109 | version "2.0.0" 110 | resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" 111 | integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== 112 | 113 | randombytes@^2.1.0: 114 | version "2.1.0" 115 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 116 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 117 | dependencies: 118 | safe-buffer "^5.1.0" 119 | 120 | rollup-plugin-terser@^7.0.0: 121 | version "7.0.0" 122 | resolved "https://registry.yarnpkg.com/rollup-plugin-terser/-/rollup-plugin-terser-7.0.0.tgz#26b38ada4f0b351cd7cd872ca04c0f8532d4864f" 123 | integrity sha512-p/N3lLiFusCjYTLfVkoaiRTOGr5AESEaljMPH12MhOtoMkmTBhIAfuadrcWy4am1U0vU4WTxO9fi0K09O4CboQ== 124 | dependencies: 125 | "@babel/code-frame" "^7.10.4" 126 | jest-worker "^26.2.1" 127 | serialize-javascript "^4.0.0" 128 | terser "^5.0.0" 129 | 130 | rollup@^2.26.2: 131 | version "2.26.5" 132 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.26.5.tgz#5562ec36fcba3eed65cfd630bd78e037ad0e0307" 133 | integrity sha512-rCyFG3ZtQdnn9YwfuAVH0l/Om34BdO5lwCA0W6Hq+bNB21dVEBbCRxhaHOmu1G7OBFDWytbzAC104u7rxHwGjA== 134 | optionalDependencies: 135 | fsevents "~2.1.2" 136 | 137 | safe-buffer@^5.1.0: 138 | version "5.2.1" 139 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 140 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 141 | 142 | serialize-javascript@^4.0.0: 143 | version "4.0.0" 144 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" 145 | integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== 146 | dependencies: 147 | randombytes "^2.1.0" 148 | 149 | source-map-support@~0.5.12: 150 | version "0.5.19" 151 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 152 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 153 | dependencies: 154 | buffer-from "^1.0.0" 155 | source-map "^0.6.0" 156 | 157 | source-map@^0.6.0, source-map@~0.6.1: 158 | version "0.6.1" 159 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 160 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 161 | 162 | supports-color@^5.3.0: 163 | version "5.5.0" 164 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 165 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 166 | dependencies: 167 | has-flag "^3.0.0" 168 | 169 | supports-color@^7.0.0: 170 | version "7.1.0" 171 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" 172 | integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== 173 | dependencies: 174 | has-flag "^4.0.0" 175 | 176 | terser@^5.0.0: 177 | version "5.1.0" 178 | resolved "https://registry.yarnpkg.com/terser/-/terser-5.1.0.tgz#1f4ab81c8619654fdded51f3157b001e1747281d" 179 | integrity sha512-pwC1Jbzahz1ZPU87NQ8B3g5pKbhyJSiHih4gLH6WZiPU8mmS1IlGbB0A2Nuvkj/LCNsgIKctg6GkYwWCeTvXZQ== 180 | dependencies: 181 | commander "^2.20.0" 182 | source-map "~0.6.1" 183 | source-map-support "~0.5.12" 184 | --------------------------------------------------------------------------------