├── .gitignore ├── .github ├── CODEOWNERS ├── LEARN.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── playbutton.png │ ├── feature_request.md │ └── bug_report.md ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── README.md ├── engine ├── Sounds │ ├── dom.js │ ├── index.js │ └── soundsengine.js ├── Engine │ ├── utils.js │ ├── setup.js │ ├── engine.js │ ├── index.js │ ├── dom.js │ ├── terrain.js │ └── cloud.js ├── TerrainColor.js ├── Instruments │ ├── BaseInstrument.js │ ├── Altimeter.js │ └── Speedometer.js ├── Ui.js ├── Perlin.js ├── SHA256.js ├── CommandHandler.js ├── Commands.js ├── Controls.js └── Plane.js ├── css ├── WASD.png ├── close.png ├── enter.png ├── start.png ├── boxyFont.otf ├── default.png ├── arrowKeys.png ├── hamburger.png ├── tlpp-icon.png ├── speakerphone.png └── Style.css ├── LICENSE └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Glowstick0017 2 | -------------------------------------------------------------------------------- /engine/Sounds/dom.js: -------------------------------------------------------------------------------- 1 | const $speakerphone = document.getElementById("speakerphone"); 2 | -------------------------------------------------------------------------------- /.github/LEARN.md: -------------------------------------------------------------------------------- 1 | # The Little Plane Project 2 | 3 | > Check readme for project setup 4 | -------------------------------------------------------------------------------- /css/WASD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/WASD.png -------------------------------------------------------------------------------- /css/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/close.png -------------------------------------------------------------------------------- /css/enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/enter.png -------------------------------------------------------------------------------- /css/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/start.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Glowstick0017] 4 | -------------------------------------------------------------------------------- /css/boxyFont.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/boxyFont.otf -------------------------------------------------------------------------------- /css/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/default.png -------------------------------------------------------------------------------- /css/arrowKeys.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/arrowKeys.png -------------------------------------------------------------------------------- /css/hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/hamburger.png -------------------------------------------------------------------------------- /css/tlpp-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/tlpp-icon.png -------------------------------------------------------------------------------- /css/speakerphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/css/speakerphone.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/playbutton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glowstick0017/Little-Plane-Project/HEAD/.github/ISSUE_TEMPLATE/playbutton.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /engine/Engine/utils.js: -------------------------------------------------------------------------------- 1 | function PositionSystem(initialX, initialY) { 2 | let posX = initialX; 3 | let posY = initialY; 4 | 5 | return { 6 | getX: () => posX, 7 | getY: () => posY, 8 | setX: (updatedX) => posX = updatedX, 9 | setY: (updatedY) => posY = updatedY, 10 | applyOnX: (applyFn) => posX = applyFn(posX), 11 | applyOnY: (applyFn) => posY = applyFn(posY), 12 | }; 13 | } 14 | 15 | function cameraHeight(altitude) { 16 | return altitude / altitudeFactor; 17 | } 18 | 19 | function adjustedPositions(xVal, yVal) { 20 | let adjustedX = Math.round(xVal / quality) * quality - (width / 2); 21 | let adjustedY = Math.round(yVal / quality) * quality - (height / 2); 22 | 23 | return { 24 | x: adjustedX, 25 | y: adjustedY 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /engine/Sounds/index.js: -------------------------------------------------------------------------------- 1 | // Propeller-like sound - softer and more rhythmic 2 | // Format: [frequency, volume, waveType] 3 | const planeSound = [ 4 | [40.0, 0.20, 'sine'], // Deep thump of blades 5 | [80.0, 0.12, 'triangle'], // Softer first harmonic 6 | [120.0, 0.06, 'triangle'], // Gentle second harmonic 7 | [200.0, 0.04, 'sine'], // Subtle upper harmonic 8 | [280.0, 0.02, 'sine'], // Very soft high frequency 9 | ]; 10 | 11 | const soundsEngine = SoundsEngine({ 12 | defaultFrequencies: planeSound 13 | }); 14 | 15 | $speakerphone.addEventListener("click", () => { 16 | $speakerphone.classList.toggle("muted"); 17 | if ($speakerphone.classList.contains("muted")) { 18 | soundsEngine.setVolume(0); 19 | } else { 20 | soundsEngine.setVolume(1); 21 | } 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /engine/Engine/setup.js: -------------------------------------------------------------------------------- 1 | let canvas = document.getElementById("canvas"); 2 | 3 | let rect = $canvas.getBoundingClientRect(); 4 | let width = ($canvas.width = rect.width); 5 | let height = ($canvas.height = rect.height); 6 | let ctx = $canvas.getContext("2d"); 7 | 8 | // altitude of the plane 9 | let altitudeFromGround = 200; 10 | let altitudeFactor = 50000; 11 | 12 | // update altitude display 13 | let displayAltitude = Math.round(altitudeFromGround); 14 | $altitude.innerHTML = "Altitude = " + displayAltitude; 15 | $settingsAltitude.innerHTML = "Altitude: " + displayAltitude; 16 | 17 | // block size by pixel, i wouldn't recommend going under 5, load times will be longer 18 | let quality = 10; 19 | $quality.innerHTML = "Quality: " + quality + "px"; 20 | 21 | // speed at which the camera moves 22 | let speed = 1; 23 | $speed.innerHTML = "Speed: " + speed; 24 | 25 | let color = 1; 26 | $color.innerHTML = "Color: " + color; 27 | 28 | // start every game with random seed 29 | let seedVal = Math.floor(Math.random() * 1000000); 30 | seed(HashToNumber(SHA256(seedVal + ""))); 31 | $seed.innerHTML = "Seed: " + seedVal; 32 | 33 | -------------------------------------------------------------------------------- /engine/Engine/engine.js: -------------------------------------------------------------------------------- 1 | function Engine( 2 | terrainInstance, 3 | cloudInstance, 4 | ) { 5 | function updateX(updatedX) { 6 | terrainInstance.updatePosX(updatedX); 7 | cloudInstance.updatePosX(updatedX); 8 | } 9 | 10 | function updateY(updatedY) { 11 | terrainInstance.updatePosY(updatedY); 12 | cloudInstance.updatePosY(updatedY); 13 | } 14 | 15 | function applyOnX(applyFn) { 16 | terrainInstance.applyOnX(applyFn); 17 | cloudInstance.applyOnX(applyFn); 18 | } 19 | 20 | function applyOnY(applyFn) { 21 | terrainInstance.applyOnY(applyFn); 22 | cloudInstance.applyOnY(applyFn); 23 | } 24 | 25 | function updateZoom(dAltitude) { 26 | terrainInstance.updateZoom(dAltitude); 27 | cloudInstance.updateZoom(dAltitude); 28 | } 29 | 30 | function draw() { 31 | ctx.clearRect(0, 0, width, height); 32 | ctx.drawImage(terrain.getTerrainCanvas(), 0, 0); 33 | ctx.drawImage(cloud.getCloudCanvas(), 0, 0); 34 | } 35 | 36 | return { 37 | updateX, 38 | updateY, 39 | applyOnX, 40 | applyOnY, 41 | updateZoom, 42 | draw, 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Glowstick // Robbie 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 | -------------------------------------------------------------------------------- /engine/Engine/index.js: -------------------------------------------------------------------------------- 1 | const MOUNTAIN_HEIGHT = 1.0; 2 | const QUALITY = quality; 3 | const ALTIUDE_FACTOR = altitudeFactor; 4 | const MIN_ALTITUDE = 100; 5 | const MAX_ALTITUDE = 1000; 6 | const DEFAULT_ALTITUDE = 200; 7 | const CLOUD_HEIGHT = 110; // Clouds start at altitude 210 (200 + 10) 8 | const CANVAS_DIMENSIONS = { 9 | width, height 10 | } 11 | 12 | const terrain = Terrain( 13 | (c) => { 14 | $coords.innerHTML = c; 15 | $coordinates.innerHTML = c; 16 | }, 17 | (a) => altimeterUpdate(a), 18 | (sL) => terrainColor(sL), 19 | (x, y) => perlin2(x, y), 20 | (s) => seed(s), 21 | MOUNTAIN_HEIGHT, 22 | CANVAS_DIMENSIONS, 23 | QUALITY, 24 | ALTIUDE_FACTOR, 25 | MIN_ALTITUDE, 26 | MAX_ALTITUDE, 27 | DEFAULT_ALTITUDE 28 | ); 29 | 30 | const cloud = Cloud( 31 | (x, y) => perlin2(x, y), 32 | (s) => seed(s), 33 | CANVAS_DIMENSIONS, 34 | QUALITY, 35 | ALTIUDE_FACTOR, 36 | MIN_ALTITUDE, 37 | MAX_ALTITUDE, 38 | CLOUD_HEIGHT, 39 | ) 40 | 41 | terrain.setSeed(HashToNumber(SHA256(seedVal + ""))); 42 | cloud.setSeed(HashToNumber(SHA256((seedVal + 200) + ""))); 43 | cloud.moveCloud(); 44 | 45 | const engine = Engine(terrain, cloud); 46 | engine.draw(); 47 | 48 | -------------------------------------------------------------------------------- /engine/Engine/dom.js: -------------------------------------------------------------------------------- 1 | // create all the variables with references to the html elements 2 | const $canvas = document.getElementById("canvas"); 3 | const $color = document.getElementById("color"); 4 | const $commandBox = document.getElementById("commandBox"); 5 | const $coordinates = document.getElementById("coordinates"); 6 | const $coords = document.getElementById("coords"); 7 | const $customize = document.getElementById("customize"); 8 | const $customizeMenu = document.getElementById("customizeMenu"); 9 | const $hamburger = document.getElementById("hamburger"); 10 | const $nav = document.getElementById("nav"); 11 | const $quality = document.getElementById("quality"); 12 | const $resetDefault = document.getElementById("resetDefault"); 13 | const $result = document.getElementById("result"); 14 | const $seed = document.getElementById("seed"); 15 | const $settings = document.getElementById("settings"); 16 | const $settingsMenu = document.getElementById("settingsMenu"); 17 | const $speed = document.getElementById("speed"); 18 | const $altitude = document.getElementById("altitude"); 19 | const $settingsAltitude = document.getElementById("settings-altitude"); 20 | const $start = document.getElementById("start"); 21 | const $ui = document.getElementById("ui"); 22 | const $welcome = document.getElementById("welcome"); 23 | const $planeColors = document.querySelectorAll( 24 | '.planeColors input[type="color"]' 25 | ); 26 | 27 | -------------------------------------------------------------------------------- /engine/TerrainColor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines the appropriate terrain color based on the value of c. 3 | * 4 | * @param {number} c - The terrain coefficient, typically between 0 and 1. 5 | * @param {object|string} [terrainConfig=defaultTerrains] - The terrain configuration object or its JSON representation. 6 | * @returns {string} - The RGB color value for the terrain. 7 | */ 8 | function terrainColor(c, terrainConfig = defaultTerrains) { 9 | let terrains; 10 | 11 | // Check if the provided config is JSON, if so, parse it. 12 | if (typeof terrainConfig === "string") { 13 | try { 14 | terrains = JSON.parse(terrainConfig); 15 | } catch (e) { 16 | throw new Error("Invalid JSON provided for terrainConfig"); 17 | } 18 | } else if (typeof terrainConfig === "object") { 19 | terrains = terrainConfig; 20 | } else { 21 | throw new Error( 22 | "Invalid terrainConfig provided. Expected JSON string or object." 23 | ); 24 | } 25 | 26 | for (let terrain of terrains) { 27 | if (c >= terrain.range[0] && c < terrain.range[1]) { 28 | for (let colorStop of terrain.colors) { 29 | if (c < colorStop.limit) { 30 | return colorStop.value; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | const defaultTerrains = [ 38 | { 39 | name: "sea", 40 | range: [0, 0.33], 41 | colors: [ 42 | { limit: 0.055, value: "rgb(0, 15, 44)" }, 43 | { limit: 0.11, value: "rgb(0, 31, 60)" }, 44 | { limit: 0.165, value: "rgb(3, 55, 81)" }, 45 | { limit: 0.22, value: "rgb(14, 78, 106)" }, 46 | { limit: 0.275, value: "rgb(23, 93, 119)" }, 47 | { limit: 0.33, value: "rgb(37, 107, 133)" }, 48 | ], 49 | }, 50 | { 51 | name: "beach", 52 | range: [0.33, 0.5], 53 | colors: [ 54 | { limit: 0.358, value: "rgb(100, 171, 227)" }, 55 | { limit: 0.386, value: "rgb(146, 196, 238)" }, 56 | { limit: 0.414, value: "rgb(187, 219, 247)" }, 57 | { limit: 0.442, value: "rgb(246, 227, 212)" }, 58 | { limit: 0.47, value: "rgb(253, 216, 181)" }, 59 | { limit: 0.5, value: "rgb(249, 209, 153)" }, 60 | ], 61 | }, 62 | { 63 | name: "plains", 64 | range: [0.5, 0.75], 65 | colors: [ 66 | { limit: 0.54, value: "rgb(198, 204, 81)" }, 67 | { limit: 0.58, value: "rgb(151, 193, 75)" }, 68 | { limit: 0.62, value: "rgb(128, 177, 69)" }, 69 | { limit: 0.66, value: "rgb(89, 149, 74)" }, 70 | { limit: 0.7, value: "rgb(68, 119, 65)" }, 71 | { limit: 0.75, value: "rgb(49, 96, 51)" }, 72 | ], 73 | }, 74 | { 75 | // expanded mountain values + amended limit: 1 = snowy white 76 | name: "mountains", 77 | range: [0.75, 1], 78 | colors: [ 79 | { limit: 0.8, value: "rgb(111, 65, 23)" }, 80 | { limit: 0.825, value: "rgb(93, 54, 19)" }, 81 | { limit: 0.85, value: "rgb(57, 29, 0)" }, 82 | { limit: 0.875, value: "rgb(75, 69, 69)" }, 83 | { limit: 0.9, value: "rgb(98, 91, 91)" }, 84 | { limit: 0.925, value: "rgb(79, 69, 73)" }, 85 | { limit: 0.95, value: "rgb(62, 55, 62)" }, 86 | { limit: 0.975, value: "rgb(48, 42, 48)" }, 87 | { limit: 1, value: "rgb(255, 255, 255)" }, 88 | ], 89 | }, 90 | ]; 91 | -------------------------------------------------------------------------------- /engine/Instruments/BaseInstrument.js: -------------------------------------------------------------------------------- 1 | class BaseInstrument { 2 | constructor(canvasId) { 3 | // Setting up canvas and its dimensions 4 | /** @type {HTMLCanvasElement} */ 5 | this.canvas = document.getElementById(canvasId); 6 | this.rect = this.canvas.getBoundingClientRect(); 7 | this.width = this.canvas.width = this.rect.width; 8 | this.height = this.canvas.height = this.rect.height; 9 | this.ctx = this.canvas.getContext("2d"); 10 | 11 | // Defining parts with their colors and coordinates for rendering 12 | this.parts = {}; 13 | 14 | // Offsets for the instruments 15 | this.shiftX = 0; 16 | this.shiftY = 0; 17 | } 18 | 19 | // Helper function to draw a rectangle with scaling 20 | drawRect(x, y, width, height, color) { 21 | this.ctx.translate(this.width / 2 + this.shiftX, this.height + this.shiftY); 22 | this.ctx.scale(0.5, 0.5); 23 | this.ctx.fillStyle = color; 24 | this.ctx.fillRect(x, y, width, height); 25 | this.ctx.scale(2, 2); 26 | this.ctx.translate(-(this.width / 2 + this.shiftX), -(this.height + this.shiftY)); 27 | } 28 | 29 | drawInstrument() { 30 | // Adjust the canvas so that element comes in center and bottom 31 | this.shiftY = 0; 32 | 33 | for (const partName in this.parts) { 34 | const part = this.parts[partName]; 35 | for (const coord of part.coords) { 36 | let [y1, y2] = [coord[1], coord[3]]; 37 | this.shiftY = -Math.max(-this.shiftY, y1, y2); 38 | } 39 | } 40 | 41 | // Iterating over each plane part to draw it 42 | for (const partName in this.parts) { 43 | const part = this.parts[partName]; 44 | for (const coord of part.coords) { 45 | let [x1, y1, x2, y2] = coord; 46 | 47 | let x = Math.min(x1, x2); 48 | let y = Math.min(y1, y2); 49 | let w = Math.abs(x2 - x1); 50 | let h = Math.abs(y2 - y1); 51 | 52 | this.drawRect(x, y, w, h, part.color); 53 | } 54 | } 55 | } 56 | 57 | // Main draw function to render the plane 58 | draw() { 59 | // Clearing the canvas before drawing 60 | this.ctx.clearRect(0, 0, this.width, this.height); 61 | 62 | // this.ctx.translate(this.width / 2, this.height / 2); 63 | // this.ctx.rotate(this.angle); 64 | // this.ctx.translate(-this.width / 2, -this.height / 2); 65 | this.drawInstrument(); 66 | this.ctx.resetTransform(); 67 | } 68 | 69 | // Function to change the color of a plane part 70 | setColor(partName, newColor) { 71 | if (this.parts[partName]) { 72 | this.parts[partName].color = newColor; 73 | this.draw(); 74 | } 75 | } 76 | 77 | resetDefaults() { 78 | document.getElementById("outer").value = this.parts["outer"].color = '#770619'; 79 | document.getElementById("innerMain").value = this.parts["innerMain"].color = '#ac322e'; 80 | document.getElementById("innerHighlight").value = this.parts["innerHighlight"].color = '#d85665'; 81 | document.getElementById("innerDarkHighlight").value = this.parts["innerDarkHighlight"].color = '#8c0308'; 82 | document.getElementById("aroundWindshield").value = this.parts["aroundWindshield"].color = '#570101'; 83 | document.getElementById("windshield").value = this.parts["windshield"].color = '#0ba0d2'; 84 | document.getElementById("propeller").value = this.parts["propeller"].color = '#333333'; 85 | document.getElementById("propellerBlades").value = this.parts["propellerBlades"].color = '#b3b3b3'; 86 | this.draw(); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## New contributor guide 2 | 3 | To get an overview of the project, read the [README](README.md) file. Here are some resources to help you get started with open source contributions: 4 | 5 | - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) 6 | - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) 7 | - [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) 8 | - [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests) 9 | 10 | 11 | ### Issues 12 | 13 | #### Create a new issue 14 | 15 | If you spot a problem or come up with an idea for the project, [search if an issue already exists](https://github.com/Glowstick0017/Little-Plane-Project/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/Glowstick0017/Little-Plane-Project/issues/new/choose). 16 | 17 | #### Solve an issue 18 | 19 | Scan through our [existing issues](https://github.com/Glowstick0017/Little-Plane-Project/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix. 20 | 21 | ### Make changes locally 22 | 23 | 1. Fork the repository. 24 | - Using GitHub Desktop: 25 | - [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop. 26 | - Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)! 27 | 28 | - Using the command line: 29 | - [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them. 30 | 31 | 2. Create a working branch and start with your changes! 32 | 33 | ### Commit your update 34 | 35 | Commit the changes once you are happy with them. Don't forget to test commands and controls :zap:. 36 | 37 | ### Pull Request 38 | 39 | When you're finished with the changes, create a pull request, also known as a PR. 40 | - Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one. 41 | - Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. 42 | Once you submit your PR, a maintainer will review your proposal. We may ask questions or request additional information. 43 | - We may ask for changes to be made before a PR can be merged using pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch. 44 | - As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations). 45 | - If you run into any merge issues, checkout this [git tutorial](https://github.com/skills/resolve-merge-conflicts) to help you resolve merge conflicts and other issues. 46 | 47 | ### Your PR is merged! 48 | 49 | Congratulations :tada::tada: The little plane project team thanks you :sparkles:. 50 | 51 | Once your PR is merged, your contributions will be publicly visible on the [latest snapshot](https://glowstick0017.github.io/Little-Plane-Project/index) until released with other changes to the [live version](https://glowstick.me/tlpp/). 52 | -------------------------------------------------------------------------------- /engine/Ui.js: -------------------------------------------------------------------------------- 1 | class UiController { 2 | constructor() { 3 | // Binding methods to the instance for correct `this` context when called from event listeners 4 | this.handleHamburgerClick = this.handleHamburgerClick.bind(this); 5 | this.handleCustomizeClick = this.handleCustomizeClick.bind(this); 6 | this.handleSettingsClick = this.handleSettingsClick.bind(this); 7 | this.updatePlaneColor = this.updatePlaneColor.bind(this); 8 | 9 | this.setupEventListeners(); 10 | } 11 | 12 | static getElement(el) { 13 | return typeof el === "string" ? document.getElementById(el) : el; 14 | } 15 | 16 | static setDisplay(state, ...elements) { 17 | elements.forEach((element) => { 18 | const el = UiController.getElement(element); 19 | switch (state) { 20 | case "toggle": 21 | el.style.display = el.style.display === "block" ? "none" : "block"; 22 | break; 23 | case "none": 24 | el.style.display = "none"; 25 | break; 26 | case "block": 27 | el.style.display = "block"; 28 | break; 29 | } 30 | }); 31 | } 32 | 33 | handleMenuToggle(menuToShow, menuToHide) { 34 | if ( 35 | menuToShow.style.display === "block" || 36 | menuToHide.style.display === "block" 37 | ) { 38 | UiController.setDisplay("none", $ui, menuToShow, menuToHide); 39 | } else { 40 | UiController.setDisplay("block", $ui, menuToShow); 41 | UiController.setDisplay("none", menuToHide); 42 | } 43 | } 44 | 45 | handleCustomizeClick() { 46 | this.handleMenuToggle($customizeMenu, $settingsMenu); 47 | } 48 | 49 | handleSettingsClick() { 50 | this.handleMenuToggle($settingsMenu, $customizeMenu); 51 | } 52 | 53 | handleHamburgerClick() { 54 | if ($welcome.style.display === "block") { 55 | constantFlight = true; 56 | UiController.setDisplay("none", $welcome, $ui); 57 | } 58 | UiController.setDisplay("toggle", $nav); 59 | 60 | if ($nav.style.display === "block") 61 | $hamburger.style.backgroundImage = "url('css/close.png')"; 62 | else { 63 | $hamburger.style.backgroundImage = "url('css/hamburger.png')"; 64 | UiController.setDisplay("none", $ui, $customizeMenu, $settingsMenu); 65 | } 66 | } 67 | 68 | updatePlaneColor(e) { 69 | const { id: partName, value: newColor } = e.target; 70 | // save colors set in local storage 71 | const littlePlaneColorsAlready = localStorage.getItem("littlePlaneColors"); 72 | localStorage.setItem( 73 | "littlePlaneColors", 74 | littlePlaneColorsAlready + "," + partName + ":" + newColor 75 | ); 76 | plane.setColor(partName, newColor); 77 | } 78 | 79 | setupEventListeners() { 80 | $hamburger.addEventListener("click", this.handleHamburgerClick); 81 | $customize.addEventListener("click", this.handleCustomizeClick); 82 | $settings.addEventListener("click", this.handleSettingsClick); 83 | $start.addEventListener("click", () => { 84 | UiController.setDisplay("none", $welcome, $ui); 85 | constantFlight = true; 86 | pause = false; 87 | }); 88 | $planeColors.forEach((input) => { 89 | input.addEventListener("change", this.updatePlaneColor); 90 | }); 91 | $resetDefault.addEventListener("click", () => { 92 | localStorage.removeItem("littlePlaneColors"); 93 | plane.resetDefaults(); 94 | }); 95 | document.addEventListener("keydown", (e) => { 96 | if (e.key === "Escape") { 97 | if ($welcome.style.display === "block") { 98 | pause = false; 99 | constantFlight = true; 100 | } 101 | UiController.setDisplay("none", $ui, $customizeMenu, $settingsMenu, $welcome); 102 | } 103 | }); 104 | } 105 | } 106 | 107 | // Initialize the class to set up event listeners and other functionalities 108 | const uiController = new UiController(); 109 | -------------------------------------------------------------------------------- /engine/Perlin.js: -------------------------------------------------------------------------------- 1 | // room to grow for 3d 2 | function Gradient(x, y, z) { 3 | this.x = x; this.y = y; this.z = z; 4 | } 5 | 6 | Gradient.prototype.dot = function dot(x, y) { 7 | return this.x*x + this.y*y; 8 | }; 9 | 10 | // room to grow for 3d 11 | let gradient3 = [new Gradient(1,1,0),new Gradient(-1,1,0),new Gradient(1,-1,0),new Gradient(-1,-1,0), 12 | new Gradient(1,0,1),new Gradient(-1,0,1),new Gradient(1,0,-1),new Gradient(-1,0,-1), 13 | new Gradient(0,1,1),new Gradient(0,-1,1),new Gradient(0,1,-1),new Gradient(0,-1,-1)]; 14 | 15 | // permutation table, originally created by Ken Perlin 16 | let permutations = [151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 17 | 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 18 | 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 19 | 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 20 | 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 21 | 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 22 | 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 23 | 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 24 | 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 25 | 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 26 | 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 27 | 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 28 | 241, 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 29 | 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 30 | 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, 151]; 31 | 32 | let perm = new Array(512); 33 | let gradientP = new Array(512); 34 | 35 | let cachedSeeds = {}; 36 | 37 | // seed definition function 38 | function seed(seed) { 39 | if (cachedSeeds[seed]) { 40 | perm = cachedSeeds[seed].perm; 41 | gradientP = cachedSeeds[seed].gradientP; 42 | return; 43 | } 44 | 45 | if(seed > 0 && seed < 1) { 46 | // Scale the seed out 47 | seed *= 65536; 48 | } 49 | 50 | seed = Math.floor(seed); 51 | if(seed < 256) { 52 | seed |= seed << 8; 53 | } 54 | 55 | for(let i = 0; i < 256; i++) { 56 | let v; 57 | if (i & 1) { 58 | v = permutations[i] ^ (seed & 255); 59 | } else { 60 | v = permutations[i] ^ ((seed>>8) & 255); 61 | } 62 | 63 | perm[i] = perm[i + 256] = v; 64 | gradientP[i] = gradientP[i + 256] = gradient3[v % 12]; 65 | } 66 | 67 | // cache the perm and gradientP arrays 68 | cachedSeeds[seed] = { 69 | perm: [...perm], 70 | gradientP: [...gradientP] 71 | }; 72 | } 73 | 74 | // default seed 75 | seed(0); 76 | 77 | // fade curve 78 | function fade(t) { 79 | return t*t*t*(t*(t*6-15)+10); 80 | } 81 | 82 | // 83 | function interpolate(a, b, t) { 84 | return (1-t)*a + t*b; 85 | } 86 | 87 | // 2D Perlin Noise 88 | function perlin2(x, y) { 89 | // Get coordinates 90 | let X = Math.floor(x), Y = Math.floor(y); 91 | // Get relative xy coordinates of point within that cell 92 | x = x - X; y = y - Y; 93 | // Wrap the integer cells at 255 (smaller integer period can be introduced here) 94 | X = X & 255; Y = Y & 255; 95 | 96 | // Calculate noise contributions from each of the four corners 97 | let n00 = gradientP[X+permutations[Y]].dot(x, y); 98 | let n01 = gradientP[X+permutations[Y+1]].dot(x, y-1); 99 | let n10 = gradientP[X+1+permutations[Y]].dot(x-1, y); 100 | let n11 = gradientP[X+1+permutations[Y+1]].dot(x-1, y-1); 101 | 102 | // Compute the fade curve value for x 103 | let u = fade(x); 104 | 105 | // Interpolate the four results 106 | return interpolate( 107 | interpolate(n00, n10, u), 108 | interpolate(n01, n11, u), 109 | fade(y)); 110 | } -------------------------------------------------------------------------------- /engine/Engine/terrain.js: -------------------------------------------------------------------------------- 1 | function Terrain( 2 | updateCoordinatesDisplay, 3 | updateAltimeterDisplay, 4 | getTerrainColor, 5 | getPerlinValue, 6 | applySeed, 7 | MOUNTAIN_HEIGHT, 8 | CANVAS_DIMENSIONS, 9 | QUALITY, 10 | ALTIUDE_FACTOR, 11 | MIN_ALTITUDE, 12 | MAX_ALTITUDE, 13 | DEFAULT_ALTITUDE 14 | ) { 15 | const pos = PositionSystem(0, 0); 16 | let altitude = DEFAULT_ALTITUDE; 17 | let seed = 0; 18 | 19 | const terrain_canvas = document.createElement('canvas'); 20 | terrain_canvas.width = CANVAS_DIMENSIONS.width; 21 | terrain_canvas.height = CANVAS_DIMENSIONS.height; 22 | const terrain_ctx = terrain_canvas.getContext("2d"); 23 | 24 | function refreshCoordinate() { 25 | const coordFactor = ALTIUDE_FACTOR / (altitude * 10); 26 | 27 | const pX = Math.round(pos.getX() / coordFactor); 28 | const pY = Math.round(pos.getY() / -coordFactor); 29 | 30 | updateCoordinatesDisplay(`Coordinates: X=${pX}, Y=${pY}`); 31 | } 32 | 33 | function calculateSeaLevel(x, y) { 34 | let camHeight = altitude / ALTIUDE_FACTOR; 35 | 36 | let { 37 | x: adjustedX, 38 | y: adjustedY, 39 | } = adjustedPositions( 40 | pos.getX(), 41 | pos.getY(), 42 | ); 43 | 44 | return ( 45 | ( 46 | getPerlinValue( 47 | (x + adjustedX) * camHeight, 48 | (y + adjustedY) * camHeight 49 | ) + MOUNTAIN_HEIGHT 50 | ) / 2 51 | ); 52 | } 53 | 54 | function drawTerrain() { 55 | const drawing_batch = new Map(); 56 | 57 | for ( 58 | let x = -QUALITY; 59 | x < CANVAS_DIMENSIONS.width + QUALITY; 60 | x += QUALITY 61 | ) { 62 | let last_color = ""; 63 | 64 | for ( 65 | let y = -QUALITY; 66 | y < height + QUALITY; 67 | y += QUALITY 68 | ) { 69 | const seaLevel = calculateSeaLevel(x, y); 70 | const color = getTerrainColor(seaLevel); 71 | 72 | if (!drawing_batch.get(color)) { 73 | drawing_batch.set(color, []) 74 | } 75 | 76 | if (last_color === color) { 77 | const to_increase = drawing_batch.get(color).pop() 78 | drawing_batch.get(color).push({ 79 | ...to_increase, 80 | delta_y: to_increase.delta_y + QUALITY 81 | }) 82 | } else { 83 | drawing_batch.get(color).push({ 84 | x, y, delta_x: QUALITY, delta_y: QUALITY 85 | }) 86 | } 87 | 88 | last_color = color 89 | } 90 | } 91 | 92 | const drawOffsetX = (pos.getX() + QUALITY / 2) % QUALITY; 93 | const drawOffsetY = (pos.getY() + QUALITY / 2) % QUALITY; 94 | 95 | for (let [color, squares] of drawing_batch) { 96 | terrain_ctx.fillStyle = color; 97 | for (let square of squares) { 98 | terrain_ctx.fillRect( 99 | square.x - drawOffsetX, 100 | square.y - drawOffsetY, 101 | square.delta_x, 102 | square.delta_y 103 | ); 104 | } 105 | } 106 | } 107 | 108 | 109 | function updatePosX(updatedX) { 110 | pos.setX(updatedX); 111 | refreshCoordinate(); 112 | } 113 | 114 | function updatePosY(updatedY) { 115 | pos.setY(updatedY); 116 | refreshCoordinate(); 117 | } 118 | 119 | function applyOnX(applyFn) { 120 | pos.applyOnX(applyFn); 121 | refreshCoordinate(); 122 | } 123 | 124 | function applyOnY(applyFn) { 125 | pos.applyOnY(applyFn); 126 | refreshCoordinate(); 127 | } 128 | 129 | function updateZoom(dAltitude) { 130 | let oldAlt = altitude; 131 | 132 | altitude += dAltitude; 133 | altitude = clamp( 134 | altitude, 135 | MIN_ALTITUDE, 136 | MAX_ALTITUDE, 137 | ); 138 | 139 | let newCamHeight = altitude; 140 | let camHeightRatio = newCamHeight / oldAlt; 141 | 142 | applyOnX(x => x / camHeightRatio); 143 | applyOnY(y => y / camHeightRatio); 144 | 145 | updateAltimeterDisplay(altitude); 146 | } 147 | 148 | function setSeed(updatedSeed) { 149 | seed = updatedSeed; 150 | } 151 | 152 | function getTerrainCanvas() { 153 | applySeed(seed); 154 | drawTerrain(); 155 | return terrain_canvas; 156 | } 157 | 158 | return { 159 | updatePosX, 160 | updatePosY, 161 | applyOnX, 162 | applyOnY, 163 | updateZoom, 164 | setSeed, 165 | getTerrainCanvas, 166 | }; 167 | } 168 | 169 | -------------------------------------------------------------------------------- /engine/Instruments/Altimeter.js: -------------------------------------------------------------------------------- 1 | class Altimeter extends BaseInstrument { 2 | constructor(canvasId) { 3 | super(canvasId); 4 | 5 | this.parts = { 6 | outerRing: { 7 | color: '#333333', 8 | coords: [ 9 | [-1 * -40, -1 * -80, -1 * 40, -1 * 80], 10 | [-1 * -100, -1 * -60, -1 * 100, -1 * 40], 11 | [-1 * -60, -1 * -100, -1 * 60, -1 * 80], 12 | [-1 * -80, -1 * -80, -1 * 80, -1 * 60], 13 | ] 14 | }, 15 | innerRing: { 16 | color: '#666666', 17 | coords: [ 18 | [-1 * -30, -1 * -70, -1 * 30, -1 * 70], 19 | [-1 * -90, -1 * -50, -1 * 90, -1 * 30], 20 | [-1 * -50, -1 * -90, -1 * 50, -1 * 70], 21 | [-1 * -70, -1 * -70, -1 * 70, -1 * 50], 22 | ] 23 | }, 24 | rangeIndicator1: { 25 | color: '#000000', 26 | coords: [ 27 | [-70, -15, -50, 5] 28 | ] 29 | }, 30 | rangeIndicator2: { 31 | color: '#333333', 32 | coords: [ 33 | [-25, -40, -45, -20] 34 | ] 35 | }, 36 | rangeIndicator3: { 37 | color: '#999999', 38 | coords: [ 39 | [-10, -50, 10, -30] 40 | ] 41 | }, 42 | rangeIndicator4: { 43 | color: '#dddddd', 44 | coords: [ 45 | [25, -40, 45, -20] 46 | ] 47 | }, 48 | rangeIndicator5: { 49 | color: '#ffffff', 50 | coords: [ 51 | [50, -15, 70, 5] 52 | ] 53 | }, 54 | needle: { 55 | color: '#ffffff', 56 | coords: [] 57 | }, 58 | centerCircle: { 59 | color: '#333333', 60 | coords: [ 61 | [-10, 20, 10, 40] 62 | ] 63 | } 64 | }; 65 | 66 | this.needleState = 0; 67 | this.needleCoords = { 68 | 0: [ 69 | [-70, -5, -50, 5], 70 | [-50, 5, -30, 15], 71 | [-30, 15, -10, 25], 72 | [-10, 25, 10, 35], 73 | [10, 35, 20, 45] 74 | ], 75 | 25: [ 76 | [-35, -50, -45, -40], 77 | [-35, -20, -25, -40], 78 | [-15, -20, -25, 0], 79 | [-5, 0, -15, 20], 80 | [-5, 20, 5, 30], 81 | [5, 30, 15, 50] 82 | ], 83 | 50: [ 84 | [-5, -50, 5, 50] 85 | ], 86 | 75: [ 87 | [-1 * -35, -50, -1 * -45, -40], 88 | [-1 * -35, -20, -1 * -25, -40], 89 | [-1 * -15, -20, -1 * -25, 0], 90 | [-1 * -5, 0, -1 * -15, 20], 91 | [-1 * -5, 20, -1 * 5, 30], 92 | [-1 * 5, 30, -1 * 15, 50] 93 | ], 94 | 100: [ 95 | [-1 * -70, -5, -1 * -50, 5], 96 | [-1 * -50, 5, -1 * -30, 15], 97 | [-1 * -30, 15, -1 * -10, 25], 98 | [-1 * -10, 25, -1 * 10, 35], 99 | [-1 * 10, 35, -1 * 20, 45] 100 | ], 101 | } 102 | 103 | this.parts.needle.coords = this.needleCoords[this.needleState]; 104 | this.shiftX = 55; 105 | 106 | this.draw(); 107 | } 108 | 109 | draw() { 110 | super.drawInstrument(); 111 | } 112 | } 113 | 114 | const altimeter = new Altimeter("instrumentCanvas"); 115 | altimeter.draw(); 116 | 117 | function updateAltimeterNeedle(needleAnglePercent) { 118 | needleAnglePercent = Math.min(needleAnglePercent, 100); 119 | needleAnglePercent = Math.max(needleAnglePercent, 0); 120 | 121 | let stateCount = Object.keys(altimeter.needleCoords).length - 1; 122 | let needleState = stateCount * (needleAnglePercent / 100); 123 | needleState = Math.floor(needleState) * (100 / stateCount); 124 | 125 | altimeter.needleState = needleState; 126 | let needleCoords = altimeter.needleCoords[needleState]; 127 | altimeter.parts.needle.coords = needleCoords; 128 | 129 | altimeter.draw(); 130 | } 131 | 132 | -------------------------------------------------------------------------------- /engine/Instruments/Speedometer.js: -------------------------------------------------------------------------------- 1 | class Speedometer extends BaseInstrument { 2 | constructor(canvasId) { 3 | super(canvasId); 4 | 5 | this.parts = { 6 | outerRing: { 7 | color: '#333333', 8 | coords: [ 9 | [-1 * -40, -1 * -80, -1 * 40, -1 * 80], 10 | [-1 * -100, -1 * -60, -1 * 100, -1 * 40], 11 | [-1 * -60, -1 * -100, -1 * 60, -1 * 80], 12 | [-1 * -80, -1 * -80, -1 * 80, -1 * 60], 13 | ] 14 | }, 15 | innerRing: { 16 | color: '#666666', 17 | coords: [ 18 | [-1 * -30, -1 * -70, -1 * 30, -1 * 70], 19 | [-1 * -90, -1 * -50, -1 * 90, -1 * 30], 20 | [-1 * -50, -1 * -90, -1 * 50, -1 * 70], 21 | [-1 * -70, -1 * -70, -1 * 70, -1 * 50], 22 | ] 23 | }, 24 | rangeIndicator1: { 25 | color: '#00ff00', 26 | coords: [ 27 | [-70, -15, -50, 5] 28 | ] 29 | }, 30 | rangeIndicator2: { 31 | color: '#AAff00', 32 | coords: [ 33 | [-25, -40, -45, -20] 34 | ] 35 | }, 36 | rangeIndicator3: { 37 | color: '#ffff00', 38 | coords: [ 39 | [-10, -50, 10, -30] 40 | ] 41 | }, 42 | rangeIndicator4: { 43 | color: '#ffaa00', 44 | coords: [ 45 | [25, -40, 45, -20] 46 | ] 47 | }, 48 | rangeIndicator5: { 49 | color: '#ff0000', 50 | coords: [ 51 | [50, -15, 70, 5] 52 | ] 53 | }, 54 | needle: { 55 | color: '#ffffff', 56 | coords: [] 57 | }, 58 | centerCircle: { 59 | color: '#333333', 60 | coords: [ 61 | [-10, 20, 10, 40] 62 | ] 63 | } 64 | }; 65 | 66 | this.needleState = 0; 67 | this.needleCoords = { 68 | 0: [ 69 | [-70, -5, -50, 5], 70 | [-50, 5, -30, 15], 71 | [-30, 15, -10, 25], 72 | [-10, 25, 10, 35], 73 | [10, 35, 20, 45] 74 | ], 75 | 25: [ 76 | [-35, -50, -45, -40], 77 | [-35, -20, -25, -40], 78 | [-15, -20, -25, 0], 79 | [-5, 0, -15, 20], 80 | [-5, 20, 5, 30], 81 | [5, 30, 15, 50] 82 | ], 83 | 50: [ 84 | [-5, -50, 5, 50] 85 | ], 86 | 75: [ 87 | [-1 * -35, -50, -1 * -45, -40], 88 | [-1 * -35, -20, -1 * -25, -40], 89 | [-1 * -15, -20, -1 * -25, 0], 90 | [-1 * -5, 0, -1 * -15, 20], 91 | [-1 * -5, 20, -1 * 5, 30], 92 | [-1 * 5, 30, -1 * 15, 50] 93 | ], 94 | 100: [ 95 | [-1 * -70, -5, -1 * -50, 5], 96 | [-1 * -50, 5, -1 * -30, 15], 97 | [-1 * -30, 15, -1 * -10, 25], 98 | [-1 * -10, 25, -1 * 10, 35], 99 | [-1 * 10, 35, -1 * 20, 45] 100 | ], 101 | } 102 | 103 | this.parts.needle.coords = this.needleCoords[this.needleState]; 104 | this.shiftX = -55; 105 | 106 | this.draw(); 107 | } 108 | 109 | draw() { 110 | super.drawInstrument(); 111 | } 112 | } 113 | 114 | const speedometer = new Speedometer("instrumentCanvas"); 115 | speedometer.draw(); 116 | 117 | function updateSpeedometerNeedle(needleAnglePercent) { 118 | needleAnglePercent = Math.min(needleAnglePercent, 100); 119 | needleAnglePercent = Math.max(needleAnglePercent, 0); 120 | 121 | let stateCount = Object.keys(speedometer.needleCoords).length - 1; 122 | let needleState = stateCount * (needleAnglePercent / 100); 123 | needleState = Math.floor(needleState) * (100 / stateCount); 124 | 125 | speedometer.needleState = needleState; 126 | let needleCoords = speedometer.needleCoords[needleState]; 127 | speedometer.parts.needle.coords = needleCoords; 128 | 129 | speedometer.draw(); 130 | } 131 | 132 | -------------------------------------------------------------------------------- /css/Style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | padding: 0px; 4 | overflow: hidden; 5 | } 6 | #canvas-container { 7 | width: 100vw; 8 | height: 100vh; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | } 13 | #canvas { 14 | width: max(1600px, 100%); 15 | height: max(800px, 100%); 16 | position: absolute; 17 | z-index: 0; 18 | } 19 | #planeCanvas { 20 | width: clamp(37.5rem, 28.846rem + 46.154vw, 75rem); 21 | height: clamp(18.75rem, 14.423rem + 23.077vw, 37.5rem); 22 | position: absolute; 23 | z-index: 1; 24 | background-color: transparent; 25 | } 26 | 27 | #instrumentCanvases { 28 | width: 100%; 29 | height: 100%; 30 | position: absolute; 31 | z-index: 2; 32 | background-color: transparent; 33 | display: flex; 34 | justify-content: center; 35 | align-items: end; 36 | } 37 | #speedometerCanvas { 38 | width: 100%; 39 | height: 100%; 40 | position: absolute; 41 | z-index: 3; 42 | background-color: transparent; 43 | } 44 | 45 | #result { 46 | position: absolute; 47 | left: 25px; 48 | bottom: 50px; 49 | width: 300px; 50 | min-height: 25px; /* Changed from height to min-height */ 51 | overflow-y: hidden; /* Prevent scroll bar */ 52 | resize: none; /* Prevent manual resize by user */ 53 | background: rgba(0, 0, 0, 0.6); 54 | border: transparent; 55 | opacity: 0; 56 | color: white; 57 | font-family: boxyFont; 58 | letter-spacing: 1px; 59 | font-size: 16px; 60 | padding: 4px; /* Add padding for better text alignment */ 61 | } 62 | 63 | #commandBox { 64 | position: absolute; 65 | left: 25px; 66 | bottom: 25px; 67 | width: 300px; 68 | height: 25px; 69 | background: rgba(0, 0, 0, 0.6); 70 | border: transparent; 71 | color: transparent; 72 | opacity: 0; 73 | } 74 | 75 | #commandBox:focus, 76 | #result:focus { 77 | outline: none; 78 | background: rgba(0, 0, 0, 0.6); 79 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.9); 80 | color: white; 81 | font-family: boxyFont; 82 | letter-spacing: 1px; 83 | font-size: 16px; 84 | opacity: 1; 85 | } 86 | 87 | #coords { 88 | position: absolute; 89 | right: 25px; 90 | top: 25px; 91 | color: black; 92 | font-family: boxyFont; 93 | letter-spacing: 1px; 94 | font-size: 16px; 95 | text-shadow: 2px 2px #fff; 96 | } 97 | 98 | #altitude { 99 | position: absolute; 100 | right: 25px; 101 | top: 46px; 102 | color: black; 103 | font-family: boxyFont; 104 | letter-spacing: 1px; 105 | font-size: 16px; 106 | text-shadow: 2px 2px #fff; 107 | } 108 | 109 | #navigateControls { 110 | position: absolute; 111 | bottom: 25px; 112 | right: 25px; 113 | z-index: 100; 114 | } 115 | 116 | /* Menu UI */ 117 | #menu { 118 | position: absolute; 119 | top: 25px; 120 | left: 25px; 121 | z-index: 100; 122 | display: flex; 123 | gap: 10px; 124 | } 125 | #hamburger { 126 | width: 50px; 127 | height: 50px; 128 | background: url("hamburger.png") 0 0; /* Set the sprite image */ 129 | background-size: 50px 50px; 130 | cursor: pointer; 131 | } 132 | #nav { 133 | background: rgba(255, 255, 255, 0.8); 134 | border: solid #323c39 4px; 135 | border-radius: 5%; 136 | display: none; 137 | margin-top: 0.5rem; 138 | padding: 0.2rem 0.8rem; 139 | font-family: boxyFont; 140 | letter-spacing: 1px; 141 | font-size: 16px; 142 | user-select: none; 143 | } 144 | #nav ul { 145 | list-style: none; 146 | padding: 0; 147 | } 148 | #nav li { 149 | cursor: pointer; 150 | } 151 | #nav li:hover { 152 | color: rgb(6, 173, 224); 153 | } 154 | 155 | /* Customisation UI */ 156 | #ui { 157 | position: absolute; 158 | top: 50%; 159 | left: 50%; 160 | transform: translate(-50%, -50%); 161 | width: 500px; 162 | height: auto; 163 | background: rgba(255, 255, 255, 0.7); 164 | border: solid #323c39 4px; 165 | padding: 1rem; 166 | font-family: boxyFont; 167 | letter-spacing: 1px; 168 | z-index: 150; 169 | display: block; 170 | } 171 | #customizeMenu { 172 | display: none; 173 | text-shadow: 1px 1px white; 174 | } 175 | #welcome { 176 | display: block; 177 | text-shadow: 1px 1px white; 178 | text-align: center; 179 | } 180 | #settingsMenu { 181 | display: none; 182 | text-shadow: 1px 1px white; 183 | } 184 | #start { 185 | cursor: pointer; 186 | } 187 | .planeColors { 188 | display: flex; 189 | flex-direction: column; 190 | gap: 0.5rem; 191 | } 192 | 193 | #speakerphone { 194 | width: 50px; 195 | height: 50px; 196 | background-image: url("speakerphone.png"); 197 | background-size: cover; 198 | cursor: pointer; 199 | } 200 | 201 | #speakerphone:not(.muted) { 202 | background-position: top; 203 | } 204 | 205 | #speakerphone.muted { 206 | background-position: bottom; 207 | } 208 | 209 | @font-face { 210 | font-family: boxyFont; 211 | src: url("boxyFont.otf") format("opentype"); 212 | } 213 | -------------------------------------------------------------------------------- /engine/SHA256.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Secure Hash Algorithm (SHA256) 3 | * http://www.webtoolkit.info/ 4 | * Original code by Angel Marin, Paul Johnston 5 | **/ 6 | 7 | function SHA256(s){ 8 | let chrsz = 8; 9 | let hexcase = 0; 10 | 11 | function safe_add (x, y) { 12 | let lsw = (x & 0xFFFF) + (y & 0xFFFF); 13 | let msw = (x >> 16) + (y >> 16) + (lsw >> 16); 14 | return (msw << 16) | (lsw & 0xFFFF); 15 | } 16 | 17 | function S (X, n) { return ( X >>> n ) | (X << (32 - n)); } 18 | function R (X, n) { return ( X >>> n ); } 19 | function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); } 20 | function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); } 21 | function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); } 22 | function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); } 23 | function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); } 24 | function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); } 25 | 26 | function core_sha256 (m, l) { 27 | let K = [0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2]; 28 | let HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]; 29 | let W = new Array(64); 30 | let a, b, c, d, e, f, g, h, i, j; 31 | let T1, T2; 32 | 33 | m[l >> 5] |= 0x80 << (24 - l % 32); 34 | m[((l + 64 >> 9) << 4) + 15] = l; 35 | 36 | for ( let i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i % 32); 80 | } 81 | return bin; 82 | } 83 | 84 | function Utf8Encode(string) { 85 | string = string.replace(/\r\n/g,'\n'); 86 | let utftext = ''; 87 | 88 | for (let n = 0; n < string.length; n++) { 89 | 90 | let c = string.charCodeAt(n); 91 | 92 | if (c < 128) { 93 | utftext += String.fromCharCode(c); 94 | } 95 | else if((c > 127) && (c < 2048)) { 96 | utftext += String.fromCharCode((c >> 6) | 192); 97 | utftext += String.fromCharCode((c & 63) | 128); 98 | } 99 | else { 100 | utftext += String.fromCharCode((c >> 12) | 224); 101 | utftext += String.fromCharCode(((c >> 6) & 63) | 128); 102 | utftext += String.fromCharCode((c & 63) | 128); 103 | } 104 | 105 | } 106 | 107 | return utftext; 108 | } 109 | 110 | function binb2hex (binarray) { 111 | let hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef'; 112 | let str = ''; 113 | for(let i = 0; i < binarray.length * 4; i++) { 114 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i % 4)*8+4)) & 0xF) + 115 | hex_tab.charAt((binarray[i>>2] >> ((3 - i % 4)*8 )) & 0xF); 116 | } 117 | return str; 118 | } 119 | 120 | s = Utf8Encode(s); 121 | return binb2hex(core_sha256(str2binb(s), s.length * chrsz)); 122 | } 123 | 124 | function HashToNumber(str) { 125 | let number = 0.0; 126 | for(let i=0;i d1 + d2, 0) / 4; 95 | 96 | if (density === 0) continue; 97 | cloud_ctx.fillStyle = `rgba(255, 255, 255, ${density})`; 98 | cloud_ctx.fillRect( 99 | x - drawOffsetX, 100 | y - drawOffsetY, 101 | QUALITY, 102 | QUALITY 103 | ); 104 | } 105 | } 106 | } 107 | 108 | function updatePosX(updatedX) { 109 | pos.setX(updatedX); 110 | } 111 | 112 | function updatePosY(updatedY) { 113 | pos.setY(updatedY); 114 | } 115 | 116 | function applyOnX(applyFn) { 117 | pos.applyOnX(applyFn); 118 | } 119 | 120 | function applyOnY(applyFn) { 121 | pos.applyOnY(applyFn); 122 | } 123 | 124 | function updateZoom(dAltitude) { 125 | let oldAlt = altitude; 126 | 127 | altitude += dAltitude; 128 | altitude = clamp( 129 | altitude, 130 | MIN_ALTITUDE - CLOUD_HEIGHT, 131 | MAX_ALTITUDE - CLOUD_HEIGHT, 132 | ); 133 | 134 | let newCamHeight = altitude; 135 | let camHeightRatio = Math.abs(newCamHeight / oldAlt); 136 | 137 | if (isNaN(camHeightRatio)) return; 138 | if (!isFinite(camHeightRatio)) return; 139 | if (camHeightRatio === 0) return; 140 | 141 | applyOnX(x => x / camHeightRatio); 142 | applyOnY(y => y / camHeightRatio); 143 | } 144 | 145 | function setSeed(updatedSeed) { 146 | seed = updatedSeed; 147 | } 148 | 149 | function moveCloud(currentTime) { 150 | // Initialize timestamp on first call 151 | if (!currentTime) { 152 | lastCloudUpdate = performance.now(); 153 | requestAnimationFrame(moveCloud); 154 | return; 155 | } 156 | 157 | // Calculate delta time for frame-rate independent movement 158 | const deltaTime = (currentTime - lastCloudUpdate) / 1000; 159 | lastCloudUpdate = currentTime; 160 | 161 | // Cap delta time to avoid huge jumps 162 | const cappedDelta = Math.min(deltaTime, 0.1); 163 | 164 | // Normalize cloud movement to 60 FPS equivalent 165 | const normalizedMovement = cappedDelta * 60; 166 | 167 | applyOnX(x => x + cloudSpeedX * normalizedMovement); 168 | applyOnY(y => y + cloudSpeedY * normalizedMovement); 169 | requestAnimationFrame(moveCloud); 170 | } 171 | 172 | function getCloudCanvas() { 173 | applySeed(seed); 174 | drawClouds(); 175 | return cloud_canvas; 176 | } 177 | 178 | return { 179 | updatePosX, 180 | updatePosY, 181 | applyOnX, 182 | applyOnY, 183 | moveCloud, 184 | updateZoom, 185 | setSeed, 186 | getCloudCanvas, 187 | } 188 | } 189 | 190 | -------------------------------------------------------------------------------- /engine/CommandHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a single Command. 3 | */ 4 | class Command { 5 | /** 6 | * Creates a new Command instance. 7 | * @param {string | string[]} name - Name of the command (case insensitive). 8 | * @param {string} description - Description or syntax of the command. 9 | * @param {number} expectedInputs - Expected number of arguments for the command. 10 | * @param {Function} executeFunction - Function to execute the command. 11 | */ 12 | constructor(name, description, expectedInputs, executeFunction) { 13 | if (Array.isArray(name)) { 14 | this.aliases = name.map((n) => n.toUpperCase()); 15 | } else { 16 | this.aliases = [name.toUpperCase()]; 17 | } 18 | this.description = description; 19 | this.expectedInputs = expectedInputs; 20 | this.execute = executeFunction; 21 | } 22 | 23 | /** 24 | * Executes the command. 25 | * @param {string[]} args - Arguments passed to the command. 26 | * @param {CommandHandler} context - CommandHandler instance. 27 | * @return {string} Feedback or result message after executing the command. 28 | */ 29 | run(args, context) { 30 | if (args.length !== this.expectedInputs) { 31 | return `Invalid syntax for ${this.name}. Expected: ${this.description}`; 32 | } 33 | return this.execute(args, context); 34 | } 35 | } 36 | 37 | /** 38 | * Represents a Command Handler that manages and executes commands. 39 | */ 40 | class CommandHandler { 41 | /** 42 | * Creates a new CommandHandler instance and sets up event listeners. 43 | */ 44 | constructor() { 45 | this.commands = new Map(); 46 | this.isFocused = false; 47 | this.setupEventListeners(); 48 | } 49 | 50 | /** 51 | * Registers a new command. 52 | * @param {Command} command - Command instance to register. 53 | */ 54 | register(command) { 55 | if (command instanceof Command) { 56 | command.aliases.forEach((alias) => { 57 | this.commands.set(alias, command); 58 | }); 59 | } else { 60 | alert(`Invalid command type ${command}. Expected Command instance.`); 61 | } 62 | } 63 | 64 | /** 65 | * Executes a command based on input string. 66 | * @param {string} commandString - Input string containing command name and arguments. 67 | * @return {string} Feedback or result message after attempting to execute the command. 68 | */ 69 | execute(commandString) { 70 | const [name, ...args] = commandString.split(" "); 71 | const command = this.commands.get(name.toUpperCase()); 72 | if (command) { 73 | return command.run(args, this); 74 | } 75 | return `Invalid command. 76 | Available commands: ${Array.from(this.commands.keys()).join(", ")}`; 77 | } 78 | 79 | /** 80 | * Checks if the command box is focused. 81 | * @return {boolean} True if command box is focused, false otherwise. 82 | */ 83 | isCommandBoxFocused() { 84 | return this.isFocused; 85 | } 86 | 87 | /** 88 | * Sets up event listeners for command box interactions. 89 | */ 90 | setupEventListeners() { 91 | document.addEventListener("keydown", this.handleGlobalKeyDown.bind(this)); 92 | 93 | $commandBox.addEventListener( 94 | "keyup", 95 | this.handleCommandBoxKeyUp.bind(this), 96 | ); 97 | $commandBox.addEventListener("blur", this.handleCommandBoxBlur.bind(this)); 98 | } 99 | 100 | handleGlobalKeyDown(e) { 101 | if (e.key === "Enter" && !this.isCommandBoxFocused()) { 102 | this.isFocused = true; 103 | $commandBox.focus(); 104 | } 105 | } 106 | 107 | handleCommandBoxKeyUp(e) { 108 | if ( 109 | e.key === "Enter" && 110 | this.isCommandBoxFocused() && 111 | e.target.value !== "" 112 | ) { 113 | const resultMessage = this.execute(e.target.value); 114 | this.displayFeedback(resultMessage); 115 | 116 | e.target.blur(); 117 | e.target.value = ""; 118 | } else if (e.key === "Esc" || e.key === "Escape") { 119 | e.target.blur(); 120 | e.target.value = ""; 121 | } 122 | } 123 | 124 | handleCommandBoxBlur() { 125 | this.isFocused = false; 126 | } 127 | 128 | /** 129 | * Displays feedback to user after command execution and fades it out. 130 | * @param {string} message - Message to display as feedback. 131 | */ 132 | displayFeedback(message) { 133 | $result.style.width = `${Math.max( 134 | 300, 135 | ...message.split("\n").map((line) => line.length * 10), 136 | )}px`; 137 | 138 | $result.value = message; 139 | 140 | $result.style.opacity = 1; 141 | $result.style.height = "25px"; 142 | $result.style.height = $result.scrollHeight + "px"; 143 | 144 | // Determine fade duration based on message length 145 | let baseFadeDuration = 5000; // 5 seconds for short message 146 | let additionalFadeTimePerCharacter = 25; // Additional milliseconds for every character 147 | let totalFadeDuration = 148 | baseFadeDuration + message.length * additionalFadeTimePerCharacter; 149 | 150 | // Determine interval and opacity decrement rate 151 | let intervalDuration = 500; // Interval at which opacity is decreased 152 | let decrementRate = intervalDuration / totalFadeDuration; // Amount to decrease opacity in each interval 153 | 154 | let fadeEffect = setInterval(function () { 155 | if (!$result.style.opacity) { 156 | $result.style.opacity = 1; 157 | } 158 | if ($result.style.opacity > 0) { 159 | $result.style.opacity -= decrementRate; 160 | } else { 161 | clearInterval(fadeEffect); 162 | } 163 | }, intervalDuration); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /engine/Sounds/soundsengine.js: -------------------------------------------------------------------------------- 1 | function SoundsEngine({ defaultFrequencies }) { 2 | let frequencies = defaultFrequencies; 3 | let globalVolume = 1; 4 | 5 | const activeOscs = []; 6 | const audioContext = new ( 7 | window.AudioContext 8 | || window.webkitAudioContext 9 | )(); 10 | 11 | // Create a low-pass filter for more realistic engine sound 12 | const filter = audioContext.createBiquadFilter(); 13 | filter.type = 'lowpass'; 14 | filter.frequency.value = 800; // Lower cutoff for softer sound 15 | filter.Q.value = 0.5; // Gentler rolloff 16 | filter.connect(audioContext.destination); 17 | 18 | // Create a master gain node 19 | const masterGain = audioContext.createGain(); 20 | masterGain.gain.value = 0.25; // Much quieter overall 21 | masterGain.connect(filter); 22 | 23 | function unlockAudio() { 24 | if (audioContext.state !== 'suspended') return; 25 | 26 | audioContext.resume(); 27 | 28 | document.removeEventListener('touchstart', unlockAudio); 29 | document.removeEventListener('touchend', unlockAudio); 30 | document.removeEventListener('mousedown', unlockAudio); 31 | document.removeEventListener('keydown', unlockAudio); 32 | 33 | playSounds(frequencies); 34 | } 35 | 36 | function addUnlockListener() { 37 | document.addEventListener('touchstart', unlockAudio, false); 38 | document.addEventListener('touchend', unlockAudio, false); 39 | document.addEventListener('mousedown', unlockAudio, false); 40 | document.addEventListener('keydown', unlockAudio, false); 41 | } 42 | 43 | function playSounds(frequencies) { 44 | stopSounds(); 45 | 46 | frequencies.forEach(([freq, vol, waveType = 'sawtooth'], i) => { 47 | const osc = audioContext.createOscillator(); 48 | const gain = audioContext.createGain(); 49 | 50 | // Use different wave types for richer sound 51 | osc.type = waveType; 52 | osc.frequency.value = freq; 53 | 54 | // Smooth gain changes to avoid clicks 55 | gain.gain.setValueAtTime(0, audioContext.currentTime); 56 | gain.gain.linearRampToValueAtTime(vol * globalVolume, audioContext.currentTime + 0.1); 57 | 58 | osc.connect(gain); 59 | gain.connect(masterGain); 60 | 61 | osc.start(); 62 | activeOscs.push([osc, gain, waveType]); 63 | 64 | // Add subtle vibrato for engine roughness on low frequencies 65 | if (i === 0 && freq < 150) { 66 | const lfo = audioContext.createOscillator(); 67 | const lfoGain = audioContext.createGain(); 68 | 69 | lfo.frequency.value = 5 + Math.random() * 3; // 5-8 Hz vibrato 70 | lfoGain.gain.value = freq * 0.015; // Subtle pitch variation 71 | 72 | lfo.connect(lfoGain); 73 | lfoGain.connect(osc.frequency); 74 | lfo.start(); 75 | 76 | activeOscs.push([lfo, lfoGain, 'lfo']); 77 | } 78 | }); 79 | } 80 | 81 | function setFrequencies(newFrequencies) { 82 | frequencies = newFrequencies; 83 | const currentTime = audioContext.currentTime; 84 | 85 | activeOscs.forEach(([osc, gain, waveType], i) => { 86 | if (waveType === 'lfo') return; // Skip LFO oscillators 87 | 88 | const freqIndex = activeOscs.slice(0, i).filter(([,, wt]) => wt !== 'lfo').length; 89 | const [freq, vol, newWaveType = 'sawtooth'] = frequencies[freqIndex] || [0, 0, 'sawtooth']; 90 | 91 | // Smooth frequency transitions 92 | osc.frequency.cancelScheduledValues(currentTime); 93 | osc.frequency.setValueAtTime(osc.frequency.value, currentTime); 94 | osc.frequency.linearRampToValueAtTime(freq, currentTime + 0.05); 95 | 96 | // Smooth volume transitions 97 | gain.gain.cancelScheduledValues(currentTime); 98 | gain.gain.setValueAtTime(gain.gain.value, currentTime); 99 | gain.gain.linearRampToValueAtTime(vol * globalVolume, currentTime + 0.05); 100 | }); 101 | 102 | // Update filter cutoff based on frequency (higher speed = brighter sound) 103 | if (frequencies.length > 0) { 104 | const avgFreq = frequencies.reduce((sum, [f]) => sum + f, 0) / frequencies.length; 105 | const filterFreq = Math.min(1200, 600 + avgFreq * 1.2); // More mellow range 106 | filter.frequency.setValueAtTime(filter.frequency.value, currentTime); 107 | filter.frequency.linearRampToValueAtTime(filterFreq, currentTime + 0.1); 108 | } 109 | } 110 | 111 | function stopSounds() { 112 | const currentTime = audioContext.currentTime; 113 | 114 | while (activeOscs.length) { 115 | const [osc, gain] = activeOscs.pop(); 116 | try { 117 | // Fade out to avoid clicks 118 | gain.gain.cancelScheduledValues(currentTime); 119 | gain.gain.setValueAtTime(gain.gain.value, currentTime); 120 | gain.gain.linearRampToValueAtTime(0, currentTime + 0.05); 121 | 122 | osc.stop(currentTime + 0.05); 123 | } catch {} 124 | 125 | setTimeout(() => { 126 | try { osc.disconnect(); } catch {} 127 | try { gain.disconnect(); } catch {} 128 | }, 100); 129 | } 130 | } 131 | 132 | function setVolume(value) { 133 | const currentTime = audioContext.currentTime; 134 | globalVolume = value; 135 | 136 | // Smooth volume transitions 137 | masterGain.gain.cancelScheduledValues(currentTime); 138 | masterGain.gain.setValueAtTime(masterGain.gain.value, currentTime); 139 | masterGain.gain.linearRampToValueAtTime(0.25 * globalVolume, currentTime + 0.1); 140 | } 141 | 142 | addUnlockListener(); 143 | 144 | return { 145 | setVolume, 146 | setFrequencies, 147 | }; 148 | } 149 | 150 | -------------------------------------------------------------------------------- /engine/Commands.js: -------------------------------------------------------------------------------- 1 | // Seed command - spawns the player into the starting position of a new world. 2 | const seedCommand = new Command("SEED", "SEED ", 1, (args) => { 3 | const newSeed = args[0]; 4 | 5 | terrain.setSeed(HashToNumber(SHA256(newSeed))); 6 | engine.updateX(0); 7 | engine.updateY(0); 8 | 9 | $result.value = "seed set to `" + newSeed + "`"; 10 | $seed.innerHTML = "Seed: " + newSeed; 11 | engine.draw(); 12 | return "seed set to `" + newSeed + "`"; 13 | }); 14 | 15 | // Speed command - adjusts the speed of movement. 16 | const speedCommand = new Command("SPEED", "SPEED ", 1, (args) => { 17 | const speedVal = Number(args[0]); 18 | if (isNaN(speedVal) || !Number.isInteger(speedVal)) { 19 | return "Invalid syntax `speed `"; 20 | } else if (speedVal > 20 || speedVal < 0) { 21 | return "Invalid input. Range 0-20"; 22 | } else { 23 | speed = speedVal; 24 | speedometerUpdate(); 25 | return "New speed set to " + speedVal; 26 | } 27 | }); 28 | 29 | // Throttle power command - adjusts the how fast speed changes 30 | const throttlePowerCommand = new Command("POWER", "POWER ", 1, (args) => { 31 | const throttlePowerVal = Number(args[0]); 32 | if (isNaN(throttlePowerVal) || !Number.isInteger(throttlePowerVal)) { 33 | return "Invalid syntax `power `"; 34 | } else if (throttlePowerVal > 20 || throttlePowerVal < 0) { 35 | return "Invalid input. Range 0-20"; 36 | } else { 37 | throttlePower = throttlePowerVal; 38 | return "New throttle power set to " + throttlePowerVal; 39 | } 40 | }); 41 | 42 | // Max speed command - adjusts the maximum speed of movement. 43 | const maxSpeedCommand = new Command("MAXSPEED", "MAXSPEED ", 1, (args) => { 44 | const maxSpeedVal = Number(args[0]); 45 | if (isNaN(maxSpeedVal) || !Number.isInteger(maxSpeedVal)) { 46 | return "Invalid syntax `maxspeed `"; 47 | } else if (maxSpeedVal > 20 || maxSpeedVal < 0) { 48 | return "Invalid input. Range 0-20"; 49 | } else if (maxSpeedVal < minSpeed) { 50 | return "Invalid input. Max speed cannot be less than min speed"; 51 | } else { 52 | maxSpeed = maxSpeedVal; 53 | speedometerUpdate(); 54 | return "New max speed set to " + maxSpeedVal; 55 | } 56 | }); 57 | 58 | // Min speed command - adjusts the minimum speed of movement. 59 | const minSpeedCommand = new Command("MINSPEED", "MINSPEED ", 1, (args) => { 60 | const minSpeedVal = Number(args[0]); 61 | if (isNaN(minSpeedVal) || !Number.isInteger(minSpeedVal)) { 62 | return "Invalid syntax `minspeed `"; 63 | } else if (minSpeedVal > 20 || minSpeedVal < 0) { 64 | return "Invalid input. Range 0-20"; 65 | } else if (minSpeedVal > maxSpeed) { 66 | return "Invalid input. Min speed cannot be greater than max speed"; 67 | } else { 68 | minSpeed = minSpeedVal; 69 | speedometerUpdate(); 70 | return "New min speed set to " + minSpeedVal; 71 | } 72 | }); 73 | 74 | // Quality command - adjusts the quality of the world. 75 | const qualityCommand = new Command("QUALITY", "QUALITY ", 1, (args) => { 76 | const qualVal = Number(args[0]); 77 | if (isNaN(qualVal) || !Number.isInteger(qualVal)) { 78 | return "Invalid syntax `quality `"; 79 | } else if (qualVal > 20 || qualVal < 5) { 80 | return "Invalid input. Range 5-20"; 81 | } else { 82 | quality = qualVal; 83 | engine.draw(); 84 | $quality.innerHTML = "Quality: " + quality + "px"; 85 | return "Quality set to " + qualVal; 86 | } 87 | }); 88 | 89 | // Teleport command - teleports the player to specific coordinates in the current world. 90 | const teleportCommand = new Command( 91 | ["TELEPORT", "TP"], 92 | "TELEPORT or TP ", 93 | 2, 94 | (args) => { 95 | const x = Number(args[0]); 96 | const y = Number(args[1]); 97 | 98 | let invalid = false; 99 | 100 | invalid |= isNaN(x) || isNaN(y); 101 | invalid |= !Number.isInteger(x) || !Number.isInteger(y); 102 | 103 | if (invalid) { 104 | return "Invalid syntax `teleport `"; 105 | } 106 | 107 | engine.updateX(x * (altitudeFactor / (altitudeFromGround * 10))); 108 | engine.updateY(-y * (altitudeFactor / (altitudeFromGround * 10))); 109 | 110 | engine.draw(); 111 | return "Teleported to " + x + " " + y; 112 | }, 113 | ); 114 | 115 | // Color command - toggles the color setting. 116 | const colorCommand = new Command("COLOR", "COLOR", 0, (args) => { 117 | if (color === 0) { 118 | color = 1; 119 | $color.innerHTML = "Color: " + color; 120 | engine.draw(); 121 | return "Color on."; 122 | } else { 123 | color = 0; 124 | $color.innerHTML = "Color: " + color; 125 | engine.draw(); 126 | return "Color off."; 127 | } 128 | }); 129 | 130 | const helpCommand = new Command( 131 | "HELP", 132 | "list all commands and their usage", 133 | 0, 134 | (args, context) => { 135 | let helpMessages = []; 136 | context.commands.forEach((cmd) => { 137 | helpMessages.push(`${cmd.aliases.join(", ")}: ${cmd.description}`); 138 | }); 139 | // remove duplicates 140 | helpMessages = [...new Set(helpMessages)]; 141 | 142 | return helpMessages.join("\n"); 143 | }, 144 | ); 145 | 146 | // Instantiate a new CommandHandler and register the commands. 147 | const handler = new CommandHandler(); 148 | handler.register(seedCommand); 149 | handler.register(speedCommand); 150 | handler.register(throttlePowerCommand); 151 | handler.register(maxSpeedCommand); 152 | handler.register(minSpeedCommand); 153 | handler.register(qualityCommand); 154 | handler.register(teleportCommand); 155 | handler.register(colorCommand); 156 | handler.register(helpCommand); 157 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. 128 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | # The Little Plane Project 2 | ![alt text](https://github.com/Glowstick0017/Little-Plane-Project/blob/master/css/tlpp-icon.png?raw=true) 3 | [![image](/.github/ISSUE_TEMPLATE/playbutton.png)](https://glowstick.me/tlpp/index.html) 4 | 5 | [![image](https://img.shields.io/badge/Live_build-darkgreen)](https://glowstick.me/tlpp/) 6 | [![image](https://img.shields.io/badge/Snapshot-25w44a-yellow)](https://glowstick0017.github.io/Little-Plane-Project/index) 7 | 8 | ![image](https://img.shields.io/static/v1?label=Chrome&message=%E2%9C%94&color=success?style=social&logo=google-chrome&logoColor=white) 9 | ![image](https://img.shields.io/static/v1?label=Firefox&message=%E2%9C%94&color=success?style=social&logo=Firefox&logoColor=white) 10 | ![image](https://img.shields.io/static/v1?label=Microsoft%20Edge&message=%E2%9C%94&color=success?style=social&logo=Microsoft-edge&logoColor=white) 11 | ![image](https://img.shields.io/static/v1?label=Internet%20Explorer&message=%E2%9C%94&color=success?style=social&logo=Internet-Explorer&logoColor=white) 12 | ![image](https://img.shields.io/static/v1?label=Safari&message=%E2%9C%94&color=success?style=social&logo=Safari&logoColor=white) 13 | ![image](https://img.shields.io/static/v1?label=Mobile&message=In%20progress&color=critical) 14 | 15 | ## Introduction 16 | `The little plane project` is a small passion project that I started based on my interest and curiosity on perlin noise and procedural generation. Rather than making a project in a prebuilt game engine like ~~Unity~~Godot(we don't like Unity around here anymore), I wanted to create my own engine from the ground up using only javascript and no external libraries (only using a prebuilt algorithm for SHA256 made by [Angel Marin](https://anmar.eu.org/) and [Paul Johnston](http://pajhome.org.uk/)). I made this project in javascript so that a majority of people can read and understand what's happening in the code (may not be too understandable since I have poor documentation, standards, and this is my first decently sized javascript project), as well as an extreme ease of access to a demo of the project to anyone that can use a browser. Credit also goes out to [Javidx9 or OneLoneCoder](https://github.com/OneLoneCoder) for their explanation on Perlin noise in the first 10 minutes of [this video](https://youtu.be/6-0UaeJBumA) which tremendously helped wrap my head around what Perlin noise is and how to go about my implementation. 17 | 18 | # Table of contents 19 | - [Getting started](#getting-started--controls) 20 | - [Commands](#commands) 21 | - [Roadmap](#roadmap) 22 | - [Run a local/dev copy](#how-to-run-a-development-copy-of-the-project) 23 | - [Contributors](#contributors) 24 | - [License](#license) 25 | 26 | # Getting started & controls 27 | Hi! Thank you for checking out `The little plane project`. Thus far there isnt much to do in the plane project but fly around and discover generated chunks that nobody has ever seen before. To get started visit the [play site](https://glowstick.me/tlpp/index.html). You can move with either the `WASD` keys like any other game or you can use the arrow keys if you're oldschool. Up/Down changes your speed and Left/Right changes your direction. 28 | 29 | ![image](https://github.com/Glowstick0017/Little-Plane-Project/blob/master/css/WASD.png?raw=true) 30 | ![image](https://github.com/Glowstick0017/Little-Plane-Project/blob/master/css/arrowKeys.png?raw=true) 31 | 32 | To open the command box, hit your `Enter` key and you can use any of these commands to change your experience. I recommend using the seed command to generate a new world since the play site always starts on the same seed. 33 | 34 | ![image](https://github.com/Glowstick0017/Little-Plane-Project/blob/master/css/enter.png?raw=true) 35 | 36 | Enjoy your time and please send any suggestions you have to Glowstick#0017 on discord or leave your suggestions [here](https://github.com/Glowstick0017/Little-Plane-Project/issues/new?assignees=&labels=new+feature&template=feature_request.md&title=Feature%20Request). 37 | 38 | # Commands 39 | | Command | Description | Default value | 40 | |:------------------:|-------------------------------------------------------------------------------------------------------------------------------|:-------------:| 41 | | color | Toggle between full world color and gray scale to view underlying perlin plane | 1 | 42 | | quality \ | Set rendering quality, the lower the more detailed.
Anything below 10 will lag. Range 5-20 | 10 | 43 | | seed \ | Generate a new world with the given seed.
Any length of characters, words, or numbers can be entered for a unique world | random | 44 | | speed \ | Set flight speed. Range 1-10 | 1 | 45 | | teleport \ \
tp \ \ | Teleport to specified coordinates | 0 0 | 46 | 47 | 48 | ## Instructions within the Application 49 | With the addition of the context menu thanks to jackwebdev, detailed instructions are available within the application itself, providing information on controls and command execution. These instructions enhance the user experience and understanding. 50 | 51 | # Roadmap 52 | Upcoming features to be implemented 53 | - [ ] biomes by using multile perlin planes to simulate different conditions including sea level, temperature, humidity... 54 | - [X] Instructions in context menu 55 | - [ ] mobile controls (either joystick or click on displayed WASD) 56 | - [ ] context menu friendly on mobile 57 | - [X] menu screen or esc menu 58 | - [X] command to change plane or color of plane 59 | - [X] user adjusted color values 60 | - [ ] toggleable stats 61 | - [X] color command that shows underlying perlin plane in black/white 62 | - [ ] different worlds 63 | - [ ] randomly spawned airports to refuel 64 | - [ ] deliver packages from airports to others directed by arrow on screen 65 | - [ ] user feature suggestions 66 | 67 | # How to run a development copy of the project 68 | This project was made to learn the concepts and technologies used as simple as possible, from how perlin noise works to simple javascript fundamentals. 69 | The only thing you need to do to make and see your own changes in action is simply clone the repository, make your experimental changes to the code, and open index.html on your browser to view your changes. 70 | If you made any changes you think would be helpful to add to the original repository, feel free to open a pull request. All proposed changes will be reviewed and whether or not they're added, there will be meaningful comments left to help support everyones learning journey. 71 | 72 | # Contributors 73 | Meet the talented individuals who have contributed to The Little Plane Project and learn how you can join them in [contributing to the project](CONTRIBUTING.md): 74 | 75 | 76 | 77 | 78 | 79 | # License 80 | Distributed under the MIT License. See [LICENSE](https://github.com/Glowstick0017/Little-Plane-Project/blob/master/LICENSE) for more information. 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | The little plane project 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |
26 | 27 |
28 | 47 | 48 |
49 |
50 |

