├── .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 |
--------------------------------------------------------------------------------