Welcome to The Little Plane Project

51 |

Use the arrow keys to navigate the world.

52 |

Up/Down changes your speed.

53 |

Left/Right changes your direction.

54 | Arrow keys 55 |

Use the command box to interact with the world

56 | Enter Key 57 |

Use the menu to change settings and customize your plane

58 | Hamburger Menu 59 |
60 |
61 |
62 |
63 | Start 64 | 66 |

Learn more about The Little Plane Project

67 |
68 |
69 | 70 |
71 |

Customise your plane

72 |
73 |
74 | 75 | 76 |
77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 |
85 |
86 | 87 | 88 |
89 |
90 | 91 | 92 |
93 |
94 | 95 | 96 |
97 |
98 | 99 | 100 |
101 |
102 | 103 | 104 |
105 | Reset Default Colors 106 |
107 |
108 | 109 |
110 |

Settings

111 |
112 |
113 | 114 |
115 |
116 | 117 |
118 |
119 | 120 |
121 |
122 | 123 |
124 |
125 | 126 |
127 |
128 | 129 |
130 | 152 |
153 |
154 |
155 | 156 | 157 | 158 | 159 | 160 | WASD controls 161 | 162 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /engine/Controls.js: -------------------------------------------------------------------------------- 1 | // Dictionary to track the current state of key presses 2 | let keysPressed = {}; 3 | 4 | // Flag to check if the sprint mode is active 5 | let sprinting = false; 6 | 7 | // Flag to determine if the canvas needs a redraw 8 | let needsRedraw = false; 9 | 10 | //Unit Vector for direction 11 | let dx = 0; 12 | let dy = -1; 13 | 14 | //player Angle for plane's yaw 15 | let playerAngle = -3.14 / 2; 16 | let yawStrength = 0.02; //the strength of rotation 17 | 18 | //constant flight toggle 19 | let constantFlight = false; 20 | 21 | //pause 22 | let pause = true; 23 | 24 | let t = 0; 25 | let cooldown = 0; 26 | 27 | // Delta time tracking for frame-rate independent movement 28 | let lastFrameTime = performance.now(); 29 | let deltaTime = 0; 30 | 31 | // speed of the plane 32 | let minSpeed = 0.5; 33 | let maxSpeed = 1.5; 34 | let throttlePower = 0.5; 35 | 36 | // altitude based 37 | let minAltitude = 100; 38 | let maxAltitude = 1000; 39 | let altSpeed = 1; 40 | 41 | //update dx and dy based on player angle 42 | const updateDirection = () => { 43 | dx = Math.cos(playerAngle); 44 | dy = Math.sin(playerAngle); 45 | } 46 | 47 | // Defining constants for keys to improve readability 48 | const KEYS = { 49 | W: 'w', 50 | A: 'a', 51 | S: 's', 52 | D: 'd', 53 | ARROW_UP: 'ArrowUp', 54 | ARROW_DOWN: 'ArrowDown', 55 | ARROW_LEFT: 'ArrowLeft', 56 | ARROW_RIGHT: 'ArrowRight', 57 | LT_SQ_BRACKET: "[", 58 | RT_SQ_BRACKET: "]", 59 | ToggleForward: 'e', 60 | }; 61 | 62 | // Registering keypress and release events 63 | document.addEventListener("keydown", registerKeyPress); 64 | document.addEventListener("keyup", deregisterKeyPress); 65 | 66 | // Handler for key press events 67 | function registerKeyPress(e) { 68 | if (handler.isFocused) return; // Makes sure commands aren't being typed 69 | keysPressed[formatKey(e.key)] = true; 70 | } 71 | 72 | // Handler for key release events 73 | function deregisterKeyPress(e) { 74 | keysPressed[formatKey(e.key)] = false; 75 | } 76 | 77 | function formatKey(key) { 78 | return key.startsWith("Arrow") ? key : key.toLowerCase(); 79 | } 80 | 81 | // Elevate the sea level 82 | function elevateAltitude(dz) { 83 | engine.updateZoom(dz); 84 | needsRedraw = true; 85 | } 86 | 87 | function clamp(value, min, max) { 88 | if (value < min) { 89 | return min; 90 | } 91 | 92 | if (value > max) { 93 | return max; 94 | } 95 | 96 | return value; 97 | } 98 | 99 | function speedometerUpdate() { 100 | $speed.innerHTML = `Speed: ${speed}`; 101 | 102 | let speedometerNeedleAnglePercent = (speed - minSpeed); 103 | speedometerNeedleAnglePercent /= (maxSpeed - minSpeed); 104 | speedometerNeedleAnglePercent *= 100; 105 | 106 | updateSpeedometerNeedle(speedometerNeedleAnglePercent); 107 | } 108 | 109 | function altimeterUpdate(altitude) { 110 | let displayAltitude = Math.round(altitude); 111 | $altitude.innerHTML = "Altitude = " + displayAltitude; 112 | $settingsAltitude.innerHTML = "Altitude: " + displayAltitude; 113 | 114 | let altimeterNeedleAnglePercent = (altitude - minAltitude); 115 | altimeterNeedleAnglePercent /= (maxAltitude - minAltitude); 116 | altimeterNeedleAnglePercent = altimeterNeedleAnglePercent; 117 | altimeterNeedleAnglePercent *= 100; 118 | 119 | let adjustedShadowHeight = 1.01 - (altimeterNeedleAnglePercent / 100); 120 | adjustedShadowHeight *= adjustedShadowHeight * adjustedShadowHeight; 121 | 122 | plane.setShadowHeight(adjustedShadowHeight); 123 | updateAltimeterNeedle(altimeterNeedleAnglePercent * 1.5); 124 | } 125 | 126 | function throttleChange(updateFn) { 127 | let shouldClamp = true; 128 | 129 | if (minSpeed > speed || speed > maxSpeed) { 130 | shouldClamp = false; 131 | } 132 | 133 | speed = updateFn(speed, throttlePower / 100); 134 | if (shouldClamp) { 135 | speed = clamp(speed, minSpeed, maxSpeed); 136 | } 137 | 138 | // More realistic speed-to-pitch mapping for propeller sound 139 | const speedFactor = (speed - minSpeed) / (maxSpeed - minSpeed); 140 | // Gentler pitch progression for propeller 141 | const pitchMultiplier = 0.8 + (speedFactor * 0.6); 142 | 143 | soundsEngine.setFrequencies(planeSound.map(([f, vol, waveType]) => [ 144 | f * pitchMultiplier, 145 | vol * (0.85 + speedFactor * 0.3), // Subtle volume increase with speed 146 | waveType 147 | ])); 148 | 149 | speedometerUpdate(); 150 | } 151 | 152 | throttleChange((s, tp) => s); // Initialize speed 153 | 154 | // Movement Rotation 155 | function moveRotate(dx, dy , keyPressedFlag) { 156 | 157 | // Update the position of plane based on the direction of movement, only if any of the keys is pressed 158 | if (keyPressedFlag) { 159 | // Use deltaTime to make movement frame-rate independent 160 | // Movement speed is normalized to 60 FPS (deltaTime is in seconds, so multiply by speed factors) 161 | const normalizedMovement = deltaTime * 60; 162 | engine.applyOnX(x => x + dx * speed * 10 * normalizedMovement); 163 | engine.applyOnY(y => y + dy * speed * 10 * normalizedMovement); 164 | } 165 | // // Rotate the plane based on the direction of movement 166 | 167 | if(dx === 0 && dy === 0) /*Blank case*/; 168 | else if(dx >= 0) plane.rotate(Math.atan(dy/dx) + Math.PI/2); 169 | else plane.rotate(Math.atan(dy/dx) - Math.PI/2); 170 | // console.log(Math.atan(-dy/dx)*180/Math.PI); 171 | 172 | // Set the flag to redraw the canvas 173 | needsRedraw = true; 174 | } 175 | 176 | // Dictionary to map input keys to change the playerAngle 177 | const horizontalMapping = { 178 | [KEYS.A]: () => { playerAngle -= yawStrength * deltaTime * 60 , updateDirection()}, 179 | [KEYS.ARROW_LEFT]: () => { playerAngle -= yawStrength * deltaTime * 60 , updateDirection()}, 180 | [KEYS.D]: () => { playerAngle += yawStrength * deltaTime * 60 , updateDirection()}, 181 | [KEYS.ARROW_RIGHT]: () => { playerAngle += yawStrength * deltaTime * 60 , updateDirection()}, 182 | }; 183 | 184 | // Dictionary to map input keys to toggle throttle 185 | const verticalMapping = { 186 | [KEYS.W]: () => { throttleChange((s, tp) => s + tp * deltaTime * 60) }, 187 | [KEYS.ARROW_UP]: () => { throttleChange((s, tp) => s + tp * deltaTime * 60) }, 188 | [KEYS.S]: () => { throttleChange((s, tp) => s - tp * deltaTime * 60) }, 189 | [KEYS.ARROW_DOWN]: () => { throttleChange((s, tp) => s - tp * deltaTime * 60) }, 190 | }; 191 | 192 | // Function to initialize the game 193 | function gameInit() { 194 | // update instruments 195 | speedometerUpdate(); 196 | altimeterUpdate(200); 197 | } 198 | 199 | // Game loop to handle movement and rendering 200 | function gameLoop(currentTime) { 201 | // Calculate delta time in seconds 202 | deltaTime = (currentTime - lastFrameTime) / 1000; 203 | lastFrameTime = currentTime; 204 | 205 | // Cap delta time to avoid huge jumps (e.g., when tab is inactive) 206 | if (deltaTime > 0.1) { 207 | deltaTime = 0.1; 208 | } 209 | 210 | if (pause) { 211 | keysPressed = {}; 212 | } 213 | 214 | let keyPressedFlag; 215 | 216 | if (keysPressed[KEYS.ToggleForward]){ 217 | constantFlight=!constantFlight; 218 | keysPressed[KEYS.ToggleForward]=false; 219 | } 220 | constantFlight ? keyPressedFlag=true : keyPressedFlag=false; 221 | 222 | // Check if any of the keys are pressed and update dx 223 | for(let key in horizontalMapping){ 224 | if (keysPressed[key]) { 225 | horizontalMapping[key](); 226 | keyPressedFlag = true; 227 | } 228 | } 229 | 230 | // Check if any of the keys are pressed and update dy and change the flag to true 231 | for(let key in verticalMapping){ 232 | if (keysPressed[key]) { 233 | verticalMapping[key](); 234 | keyPressedFlag = true; 235 | } 236 | } 237 | 238 | if(keysPressed[KEYS.LT_SQ_BRACKET]){ 239 | // Make altitude change frame-rate independent 240 | elevateAltitude(-altSpeed * deltaTime * 60); 241 | } 242 | else if(keysPressed[KEYS.RT_SQ_BRACKET]){ 243 | // Make altitude change frame-rate independent 244 | elevateAltitude(altSpeed * deltaTime * 60); 245 | } 246 | 247 | moveRotate(dx, dy , keyPressedFlag) 248 | 249 | // Redraw the canvas if needed 250 | if (needsRedraw) { 251 | ctx.clearRect(0, 0, $canvas.width, $canvas.height); 252 | engine.draw(); 253 | needsRedraw = false; // Reset the flag after drawing 254 | } 255 | 256 | requestAnimationFrame(gameLoop); // Queue the next iteration 257 | } 258 | 259 | gameInit(); // Initialize the game 260 | requestAnimationFrame(gameLoop); // Initiate the game loop with proper timestamp 261 | -------------------------------------------------------------------------------- /engine/Plane.js: -------------------------------------------------------------------------------- 1 | class Plane { 2 | constructor(canvasId) { 3 | // Setting up canvas and its dimensions 4 | /** @type {HTMLCanvasElement} */ 5 | this.canvas = document.getElementById(canvasId); 6 | this.rect = this.canvas.getBoundingClientRect(); 7 | this.width = this.canvas.width = this.rect.width; 8 | this.height = this.canvas.height = this.rect.height; 9 | this.ctx = this.canvas.getContext("2d"); 10 | 11 | // Setting up base dimensions 12 | this.scale = 2; 13 | this.startx = this.width / 2 - 120 / this.scale; 14 | this.starty = this.height / 2 - 100 / this.scale; 15 | 16 | // State 17 | this.angle = 0.0; 18 | 19 | // Propeller rotation 20 | this.propellerState = 0; 21 | this.propellerSpeed = 10; // Rotations per second (adjusted from frame-based to time-based) 22 | this.lastPropellerUpdate = performance.now(); 23 | this.propellerFrames = [ 24 | [ 25 | [80, 0, 40, 10], 26 | [130, 0, 40, 10] 27 | ], 28 | [ 29 | [90, 0, 40, 10], 30 | [120, 0, 40, 10] 31 | ], 32 | [ 33 | [100, 0, 30, 10], 34 | [120, 0, 30, 10] 35 | ], 36 | [ 37 | [120, 0, 40, 10], 38 | [90, 0, 40, 10] 39 | ] 40 | ] 41 | 42 | // Defining plane parts with their colors and coordinates for rendering 43 | this.parts = { 44 | outer: { 45 | color: 'rgb(119, 6, 24)', 46 | coords: [ 47 | [0, 60, 10, 20], 48 | [10, 50, 50, 10], 49 | [60, 40, 50, 10], 50 | [100, 20, 10, 100], 51 | [10, 80, 40, 10], 52 | [50, 90, 30, 10], 53 | [60, 80, 10, 10], 54 | [80, 100, 30, 10], 55 | [70, 70, 30, 10], 56 | [110, 120, 10, 80], 57 | [80, 190, 40, 10], 58 | [90, 170, 30, 10], 59 | [80, 180, 10, 10], 60 | [240, 60, 10, 20], 61 | [190, 50, 50, 10], 62 | [140, 40, 50, 10], 63 | [140, 20, 10, 100], 64 | [200, 80, 40, 10], 65 | [170, 90, 30, 10], 66 | [180, 80, 10, 10], 67 | [150, 100, 20, 10], 68 | [150, 70, 30, 10], 69 | [130, 120, 10, 80], 70 | [130, 190, 40, 10], 71 | [130, 170, 30, 10], 72 | [160, 180, 10, 10], 73 | [120, 200, 10, 10] 74 | ] 75 | }, 76 | innerMain: { 77 | color: 'rgb(172, 50, 46)', 78 | coords: [ 79 | [10, 60, 90, 10], 80 | [10, 70, 60, 10], 81 | [50, 80, 10, 10], 82 | [70, 80, 30, 10], 83 | [80, 90, 20, 10], 84 | [150, 60, 90, 10], 85 | [180, 70, 60, 10], 86 | [190, 80, 10, 10], 87 | [150, 80, 30, 10], 88 | [120, 20, 20, 20], 89 | [130, 40, 10, 10], 90 | [120, 100, 10, 60], 91 | [90, 180, 20, 10] 92 | ] 93 | }, 94 | innerHighlight: { 95 | color: 'rgb(216, 86, 101)', 96 | coords: [ 97 | [60, 50, 40, 10], 98 | [150, 50, 40, 10], 99 | [110, 20, 10, 30], 100 | [110, 90, 10, 30], 101 | [120, 160, 10, 20] 102 | ] 103 | }, 104 | innerDarkHighlight: { 105 | color: 'rgb(140, 3, 8)', 106 | coords: [ 107 | [130, 90, 10, 30], 108 | [150, 90, 20, 10], 109 | [140, 180, 20, 10], 110 | [120, 180, 10, 20] 111 | ] 112 | }, 113 | aroundWindshield: { 114 | color: 'rgb(87, 1, 1)', 115 | coords: [ 116 | [120, 40, 10, 10], 117 | [120, 90, 10, 10], 118 | [110, 50, 10, 40], 119 | [130, 50, 10, 40], 120 | [140, 100, 10, 20] 121 | ] 122 | }, 123 | windshield: { 124 | color: 'rgb(11, 160, 210)', 125 | coords: [ 126 | [120, 50, 10, 40] 127 | ] 128 | }, 129 | propeller: { 130 | color: 'rgb(51, 51, 51)', 131 | coords: [ 132 | [110, 10, 30, 10], 133 | [120, 0, 10, 10] 134 | ] 135 | }, 136 | propellerBlades: { 137 | color: 'rgb(179, 179, 179)', 138 | coords: [ 139 | [80, 0, 40, 10], 140 | [130, 0, 40, 10] 141 | ] 142 | } 143 | }; 144 | 145 | // Shadow height 146 | this.shadowHeight = 0.5; 147 | } 148 | 149 | // Helper function to draw a rectangle with scaling 150 | drawRect(x, y, width, height, color) { 151 | this.ctx.fillStyle = color; 152 | this.ctx.fillRect( 153 | this.startx + x / this.scale, 154 | this.starty + y / this.scale, 155 | width / this.scale, 156 | height / this.scale 157 | ); 158 | } 159 | 160 | drawShadow() { 161 | let shadowHeightSq = this.shadowHeight * this.shadowHeight; 162 | if (shadowHeightSq <= 0.4) { return; } 163 | 164 | this.scale /= shadowHeightSq; 165 | this.ctx.translate( 166 | -32 * this.scale / this.shadowHeight + 48, 167 | 32 * this.scale / this.shadowHeight - 48 168 | ); 169 | 170 | this.ctx.translate(this.width / 2, this.height / 2); 171 | this.ctx.rotate(this.angle); 172 | this.ctx.translate(-this.width / 2, -this.height / 2); 173 | 174 | for (const partName in this.parts) { 175 | const part = this.parts[partName]; 176 | for (const coord of part.coords) { 177 | this.drawRect( 178 | ...coord, 179 | `rgba(0, 0, 0, ${shadowHeightSq * 0.5})` 180 | ); 181 | } 182 | } 183 | 184 | this.scale *= this.shadowHeight * this.shadowHeight; 185 | } 186 | 187 | drawPlane() { 188 | // Iterating over each plane part to draw it 189 | for (const partName in this.parts) { 190 | const part = this.parts[partName]; 191 | for (const coord of part.coords) { 192 | this.drawRect(...coord, part.color); 193 | } 194 | } 195 | } 196 | 197 | // Main draw function to render the plane 198 | draw() { 199 | // Clearing the canvas before drawing 200 | this.ctx.clearRect(0, 0, this.width, this.height); 201 | 202 | this.drawShadow(); 203 | this.ctx.resetTransform(); 204 | 205 | this.ctx.translate(this.width / 2, this.height / 2); 206 | this.ctx.rotate(this.angle); 207 | this.ctx.translate(-this.width / 2, -this.height / 2); 208 | this.drawPlane(); 209 | this.ctx.resetTransform(); 210 | } 211 | 212 | // Function to rotate the plane 213 | rotate(angle) { 214 | // this.ctx.restore(); 215 | // this.ctx.save(); 216 | // this.ctx.translate(this.width / 2, this.height / 2); 217 | // this.ctx.rotate(angle); 218 | // this.ctx.translate(-this.width / 2, -this.height / 2); 219 | // this.draw(); 220 | this.angle = angle; 221 | this.draw(); 222 | } 223 | 224 | // Function to rotate the propeller 225 | rotatePropeller() { 226 | const currentTime = performance.now(); 227 | const deltaTime = (currentTime - this.lastPropellerUpdate) / 1000; // Convert to seconds 228 | this.lastPropellerUpdate = currentTime; 229 | 230 | // Update propeller state based on time elapsed 231 | this.propellerState = (this.propellerState + this.propellerSpeed * deltaTime) % this.propellerFrames.length; 232 | this.parts.propellerBlades.coords = this.propellerFrames[Math.floor(this.propellerState)]; 233 | this.draw(); 234 | } 235 | 236 | // Function to change the color of a plane part 237 | setColor(partName, newColor) { 238 | if (this.parts[partName]) { 239 | this.parts[partName].color = newColor; 240 | this.draw(); 241 | } 242 | } 243 | 244 | // Function to adjust the shadow height 245 | setShadowHeight(newHeight) { 246 | this.shadowHeight = newHeight; 247 | this.draw(); 248 | } 249 | 250 | resetDefaults() { 251 | document.getElementById("outer").value = this.parts["outer"].color = '#770619'; 252 | document.getElementById("innerMain").value = this.parts["innerMain"].color = '#ac322e'; 253 | document.getElementById("innerHighlight").value = this.parts["innerHighlight"].color = '#d85665'; 254 | document.getElementById("innerDarkHighlight").value = this.parts["innerDarkHighlight"].color = '#8c0308'; 255 | document.getElementById("aroundWindshield").value = this.parts["aroundWindshield"].color = '#570101'; 256 | document.getElementById("windshield").value = this.parts["windshield"].color = '#0ba0d2'; 257 | document.getElementById("propeller").value = this.parts["propeller"].color = '#333333'; 258 | document.getElementById("propellerBlades").value = this.parts["propellerBlades"].color = '#b3b3b3'; 259 | this.draw(); 260 | } 261 | } 262 | 263 | // Instantiating the plane and drawing it for the first time 264 | const plane = new Plane("planeCanvas"); 265 | // loading colors from local storage 266 | if (localStorage.getItem("littlePlaneColors")) { 267 | let colors = localStorage.getItem("littlePlaneColors"); 268 | colors = colors.split(","); 269 | colors = colors.slice(1); // remove null at start of array 270 | for (let color of colors) { 271 | // later colors overwrites the previous ones 272 | let values = color.split(":"); 273 | plane.setColor(values[0], values[1]); 274 | // overwrites the default values of color UI` 275 | document.getElementById(values[0]).value = values[1]; 276 | } 277 | } 278 | plane.draw(); 279 | 280 | // Always rotate the propeller using requestAnimationFrame for smooth, frame-rate independent animation 281 | function animatePropeller() { 282 | plane.rotatePropeller(); 283 | requestAnimationFrame(animatePropeller); 284 | } 285 | animatePropeller(); 286 | --------------------------------------------------------------------------------